diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f7925ec --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +/.git +/.github diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3dcffab --- /dev/null +++ b/.editorconfig @@ -0,0 +1,905 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = false +ij_smart_tabs = false +ij_visual_guides = none +ij_wrap_on_typing = false + +[*.blade.php] +ij_blade_keep_indents_on_empty_lines = false + +[*.css] +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_brace_placement = end_of_line +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true +ij_css_value_alignment = do_not_align + +[*.feature] +indent_size = 2 +ij_gherkin_keep_indents_on_empty_lines = false + +[*.haml] +indent_size = 2 +ij_haml_keep_indents_on_empty_lines = false + +[*.less] +indent_size = 2 +ij_less_align_closing_brace_with_properties = false +ij_less_blank_lines_around_nested_selector = 1 +ij_less_blank_lines_between_blocks = 1 +ij_less_brace_placement = 0 +ij_less_enforce_quotes_on_format = false +ij_less_hex_color_long_format = false +ij_less_hex_color_lower_case = false +ij_less_hex_color_short_format = false +ij_less_hex_color_upper_case = false +ij_less_keep_blank_lines_in_code = 2 +ij_less_keep_indents_on_empty_lines = false +ij_less_keep_single_line_blocks = false +ij_less_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_less_space_after_colon = true +ij_less_space_before_opening_brace = true +ij_less_use_double_quotes = true +ij_less_value_alignment = 0 + +[*.rs] +max_line_length = 100 +ij_continuation_indent_size = 4 +ij_rust_align_multiline_chained_methods = false +ij_rust_align_multiline_parameters = true +ij_rust_align_multiline_parameters_in_calls = true +ij_rust_align_ret_type = true +ij_rust_align_type_params = false +ij_rust_align_where_bounds = true +ij_rust_align_where_clause = false +ij_rust_allow_one_line_match = false +ij_rust_block_comment_at_first_column = false +ij_rust_indent_where_clause = true +ij_rust_keep_blank_lines_in_code = 2 +ij_rust_keep_blank_lines_in_declarations = 2 +ij_rust_keep_indents_on_empty_lines = false +ij_rust_keep_line_breaks = true +ij_rust_line_comment_add_space = true +ij_rust_line_comment_at_first_column = false +ij_rust_min_number_of_blanks_between_items = 1 +ij_rust_preserve_punctuation = false +ij_rust_spaces_around_assoc_type_binding = false + +[*.sass] +indent_size = 2 +ij_sass_align_closing_brace_with_properties = false +ij_sass_blank_lines_around_nested_selector = 1 +ij_sass_blank_lines_between_blocks = 1 +ij_sass_brace_placement = 0 +ij_sass_enforce_quotes_on_format = false +ij_sass_hex_color_long_format = false +ij_sass_hex_color_lower_case = false +ij_sass_hex_color_short_format = false +ij_sass_hex_color_upper_case = false +ij_sass_keep_blank_lines_in_code = 2 +ij_sass_keep_indents_on_empty_lines = false +ij_sass_keep_single_line_blocks = false +ij_sass_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_sass_space_after_colon = true +ij_sass_space_before_opening_brace = true +ij_sass_use_double_quotes = true +ij_sass_value_alignment = 0 + +[*.scss] +indent_size = 2 +ij_scss_align_closing_brace_with_properties = false +ij_scss_blank_lines_around_nested_selector = 1 +ij_scss_blank_lines_between_blocks = 1 +ij_scss_brace_placement = 0 +ij_scss_enforce_quotes_on_format = false +ij_scss_hex_color_long_format = false +ij_scss_hex_color_lower_case = false +ij_scss_hex_color_short_format = false +ij_scss_hex_color_upper_case = false +ij_scss_keep_blank_lines_in_code = 2 +ij_scss_keep_indents_on_empty_lines = false +ij_scss_keep_single_line_blocks = false +ij_scss_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_scss_space_after_colon = true +ij_scss_space_before_opening_brace = true +ij_scss_use_double_quotes = true +ij_scss_value_alignment = 0 + +[*.twig] +ij_twig_keep_indents_on_empty_lines = false +ij_twig_spaces_inside_comments_delimiters = true +ij_twig_spaces_inside_delimiters = true +ij_twig_spaces_inside_variable_delimiters = true + +[*.vue] +indent_size = 4 +tab_width = 4 +ij_continuation_indent_size = 4 +ij_vue_indent_children_of_top_level = template +ij_vue_interpolation_new_line_after_start_delimiter = true +ij_vue_interpolation_new_line_before_end_delimiter = true +ij_vue_interpolation_wrap = off +ij_vue_keep_indents_on_empty_lines = false +ij_vue_spaces_within_interpolation_expressions = true + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul,phpunit.xml.dist}] +ij_xml_align_attributes = true +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = true +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = false +ij_xml_text_wrap = normal + +[{*.ats,*.ts}] +ij_continuation_indent_size = 4 +ij_typescript_align_imports = false +ij_typescript_align_multiline_array_initializer_expression = false +ij_typescript_align_multiline_binary_operation = false +ij_typescript_align_multiline_chained_methods = false +ij_typescript_align_multiline_extends_list = false +ij_typescript_align_multiline_for = true +ij_typescript_align_multiline_parameters = true +ij_typescript_align_multiline_parameters_in_calls = false +ij_typescript_align_multiline_ternary_operation = false +ij_typescript_align_object_properties = 0 +ij_typescript_align_union_types = false +ij_typescript_align_var_statements = 0 +ij_typescript_array_initializer_new_line_after_left_brace = false +ij_typescript_array_initializer_right_brace_on_new_line = false +ij_typescript_array_initializer_wrap = off +ij_typescript_assignment_wrap = off +ij_typescript_binary_operation_sign_on_next_line = false +ij_typescript_binary_operation_wrap = off +ij_typescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_typescript_blank_lines_after_imports = 1 +ij_typescript_blank_lines_around_class = 1 +ij_typescript_blank_lines_around_field = 0 +ij_typescript_blank_lines_around_field_in_interface = 0 +ij_typescript_blank_lines_around_function = 1 +ij_typescript_blank_lines_around_method = 1 +ij_typescript_blank_lines_around_method_in_interface = 1 +ij_typescript_block_brace_style = end_of_line +ij_typescript_call_parameters_new_line_after_left_paren = false +ij_typescript_call_parameters_right_paren_on_new_line = false +ij_typescript_call_parameters_wrap = off +ij_typescript_catch_on_new_line = false +ij_typescript_chained_call_dot_on_new_line = true +ij_typescript_class_brace_style = end_of_line +ij_typescript_comma_on_new_line = false +ij_typescript_do_while_brace_force = never +ij_typescript_else_on_new_line = false +ij_typescript_enforce_trailing_comma = keep +ij_typescript_extends_keyword_wrap = off +ij_typescript_extends_list_wrap = off +ij_typescript_field_prefix = _ +ij_typescript_file_name_style = relaxed +ij_typescript_finally_on_new_line = false +ij_typescript_for_brace_force = never +ij_typescript_for_statement_new_line_after_left_paren = false +ij_typescript_for_statement_right_paren_on_new_line = false +ij_typescript_for_statement_wrap = off +ij_typescript_force_quote_style = false +ij_typescript_force_semicolon_style = false +ij_typescript_function_expression_brace_style = end_of_line +ij_typescript_if_brace_force = never +ij_typescript_import_merge_members = global +ij_typescript_import_prefer_absolute_path = global +ij_typescript_import_sort_members = true +ij_typescript_import_sort_module_name = false +ij_typescript_import_use_node_resolution = true +ij_typescript_imports_wrap = on_every_item +ij_typescript_indent_case_from_switch = true +ij_typescript_indent_chained_calls = true +ij_typescript_indent_package_children = 0 +ij_typescript_jsdoc_include_types = false +ij_typescript_jsx_attribute_value = braces +ij_typescript_keep_blank_lines_in_code = 2 +ij_typescript_keep_first_column_comment = true +ij_typescript_keep_indents_on_empty_lines = false +ij_typescript_keep_line_breaks = true +ij_typescript_keep_simple_blocks_in_one_line = false +ij_typescript_keep_simple_methods_in_one_line = false +ij_typescript_line_comment_add_space = true +ij_typescript_line_comment_at_first_column = false +ij_typescript_method_brace_style = end_of_line +ij_typescript_method_call_chain_wrap = off +ij_typescript_method_parameters_new_line_after_left_paren = false +ij_typescript_method_parameters_right_paren_on_new_line = false +ij_typescript_method_parameters_wrap = off +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = false +ij_typescript_parentheses_expression_right_paren_on_new_line = false +ij_typescript_place_assignment_sign_on_next_line = false +ij_typescript_prefer_as_type_cast = false +ij_typescript_prefer_explicit_types_function_expression_returns = false +ij_typescript_prefer_explicit_types_function_returns = false +ij_typescript_prefer_explicit_types_vars_fields = false +ij_typescript_prefer_parameters_wrap = false +ij_typescript_reformat_c_style_comments = false +ij_typescript_space_after_colon = true +ij_typescript_space_after_comma = true +ij_typescript_space_after_dots_in_rest_parameter = false +ij_typescript_space_after_generator_mult = true +ij_typescript_space_after_property_colon = true +ij_typescript_space_after_quest = true +ij_typescript_space_after_type_colon = true +ij_typescript_space_after_unary_not = false +ij_typescript_space_before_async_arrow_lparen = true +ij_typescript_space_before_catch_keyword = true +ij_typescript_space_before_catch_left_brace = true +ij_typescript_space_before_catch_parentheses = true +ij_typescript_space_before_class_lbrace = true +ij_typescript_space_before_class_left_brace = true +ij_typescript_space_before_colon = true +ij_typescript_space_before_comma = false +ij_typescript_space_before_do_left_brace = true +ij_typescript_space_before_else_keyword = true +ij_typescript_space_before_else_left_brace = true +ij_typescript_space_before_finally_keyword = true +ij_typescript_space_before_finally_left_brace = true +ij_typescript_space_before_for_left_brace = true +ij_typescript_space_before_for_parentheses = true +ij_typescript_space_before_for_semicolon = false +ij_typescript_space_before_function_left_parenth = true +ij_typescript_space_before_generator_mult = false +ij_typescript_space_before_if_left_brace = true +ij_typescript_space_before_if_parentheses = true +ij_typescript_space_before_method_call_parentheses = false +ij_typescript_space_before_method_left_brace = true +ij_typescript_space_before_method_parentheses = false +ij_typescript_space_before_property_colon = false +ij_typescript_space_before_quest = true +ij_typescript_space_before_switch_left_brace = true +ij_typescript_space_before_switch_parentheses = true +ij_typescript_space_before_try_left_brace = true +ij_typescript_space_before_type_colon = false +ij_typescript_space_before_unary_not = false +ij_typescript_space_before_while_keyword = true +ij_typescript_space_before_while_left_brace = true +ij_typescript_space_before_while_parentheses = true +ij_typescript_spaces_around_additive_operators = true +ij_typescript_spaces_around_arrow_function_operator = true +ij_typescript_spaces_around_assignment_operators = true +ij_typescript_spaces_around_bitwise_operators = true +ij_typescript_spaces_around_equality_operators = true +ij_typescript_spaces_around_logical_operators = true +ij_typescript_spaces_around_multiplicative_operators = true +ij_typescript_spaces_around_relational_operators = true +ij_typescript_spaces_around_shift_operators = true +ij_typescript_spaces_around_unary_operator = false +ij_typescript_spaces_within_array_initializer_brackets = false +ij_typescript_spaces_within_brackets = false +ij_typescript_spaces_within_catch_parentheses = false +ij_typescript_spaces_within_for_parentheses = false +ij_typescript_spaces_within_if_parentheses = false +ij_typescript_spaces_within_imports = false +ij_typescript_spaces_within_interpolation_expressions = false +ij_typescript_spaces_within_method_call_parentheses = false +ij_typescript_spaces_within_method_parentheses = false +ij_typescript_spaces_within_object_literal_braces = false +ij_typescript_spaces_within_object_type_braces = true +ij_typescript_spaces_within_parentheses = false +ij_typescript_spaces_within_switch_parentheses = false +ij_typescript_spaces_within_type_assertion = false +ij_typescript_spaces_within_union_types = true +ij_typescript_spaces_within_while_parentheses = false +ij_typescript_special_else_if_treatment = true +ij_typescript_ternary_operation_signs_on_next_line = false +ij_typescript_ternary_operation_wrap = off +ij_typescript_union_types_wrap = on_every_item +ij_typescript_use_chained_calls_group_indents = false +ij_typescript_use_double_quotes = true +ij_typescript_use_explicit_js_extension = global +ij_typescript_use_path_mapping = always +ij_typescript_use_public_modifier = false +ij_typescript_use_semicolon_after_statement = true +ij_typescript_var_declaration_wrap = normal +ij_typescript_while_brace_force = never +ij_typescript_while_on_new_line = false +ij_typescript_wrap_comments = false + +[{*.bash,*.bats,*.dash,*.ksh,*.mksh,*.sh,*.zsh,.bash_aliases,.bash_logout,.bash_profile,.bashrc,.profile,cli_with_proxy}] +indent_size = 2 +tab_width = 2 +ij_shell_binary_ops_start_line = false +ij_shell_function_brace_newline = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_simplify_code = false +ij_shell_switch_cases_indented = false +ij_shell_unix_line_feeds = true +ij_shell_use_google_code_style = false + +[{*.cjs,*.js}] +ij_continuation_indent_size = 4 +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = false +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = true +ij_javascript_align_multiline_parameters = true +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = false +ij_javascript_array_initializer_right_brace_on_new_line = false +ij_javascript_array_initializer_wrap = off +ij_javascript_assignment_wrap = off +ij_javascript_binary_operation_sign_on_next_line = false +ij_javascript_binary_operation_wrap = off +ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = off +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = end_of_line +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = never +ij_javascript_else_on_new_line = false +ij_javascript_enforce_trailing_comma = keep +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = never +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = off +ij_javascript_force_quote_style = false +ij_javascript_force_semicolon_style = false +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_if_brace_force = never +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 2 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = false +ij_javascript_keep_simple_methods_in_one_line = false +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = off +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = off +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_explicit_types_function_expression_returns = false +ij_javascript_prefer_explicit_types_function_returns = false +ij_javascript_prefer_explicit_types_vars_fields = false +ij_javascript_prefer_parameters_wrap = false +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = false +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = false +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = false +ij_javascript_ternary_operation_wrap = off +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = true +ij_javascript_use_explicit_js_extension = global +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = never +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.cjsx,*.coffee}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_coffeescript_align_function_body = false +ij_coffeescript_align_imports = false +ij_coffeescript_align_multiline_array_initializer_expression = true +ij_coffeescript_align_multiline_parameters = true +ij_coffeescript_align_multiline_parameters_in_calls = false +ij_coffeescript_align_object_properties = 0 +ij_coffeescript_align_union_types = false +ij_coffeescript_align_var_statements = 0 +ij_coffeescript_array_initializer_new_line_after_left_brace = false +ij_coffeescript_array_initializer_right_brace_on_new_line = false +ij_coffeescript_array_initializer_wrap = normal +ij_coffeescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_coffeescript_blank_lines_around_function = 1 +ij_coffeescript_call_parameters_new_line_after_left_paren = false +ij_coffeescript_call_parameters_right_paren_on_new_line = false +ij_coffeescript_call_parameters_wrap = normal +ij_coffeescript_chained_call_dot_on_new_line = true +ij_coffeescript_comma_on_new_line = false +ij_coffeescript_enforce_trailing_comma = keep +ij_coffeescript_field_prefix = _ +ij_coffeescript_file_name_style = relaxed +ij_coffeescript_force_quote_style = false +ij_coffeescript_force_semicolon_style = false +ij_coffeescript_function_expression_brace_style = end_of_line +ij_coffeescript_import_merge_members = global +ij_coffeescript_import_prefer_absolute_path = global +ij_coffeescript_import_sort_members = true +ij_coffeescript_import_sort_module_name = false +ij_coffeescript_import_use_node_resolution = true +ij_coffeescript_imports_wrap = on_every_item +ij_coffeescript_indent_chained_calls = true +ij_coffeescript_indent_package_children = 0 +ij_coffeescript_jsx_attribute_value = braces +ij_coffeescript_keep_blank_lines_in_code = 2 +ij_coffeescript_keep_first_column_comment = true +ij_coffeescript_keep_indents_on_empty_lines = false +ij_coffeescript_keep_line_breaks = true +ij_coffeescript_keep_simple_methods_in_one_line = false +ij_coffeescript_method_parameters_new_line_after_left_paren = false +ij_coffeescript_method_parameters_right_paren_on_new_line = false +ij_coffeescript_method_parameters_wrap = off +ij_coffeescript_object_literal_wrap = on_every_item +ij_coffeescript_prefer_as_type_cast = false +ij_coffeescript_prefer_explicit_types_function_expression_returns = false +ij_coffeescript_prefer_explicit_types_function_returns = false +ij_coffeescript_prefer_explicit_types_vars_fields = false +ij_coffeescript_reformat_c_style_comments = false +ij_coffeescript_space_after_comma = true +ij_coffeescript_space_after_dots_in_rest_parameter = false +ij_coffeescript_space_after_generator_mult = true +ij_coffeescript_space_after_property_colon = true +ij_coffeescript_space_after_type_colon = true +ij_coffeescript_space_after_unary_not = false +ij_coffeescript_space_before_async_arrow_lparen = true +ij_coffeescript_space_before_class_lbrace = true +ij_coffeescript_space_before_comma = false +ij_coffeescript_space_before_function_left_parenth = true +ij_coffeescript_space_before_generator_mult = false +ij_coffeescript_space_before_property_colon = false +ij_coffeescript_space_before_type_colon = false +ij_coffeescript_space_before_unary_not = false +ij_coffeescript_spaces_around_additive_operators = true +ij_coffeescript_spaces_around_arrow_function_operator = true +ij_coffeescript_spaces_around_assignment_operators = true +ij_coffeescript_spaces_around_bitwise_operators = true +ij_coffeescript_spaces_around_equality_operators = true +ij_coffeescript_spaces_around_logical_operators = true +ij_coffeescript_spaces_around_multiplicative_operators = true +ij_coffeescript_spaces_around_relational_operators = true +ij_coffeescript_spaces_around_shift_operators = true +ij_coffeescript_spaces_around_unary_operator = false +ij_coffeescript_spaces_within_array_initializer_braces = false +ij_coffeescript_spaces_within_array_initializer_brackets = false +ij_coffeescript_spaces_within_imports = false +ij_coffeescript_spaces_within_index_brackets = false +ij_coffeescript_spaces_within_interpolation_expressions = false +ij_coffeescript_spaces_within_method_call_parentheses = false +ij_coffeescript_spaces_within_method_parentheses = false +ij_coffeescript_spaces_within_object_braces = false +ij_coffeescript_spaces_within_object_literal_braces = false +ij_coffeescript_spaces_within_object_type_braces = true +ij_coffeescript_spaces_within_range_brackets = false +ij_coffeescript_spaces_within_type_assertion = false +ij_coffeescript_spaces_within_union_types = true +ij_coffeescript_union_types_wrap = on_every_item +ij_coffeescript_use_chained_calls_group_indents = false +ij_coffeescript_use_double_quotes = true +ij_coffeescript_use_explicit_js_extension = global +ij_coffeescript_use_path_mapping = always +ij_coffeescript_use_public_modifier = false +ij_coffeescript_use_semicolon_after_statement = false +ij_coffeescript_var_declaration_wrap = normal + +[{*.ctp,*.hphp,*.inc,*.module,*.php,*.php4,*.php5,*.phtml,metadata}] +ij_continuation_indent_size = 4 +ij_php_align_assignments = false +ij_php_align_class_constants = false +ij_php_align_group_field_declarations = false +ij_php_align_inline_comments = false +ij_php_align_key_value_pairs = false +ij_php_align_match_arm_bodies = false +ij_php_align_multiline_array_initializer_expression = false +ij_php_align_multiline_binary_operation = false +ij_php_align_multiline_chained_methods = false +ij_php_align_multiline_extends_list = true +ij_php_align_multiline_for = true +ij_php_align_multiline_parameters = false +ij_php_align_multiline_parameters_in_calls = false +ij_php_align_multiline_ternary_operation = false +ij_php_align_named_arguments = false +ij_php_align_phpdoc_comments = false +ij_php_align_phpdoc_param_names = false +ij_php_anonymous_brace_style = end_of_line +ij_php_api_weight = 28 +ij_php_array_initializer_new_line_after_left_brace = true +ij_php_array_initializer_right_brace_on_new_line = true +ij_php_array_initializer_wrap = on_every_item +ij_php_assignment_wrap = off +ij_php_attributes_wrap = off +ij_php_author_weight = 28 +ij_php_binary_operation_sign_on_next_line = false +ij_php_binary_operation_wrap = off +ij_php_blank_lines_after_class_header = 0 +ij_php_blank_lines_after_function = 1 +ij_php_blank_lines_after_imports = 1 +ij_php_blank_lines_after_opening_tag = 1 +ij_php_blank_lines_after_package = 1 +ij_php_blank_lines_around_class = 1 +ij_php_blank_lines_around_constants = 0 +ij_php_blank_lines_around_field = 0 +ij_php_blank_lines_around_method = 1 +ij_php_blank_lines_before_class_end = 0 +ij_php_blank_lines_before_imports = 1 +ij_php_blank_lines_before_method_body = 0 +ij_php_blank_lines_before_package = 1 +ij_php_blank_lines_before_return_statement = 0 +ij_php_blank_lines_between_imports = 1 +ij_php_block_brace_style = end_of_line +ij_php_call_parameters_new_line_after_left_paren = true +ij_php_call_parameters_right_paren_on_new_line = true +ij_php_call_parameters_wrap = on_every_item +ij_php_catch_on_new_line = false +ij_php_category_weight = 28 +ij_php_class_brace_style = next_line +ij_php_comma_after_last_array_element = true +ij_php_concat_spaces = true +ij_php_copyright_weight = 28 +ij_php_deprecated_weight = 28 +ij_php_do_while_brace_force = always +ij_php_else_if_style = combine +ij_php_else_on_new_line = false +ij_php_example_weight = 28 +ij_php_extends_keyword_wrap = off +ij_php_extends_list_wrap = on_every_item +ij_php_fields_default_visibility = protected +ij_php_filesource_weight = 28 +ij_php_finally_on_new_line = false +ij_php_for_brace_force = always +ij_php_for_statement_new_line_after_left_paren = true +ij_php_for_statement_right_paren_on_new_line = true +ij_php_for_statement_wrap = off +ij_php_force_short_declaration_array_style = true +ij_php_getters_setters_naming_style = camel_case +ij_php_getters_setters_order_style = getters_first +ij_php_global_weight = 28 +ij_php_group_use_wrap = on_every_item +ij_php_if_brace_force = always +ij_php_if_lparen_on_next_line = false +ij_php_if_rparen_on_next_line = false +ij_php_ignore_weight = 28 +ij_php_import_sorting = alphabetic +ij_php_indent_break_from_case = true +ij_php_indent_case_from_switch = true +ij_php_indent_code_in_php_tags = false +ij_php_internal_weight = 28 +ij_php_keep_blank_lines_after_lbrace = 0 +ij_php_keep_blank_lines_before_right_brace = 0 +ij_php_keep_blank_lines_in_code = 2 +ij_php_keep_blank_lines_in_declarations = 2 +ij_php_keep_control_statement_in_one_line = true +ij_php_keep_first_column_comment = true +ij_php_keep_indents_on_empty_lines = false +ij_php_keep_line_breaks = true +ij_php_keep_rparen_and_lbrace_on_one_line = true +ij_php_keep_simple_classes_in_one_line = false +ij_php_keep_simple_methods_in_one_line = false +ij_php_lambda_brace_style = end_of_line +ij_php_license_weight = 28 +ij_php_line_comment_add_space = false +ij_php_line_comment_at_first_column = true +ij_php_link_weight = 28 +ij_php_lower_case_boolean_const = true +ij_php_lower_case_keywords = true +ij_php_lower_case_null_const = true +ij_php_method_brace_style = next_line +ij_php_method_call_chain_wrap = on_every_item +ij_php_method_parameters_new_line_after_left_paren = true +ij_php_method_parameters_right_paren_on_new_line = true +ij_php_method_parameters_wrap = on_every_item +ij_php_method_weight = 28 +ij_php_modifier_list_wrap = false +ij_php_multiline_chained_calls_semicolon_on_new_line = false +ij_php_namespace_brace_style = 1 +ij_php_new_line_after_php_opening_tag = true +ij_php_null_type_position = in_the_end +ij_php_package_weight = 28 +ij_php_param_weight = 0 +ij_php_parameters_attributes_wrap = off +ij_php_parentheses_expression_new_line_after_left_paren = false +ij_php_parentheses_expression_right_paren_on_new_line = false +ij_php_phpdoc_blank_line_before_tags = false +ij_php_phpdoc_blank_lines_around_parameters = false +ij_php_phpdoc_keep_blank_lines = true +ij_php_phpdoc_param_spaces_between_name_and_description = 1 +ij_php_phpdoc_param_spaces_between_tag_and_type = 1 +ij_php_phpdoc_param_spaces_between_type_and_name = 1 +ij_php_phpdoc_use_fqcn = false +ij_php_phpdoc_wrap_long_lines = false +ij_php_place_assignment_sign_on_next_line = false +ij_php_place_parens_for_constructor = 0 +ij_php_property_read_weight = 28 +ij_php_property_weight = 28 +ij_php_property_write_weight = 28 +ij_php_return_type_on_new_line = false +ij_php_return_weight = 1 +ij_php_see_weight = 28 +ij_php_since_weight = 28 +ij_php_sort_phpdoc_elements = true +ij_php_space_after_colon = true +ij_php_space_after_colon_in_enum_backed_type = true +ij_php_space_after_colon_in_named_argument = true +ij_php_space_after_colon_in_return_type = true +ij_php_space_after_comma = true +ij_php_space_after_for_semicolon = true +ij_php_space_after_quest = true +ij_php_space_after_type_cast = false +ij_php_space_after_unary_not = false +ij_php_space_before_array_initializer_left_brace = false +ij_php_space_before_catch_keyword = true +ij_php_space_before_catch_left_brace = true +ij_php_space_before_catch_parentheses = true +ij_php_space_before_class_left_brace = true +ij_php_space_before_closure_left_parenthesis = true +ij_php_space_before_colon = true +ij_php_space_before_colon_in_enum_backed_type = false +ij_php_space_before_colon_in_named_argument = false +ij_php_space_before_colon_in_return_type = false +ij_php_space_before_comma = false +ij_php_space_before_do_left_brace = true +ij_php_space_before_else_keyword = true +ij_php_space_before_else_left_brace = true +ij_php_space_before_finally_keyword = true +ij_php_space_before_finally_left_brace = true +ij_php_space_before_for_left_brace = true +ij_php_space_before_for_parentheses = true +ij_php_space_before_for_semicolon = false +ij_php_space_before_if_left_brace = true +ij_php_space_before_if_parentheses = true +ij_php_space_before_method_call_parentheses = false +ij_php_space_before_method_left_brace = true +ij_php_space_before_method_parentheses = false +ij_php_space_before_quest = true +ij_php_space_before_short_closure_left_parenthesis = false +ij_php_space_before_switch_left_brace = true +ij_php_space_before_switch_parentheses = true +ij_php_space_before_try_left_brace = true +ij_php_space_before_unary_not = false +ij_php_space_before_while_keyword = true +ij_php_space_before_while_left_brace = true +ij_php_space_before_while_parentheses = true +ij_php_space_between_ternary_quest_and_colon = false +ij_php_spaces_around_additive_operators = true +ij_php_spaces_around_arrow = false +ij_php_spaces_around_assignment_in_declare = false +ij_php_spaces_around_assignment_operators = true +ij_php_spaces_around_bitwise_operators = true +ij_php_spaces_around_equality_operators = true +ij_php_spaces_around_logical_operators = true +ij_php_spaces_around_multiplicative_operators = true +ij_php_spaces_around_null_coalesce_operator = true +ij_php_spaces_around_pipe_in_union_type = false +ij_php_spaces_around_relational_operators = true +ij_php_spaces_around_shift_operators = true +ij_php_spaces_around_unary_operator = false +ij_php_spaces_around_var_within_brackets = false +ij_php_spaces_within_array_initializer_braces = false +ij_php_spaces_within_brackets = false +ij_php_spaces_within_catch_parentheses = false +ij_php_spaces_within_for_parentheses = false +ij_php_spaces_within_if_parentheses = false +ij_php_spaces_within_method_call_parentheses = false +ij_php_spaces_within_method_parentheses = false +ij_php_spaces_within_parentheses = false +ij_php_spaces_within_short_echo_tags = true +ij_php_spaces_within_switch_parentheses = false +ij_php_spaces_within_while_parentheses = false +ij_php_special_else_if_treatment = false +ij_php_subpackage_weight = 28 +ij_php_ternary_operation_signs_on_next_line = false +ij_php_ternary_operation_wrap = off +ij_php_throws_weight = 2 +ij_php_todo_weight = 28 +ij_php_unknown_tag_weight = 28 +ij_php_upper_case_boolean_const = false +ij_php_upper_case_null_const = false +ij_php_uses_weight = 28 +ij_php_var_weight = 28 +ij_php_variable_naming_style = camel_case +ij_php_version_weight = 28 +ij_php_while_brace_force = always +ij_php_while_on_new_line = false + +[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,composer.lock,jest.config}] +indent_size = 2 +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = false +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.htm,*.html,*.ng,*.sht,*.shtm,*.shtml}] +ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = normal +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p +ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot +ij_html_enforce_quotes = false +ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var +ij_html_keep_blank_lines = 2 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span,pre,textarea,translate +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = never +ij_html_new_line_before_first_attribute = never +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = false +ij_html_text_wrap = normal + +[{*.markdown,*.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 + +[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}] +ij_toml_keep_indents_on_empty_lines = false + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true + +[*.neon] +indent_style = tab diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..041ba5e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Force LF line ending on shell scripts +Dockerfile text eol=lf +*.cnf text eol=lf +*.sh text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.json text eol=lf + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary \ No newline at end of file diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml new file mode 100644 index 0000000..9e87197 --- /dev/null +++ b/.github/workflows/test-and-deploy.yml @@ -0,0 +1,78 @@ +name: Test and Deploy + +on: [ push, pull_request, workflow_dispatch ] + +jobs: + build: + name: Build and Test + runs-on: self-hosted + steps: + - uses: actions/checkout@master + + - name: Cache PHP dependencies + uses: actions/cache@v4 + with: + path: www/vendor + key: ${{ runner.OS }}-build-${{ hashFiles('**/composer.lock') }} + + - name: Run Composer install + run: | + composer install --no-interaction --ignore-platform-reqs + composer require staabm/annotate-pull-request-from-checkstyle + + - name : Run PHP Linter + run : | + vendor/bin/parallel-lint . --exclude vendor --checkstyle | vendor/bin/cs2pr + + - name : Run PHPStan + run : | + vendor/bin/phpstan analyze --error-format=checkstyle | vendor/bin/cs2pr + + - name : Run PHP Code Sniffer + run : | + vendor/bin/phpcs --report=checkstyle | vendor/bin/cs2pr + + publish: + name: Publish + needs: build + runs-on: self-hosted + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + steps: + - uses: actions/checkout@master + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Docker Metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/waterwolfdev/waterwolf-site + tags: | + type=ref,event=branch + + - name: Debug Docker Metadata + run: | + echo "Tags: ${{ steps.meta.outputs.tags }}" + echo "GitHub Ref: ${{ github.ref }}" + echo "Default Branch: ${{ github.event.repository.default_branch }}" + echo "GitHub Event Name: ${{ github.event_name }}" + + - name: Publish to Docker Image Repo + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/amd64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=ghcr.io/waterwolfdev/waterwolf-site:buildcache,mode=max + cache-to: type=registry,ref=ghcr.io/waterwolfdev/waterwolf-site:buildcache,mode=max diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c730f58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Compiled Code +/web/static/dist/* + +# User Uploaded Content +/web/media/* + +# Local Dev/Editors +/build/dev/db_full.sql +/.idea +/dev.env +/docker-compose.override.yml + +# Packages +/vendor/* +/node_modules/* diff --git a/.phplint.yml b/.phplint.yml new file mode 100644 index 0000000..6879918 --- /dev/null +++ b/.phplint.yml @@ -0,0 +1,8 @@ +path: ./ +jobs: 10 +cache: ../www_tmp/phplint.cache +extensions: + - php + - phtml +exclude: + - vendor diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php new file mode 100644 index 0000000..80b02b5 --- /dev/null +++ b/.phpstorm.meta.php @@ -0,0 +1,13 @@ + '@', + ] + ) + ); +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..300f4f3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,90 @@ +# +# Base build (common steps) +# +FROM php:8.3-fpm-alpine3.19 AS base + +ENV TZ=UTC + +COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ + +RUN install-php-extensions @composer gd curl xml zip mbstring pdo_mysql apcu + +RUN apk add --no-cache zip git curl bash \ + supervisor \ + caddy \ + nodejs npm \ + supercronic \ + su-exec + +# Set up App user +RUN mkdir -p /var/app/www \ + && addgroup -g 1000 app \ + && adduser -u 1000 -G app -h /var/app/ -s /bin/sh -D app \ + && addgroup app www-data \ + && mkdir -p /var/app/media /var/app/www /var/app/www_tmp /run/supervisord /logs \ + && chown -R app:app /var/app /logs + +COPY --chown=app:app ./build/scripts/ /usr/local/bin +RUN chmod a+x /usr/local/bin/* + +COPY ./build/supervisord.conf /etc/supervisord.conf +COPY ./build/services/ /etc/supervisor.d/ + +COPY --chown=app:app ./build/cron /etc/cron.d/app + +COPY ./build/phpfpmpool.conf /usr/local/etc/php-fpm.d/www.conf +COPY ./build/php.ini /usr/local/etc/php/php.ini + +VOLUME ["/var/app/www_tmp"] +VOLUME ["/var/app/media"] + +EXPOSE 8080 + +WORKDIR /var/app/www + +COPY --chown=app:app . . + +# +# Development Build +# +FROM base AS development + +COPY ./build/dev/services/ /etc/supervisor.d/ +COPY ./build/dev/Caddyfile /etc/Caddyfile +COPY ./build/dev/entrypoint.sh /var/app/entrypoint.sh + +RUN chmod a+x /var/app/entrypoint.sh + +USER root + +ENV APPLICATION_ENV=development + +ENTRYPOINT ["/var/app/entrypoint.sh"] +CMD ["supervisord", "-c", "/etc/supervisord.conf"] + +# +# Production Build +# +FROM base AS production + +COPY ./build/prod/Caddyfile /etc/Caddyfile +COPY ./build/prod/entrypoint.sh /var/app/entrypoint.sh + +RUN chmod a+x /var/app/entrypoint.sh + +USER app + +RUN composer install --no-dev --no-ansi --no-autoloader --no-interaction \ + && composer dump-autoload --optimize --classmap-authoritative \ + && composer clear-cache + +RUN npm ci --include=dev \ + && npm run build \ + && npm cache clean --force + +USER root + +ENV APPLICATION_ENV=production + +ENTRYPOINT ["/var/app/entrypoint.sh"] +CMD ["supervisord", "-c", "/etc/supervisord.conf"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..93d93b8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 WaterWolf + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9fab6e6 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +SHELL=/bin/bash +.PHONY: * + +list: + @LC_ALL=C $(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' + +up: + docker-compose up -d + +down: + docker-compose down + +restart: down up + +build: # Rebuild all containers and restart + docker-compose build --no-cache + $(MAKE) restart + +bash: + docker-compose exec --user=app web bash + +bash-root: + docker-compose exec web bash diff --git a/README.md b/README.md new file mode 100644 index 0000000..36f4d50 --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +# WaterWolf Site + +This is a rewritten version of the WaterWolf web site that employs modern security and coding best practices while +still remaining easy to maintain and contribute to for a variety of skill levels. + +See the TODO section for pending work that can be claimed. + +## Production + +This repository is deployed to remote servers via the Docker image it builds as part of its GitHub Actions CI suite. + +See `Dockerfile` for the build details, and the `.github` folder for the GitHub Actions details. + +## Development + +### Live Development Site + +Live dev website can be found [https://dev.waterwolf.club](https://dev.waterwolf.club) + +### Developing Locally + +Developers running [Docker Desktop for Windows](https://www.docker.com/products/docker-desktop/) or MacOS or Docker +for Linux can take advantage of the built-in support for Docker and Docker Compose. + +A `Makefile` also exists to allow easy shorthand access to common commands. You should have `make` installed on your +host OS to take advantage of this file, but all instructions are provided in both formats. + +#### Initial Setup + +Copy `dev.dist.env` to `dev.env` and update it with any missing secrets. + +The database will be created from the DB migrations (in `/db/migrations`) on initial startup. + +The following user accounts are created on local dev, all with the password `WaterWolf!`: + +| UID | Username | E-mail Address | +|-----|------------|--------------------------| +| 1 | User | user@waterwolf.dev | +| 2 | TeamMember | teammember@waterwolf.dev | +| 3 | Moderator | mod@waterwolf.dev | +| 4 | Admin | admin@waterwolf.dev | +| 5 | Banned | banned@waterwolf.dev | + +#### Building the Base Image + +```bash +docker-compose build +# Or +make build +``` + +#### Spinning Up Containers + +```bash +docker-compose up -d +# Or +make up +``` + +Your local instance will be available at https://localhost:8080. + +#### Stopping Containers + +```bash +docker-compose down +# Or +make down +``` + +To spin down all containers **and permanently delete volumes** (like DB data), run: + +```bash +docker-compose down -v +``` + +#### Accessing Bash Shell Inside Container + +As the `app` user: + +```bash +docker-compose exec --user=app web bash +# Or +make bash +``` + +As the `root` user: + +```bash +docker-compose exec --user=app web bash +# Or +make bash-root +``` + +## Asset Hosting + +Static assets used by the web site are stored inside this repository and can be referenced directly via `/static` links. + +User-uploaded content should instead be stored in the media storage subsystem, which resolves in +production to `media.waterwolf.town`. + +`media.waterwolf.town` structure. +- site/ -> `# Website assets` + - css/ + - js/ + - img/ + - video/ + - uploads/ -> `# User generated assets` +- public/ -> `# Long-term file sharing` + - unity/ + - video/ + - img/ + +## TODO + +I have added //// TODO to mark work inline. Use a TODO plugin or search TODO for tasks + +Infrastructure tasks: +- Implement a full local CSS/JS build system including dev hot-reloading using a tool like Vite +- Track database changes in-repo in a schema migration tool + +General tasks: +- Add self-service password reset +- Add Recaptcha to public forms. +- Optimize images, either by offloading to cloudflare or by resizing them. +- Optimize video. Server as webp or something smaller. Consider removing videos from the backgrounds. +- Replace ip/user ban system. +- AJAX forms. diff --git a/backend/bin/console b/backend/bin/console new file mode 100644 index 0000000..1ba0e6a --- /dev/null +++ b/backend/bin/console @@ -0,0 +1,9 @@ +#!/usr/bin/env php +run(); diff --git a/backend/bootstrap/functions.php b/backend/bootstrap/functions.php new file mode 100644 index 0000000..ec7d227 --- /dev/null +++ b/backend/bootstrap/functions.php @@ -0,0 +1,98 @@ +escapeHtmlAttr($htmlAttribute ?? ''); +} + +function escapeJs(mixed $string): string +{ + return json_encode($string, JSON_THROW_ON_ERROR); +} + +function mediaUrl(string $url): string +{ + // Encode individual portions of the URL between slashes. + $url = implode("/", array_map("rawurlencode", explode("/", $url))); + + return Environment::getInstance()->getMediaUrl() . '/' . ltrim($url, '/'); +} + +function mediaPath(string $path): string +{ + $mediaDir = Environment::getInstance()->getMediaPath(); + $mediaPath = Symfony\Component\Filesystem\Path::canonicalize($mediaDir . '/' . ltrim($path, '/')); + + // Check for path traversal and throw if detected. + if (!Symfony\Component\Filesystem\Path::isBasePath($mediaDir, $mediaPath)) { + throw new \InvalidArgumentException('Invalid media path!'); + } + + (new Filesystem())->mkdir(dirname($mediaPath)); + + return $mediaPath; +} + +function avatarUrl(string|bool|null $userImg): string +{ + return (!empty($userImg)) + ? mediaUrl('/img/profile/' . $userImg) + : '/static/img/avatar.webp'; +} + +function djAvatarUrl( + string|bool|null $djImg, + string|bool|null $userImg +): string { + return (!empty($djImg)) + ? mediaUrl('/img/djs/' . $djImg) + : avatarUrl($userImg); +} + +/* + * Input Escaping + */ + +function humanTime(string|int|null $timestamp = "", string $format = 'D, M d, Y \a\t g:i A'): string +{ + if (empty($timestamp) || !is_numeric($timestamp)) { + $timestamp = time(); + } + + return date($format, $timestamp); +} + +function timeAgo(int|null $time): string +{ + if ($time === null) { + return ''; + } + + $periods = ["sec", "min", "hour", "day", "week", "month", "year", "decade"]; + $lengths = ["60", "60", "24", "7", "4.35", "12", "10"]; + + $difference = time() - $time; + for ($j = 0; $difference >= $lengths[$j] && $j < count($lengths) - 1; $j++) { + $difference /= $lengths[$j]; + } + + $difference = round($difference); + if ($difference != 1) { + $periods[$j] .= "s"; + } + + return "$difference $periods[$j] ago"; +} diff --git a/backend/bootstrap/routes.php b/backend/bootstrap/routes.php new file mode 100644 index 0000000..54ac7a7 --- /dev/null +++ b/backend/bootstrap/routes.php @@ -0,0 +1,236 @@ +group('', function (RouteCollectorProxy $group) { + $group->get('/', View::staticPage('index')) + ->setName('home'); + + $group->get('/about', View::staticPage('about')) + ->setName('about'); + + $group->get('/calendar', View::staticPage('calendar')) + ->setName('calendar'); + + $group->group('/dashboard', function (RouteCollectorProxy $group) { + $group->get('', View::staticPage('dashboard/index')) + ->setName('dashboard'); + + $group->group('/admin', function (RouteCollectorProxy $group) { + $group->map( + ['GET', 'POST'], + '/add_world', + App\Controller\Dashboard\Admin\AddWorldAction::class + )->setName('dashboard:admin:add_world'); + + $group->get( + '/users', + App\Controller\Dashboard\Admin\UsersAction::class + )->setName('dashboard:admin:users'); + })->add(new App\Middleware\Auth\RequireAdmin()); + + $group->map( + ['GET', 'POST'], + '/avatar[/{type}]', + App\Controller\Dashboard\AvatarAction::class + )->setName('dashboard:avatar'); + + $group->map( + ['GET', 'POST'], + '/dmx', + App\Controller\Dashboard\DmxController::class + )->setName('dashboard:dmx'); + + $group->map( + ['GET', 'POST'], + '/password', + App\Controller\Dashboard\PasswordAction::class + )->setName('dashboard:password'); + + $group->group('/posters', function (RouteCollectorProxy $group) { + $group->get( + '', + App\Controller\Dashboard\PostersController::class . ':listAction' + )->setName('dashboard:posters'); + + $group->map( + ['GET', 'POST'], + '/create', + App\Controller\Dashboard\PostersController::class . ':createAction' + )->setName('dashboard:posters:create'); + + $group->map( + ['GET', 'POST'], + '/edit[/{id}]', + App\Controller\Dashboard\PostersController::class . ':editAction' + )->setName('dashboard:posters:edit'); + + $group->get( + '/delete[/{id}]', + App\Controller\Dashboard\PostersController::class . ':deleteAction' + )->setName('dashboard:posters:delete'); + }); + + $group->map( + ['GET', 'POST'], + '/profile[/{id}]', + App\Controller\Dashboard\EditProfileAction::class + )->setName('dashboard:profile'); + + $group->group('/short_urls', function (RouteCollectorProxy $group) { + $group->get( + '', + App\Controller\Dashboard\ShortUrlsController::class . ':listAction' + )->setName('dashboard:short_urls'); + + $group->map( + ['GET', 'POST'], + '/create', + App\Controller\Dashboard\ShortUrlsController::class . ':createAction' + )->setName('dashboard:short_urls:create'); + + $group->map( + ['GET', 'POST'], + '/edit[/{id}]', + App\Controller\Dashboard\ShortUrlsController::class . ':editAction' + )->setName('dashboard:short_urls:edit'); + + $group->get( + '/delete[/{id}]', + App\Controller\Dashboard\ShortUrlsController::class . ':deleteAction' + )->setName('dashboard:short_urls:delete'); + })->add(new App\Middleware\Auth\RequireMod()); + + $group->map( + ['GET', 'POST'], + '/skills', + App\Controller\Dashboard\SkillsController::class + )->setName('dashboard:skills'); + })->add(new App\Middleware\Auth\RequireLoggedIn()); + + $group->get('/defective', View::staticPage('defective/index')) + ->setName('defective'); + + $group->get('/donate', View::staticPage('donate')) + ->setName('donate'); + + $group->map(['GET', 'POST'], '/forgot', App\Controller\Account\ForgotAction::class) + ->setName('forgot'); + + $group->group('/foxxcon', function (RouteCollectorProxy $group) { + $group->get('', View::staticPage('foxxcon/index')) + ->setName('foxxcon'); + + $group->get('/instances', View::staticPage('foxxcon/instances')) + ->setName('foxxcon:instances'); + }); + + $group->get('/live', View::staticPage('live')) + ->setName('live'); + + $group->map(['GET', 'POST'], '/login', App\Controller\Account\LoginAction::class) + ->setName('login'); + + $group->get('/logout', App\Controller\Account\LogoutAction::class) + ->setName('logout'); + + $group->get('/portals', View::staticPage('portals')) + ->setName('portals'); + + $group->get('/posters/faq', App\Controller\Posters\GetFaqAction::class) + ->setName('posters:faq'); + + $group->get('/profile[/{user}]', App\Controller\ProfileAction::class) + ->setName('profile'); + + $group->map(['GET', 'POST'], '/recover', App\Controller\Account\RecoverAction::class) + ->setName('recover'); + + $group->map(['GET', 'POST'], '/register', App\Controller\Account\RegisterAction::class) + ->setName('register'); + + $group->get('/talent', App\Controller\TalentAction::class) + ->setName('talent'); + + $group->get('/team', App\Controller\TeamAction::class) + ->setName('team'); + + $group->group('/wwradio', function (RouteCollectorProxy $group) { + $group->get('', View::staticPage('wwradio/index')) + ->setName('wwradio'); + + $group->get('/info', View::staticPage('wwradio/info')) + ->setName('wwradio:info'); + }); + + $group->get('/worlds', App\Controller\WorldsController::class . ':listAction') + ->setName('worlds'); + + $group->get('/world[/{id}]', App\Controller\WorldsController::class . ':getAction') + ->setName('world'); + })->add(App\Middleware\EnableView::class) + ->add(App\Middleware\GetCurrentUser::class) + ->add(App\Middleware\EnableSession::class); + + /* + * No view, public-facing APIs + */ + $app->group('/api', function (RouteCollectorProxy $group) { + $group->get('/json', App\Controller\Api\JsonAction::class) + ->setName('api:json'); + + $group->post('/vrc_api', App\Controller\Api\VrcApiAction::class) + ->setName('api:vrc_api'); + + $group->group('/comments', function (RouteCollectorProxy $group) { + $group->get('/{location}', App\Controller\Api\CommentsController::class . ':listAction') + ->setName('api:comments'); + + $group->post('/{location}', App\Controller\Api\CommentsController::class . ':postAction') + ->setName('api:comments:post') + ->add(new App\Middleware\Auth\RequireLoggedIn()) + ->add(App\Middleware\GetCurrentUser::class) + ->add(App\Middleware\EnableSession::class); + + $group->delete('/{location}', App\Controller\Api\CommentsController::class . ':deleteAction') + ->setName('api:comments:delete') + ->add(new App\Middleware\Auth\RequireMod()) + ->add(App\Middleware\GetCurrentUser::class) + ->add(App\Middleware\EnableSession::class); + }); + }); + + $app->get('/posters[/{id}]', App\Controller\Posters\GetPosterAction::class) + ->setName('posters'); + + $app->get('/short_url[/{url}]', App\Controller\GetShortUrlAction::class) + ->setName('short_url'); + + /* + * URL Redirects + */ + $redirects = [ + 'discord' => 'https://discord.gg/waterwolf', + 'twitch' => 'https://www.twitch.tv/waterwolfvr', + 'twitter' => 'https://twitter.com/waterwolftown', + 'x' => 'https://twitter.com/waterwolftown', + 'vrchat' => 'https://vrc.group/WWOLF.1912', + ]; + + foreach ($redirects as $url => $dest) { + $app->get( + '/' . $url, + function (ServerRequest $request, Response $response) use ($dest): ResponseInterface { + return $response->withRedirect($dest); + } + )->setName($url); + } +}; diff --git a/backend/bootstrap/services.php b/backend/bootstrap/services.php new file mode 100644 index 0000000..fcd3869 --- /dev/null +++ b/backend/bootstrap/services.php @@ -0,0 +1,180 @@ + function ( + ContainerInterface $di, + LoggerInterface $logger, + Environment $env + ) { + $httpFactory = new HttpFactory(); + + ServerRequestCreatorFactory::setSlimHttpDecoratorsAutomaticDetection(false); + ServerRequestCreatorFactory::setServerRequestCreator($httpFactory); + + $app = new Slim\App( + responseFactory: $httpFactory, + container: $di, + ); + + $routeCollector = $app->getRouteCollector(); + $routeCollector->setDefaultInvocationStrategy(new RequestResponse()); + + if ($env->isProduction()) { + $routeCollector->setCacheFile($env->getTempDirectory() . '/app_routes.cache.php'); + } + + call_user_func(include(__DIR__ . '/routes.php'), $app); + + // System middleware for routing and body parsing. + $app->addBodyParsingMiddleware(); + $app->addRoutingMiddleware(); + + // Redirects and updates that should happen before system middleware. + $app->add(new App\Middleware\RemoveSlashes()); + $app->add(new App\Middleware\GetRemoteIp()); + + // Add an error handler for most in-controller/task situations. + $errorHandler = $app->addErrorMiddleware( + $env->isDev(), + true, + true, + $logger + ); + $errorHandler->setDefaultErrorHandler(App\Http\ErrorHandler::class); + + return $app; + }, + + RouteParserInterface::class => fn(Slim\App $app) => $app->getRouteCollector()->getRouteParser(), + + // Console + ConsoleApplication::class => function ( + ContainerInterface $di + ) { + $console = new ConsoleApplication( + 'WaterWolf CLI', + '1.0.0' + ); + + // Add commands here + $commandLoader = new ContainerCommandLoader( + $di, + [ + 'init' => App\Console\Command\InitCommand::class, + 'migrate' => App\Console\Command\MigrateCommand::class, + 'seed' => App\Console\Command\SeedCommand::class, + 'sync' => App\Console\Command\SyncCommand::class, + 'uptime-wait' => App\Console\Command\UptimeWaitCommand::class, + ] + ); + + $console->setCommandLoader($commandLoader); + + return $console; + }, + + // Escapes HTML, HTML attributes and URL chunks. + Escaper::class => static fn() => new Escaper('utf-8'), + + // Database Abstraction Layer + Connection::class => static function (Environment $env) { + $connectionParams = [ + ...$env->getDatabaseInfo(), + 'driver' => 'pdo_mysql', + 'options' => [ + \PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'UTF8MB4' COLLATE 'utf8mb4_unicode_ci'", + ], + ]; + + return Doctrine\DBAL\DriverManager::getConnection($connectionParams); + }, + + // Image manager (resizer, processor, etc.) + ImageManager::class => static fn() => new ImageManager(new ImageManagerGdDriver()), + + // E-mail delivery service + Mailer::class => static function (Environment $env) { + $dsn = MailerDsn::fromString($env->getMailerDsn()); + $transport = (new SesTransportFactory())->create($dsn); + + return new Mailer($transport); + }, + + // HTTP client + HttpClient::class => static fn() => new HttpClient([ + 'headers' => [ + 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36', + ], + ]), + + // Filesystem Utilities + Filesystem::class => static fn() => new Filesystem(), + + // PSR-6 cache + CacheItemPoolInterface::class => static function ( + Logger $logger, + Environment $env + ) { + $cacheInterface = new FilesystemAdapter( + directory: $env->getTempDirectory() . '/cache' + ); + $cacheInterface->setLogger($logger); + return $cacheInterface; + }, + + // PSR-16 cache + CacheInterface::class => static fn(CacheItemPoolInterface $psr6Cache) => new Psr16Cache($psr6Cache), + + // PSR Logger + Logger::class => function (Environment $env) { + $logger = new Logger('site'); + + $loggingLevel = $env->isProduction() + ? LogLevel::Warning + : LogLevel::Debug; + + $logger->pushHandler( + new StreamHandler('php://stderr', $loggingLevel, true) + ); + + $logger->pushHandler( + new Monolog\Handler\RotatingFileHandler( + '/logs/site.log', + 5, + $loggingLevel, + true + ) + ); + + return $logger; + }, + + LoggerInterface::class => DI\Get(Logger::class), +]; diff --git a/backend/src/AppFactory.php b/backend/src/AppFactory.php new file mode 100644 index 0000000..7fc150d --- /dev/null +++ b/backend/src/AppFactory.php @@ -0,0 +1,109 @@ +get(SlimApp::class); + } + + public static function createCli( + array $appEnvironment = [] + ): ConsoleApplication { + $environment = self::buildEnvironment($appEnvironment); + $di = self::buildContainer($environment); + + // Some CLI commands require the App to be injected for routing. + $di->get(SlimApp::class); + + return $di->get(ConsoleApplication::class); + } + + public static function buildContainer(Environment $environment): ContainerInterface + { + Environment::setInstance($environment); + + $containerBuilder = new ContainerBuilder(); + $containerBuilder->useAutowiring(true); + $containerBuilder->useAttributes(true); + + if ($environment->isProduction()) { + $containerBuilder->enableCompilation($environment->getTempDirectory()); + } + + $containerBuilder->addDefinitions([ + Environment::class => $environment, + ]); + $containerBuilder->addDefinitions(dirname(__DIR__) . '/bootstrap/services.php'); + + $di = $containerBuilder->build(); + + // Monolog setup + $logger = $di->get(Logger::class); + + $errorHandler = new ErrorHandler($logger); + $errorHandler->registerFatalHandler(); + + return $di; + } + + /** + * @param array $rawEnvironment + */ + public static function buildEnvironment(array $rawEnvironment = []): Environment + { + $_ENV = getenv(); + $rawEnvironment = array_merge(array_filter($_ENV), $rawEnvironment); + $environment = new Environment($rawEnvironment); + + self::applyPhpSettings($environment); + + return $environment; + } + + private static function applyPhpSettings(Environment $environment): void + { + error_reporting( + $environment->isProduction() + ? E_ALL & ~E_NOTICE & ~E_WARNING & ~E_STRICT & ~E_DEPRECATED + : E_ALL & ~E_NOTICE + ); + + $displayStartupErrors = (!$environment->isProduction() || $environment->isCli()) + ? '1' + : '0'; + ini_set('display_startup_errors', $displayStartupErrors); + ini_set('display_errors', $displayStartupErrors); + + ini_set('log_errors', '1'); + ini_set('error_log', '/dev/stderr'); + + mb_internal_encoding('UTF-8'); + ini_set('default_charset', 'utf-8'); + + if (!headers_sent()) { + ini_set('session.use_only_cookies', '1'); + ini_set('session.cookie_httponly', '1'); + ini_set('session.cookie_lifetime', '86400'); + ini_set('session.use_strict_mode', '1'); + + session_cache_limiter(''); + } + + date_default_timezone_set('UTC'); + } +} diff --git a/backend/src/Console/Command/AbstractCommand.php b/backend/src/Console/Command/AbstractCommand.php new file mode 100644 index 0000000..723f94f --- /dev/null +++ b/backend/src/Console/Command/AbstractCommand.php @@ -0,0 +1,28 @@ +getApplication()?->find($commandName); + if (null === $command) { + throw new \RuntimeException(sprintf('Command %s not found.', $commandName)); + } + + $input = new ArrayInput(['command' => $commandName] + $commandArgs); + $input->setInteractive(false); + + return $command->run($input, $output); + } +} diff --git a/backend/src/Console/Command/InitCommand.php b/backend/src/Console/Command/InitCommand.php new file mode 100644 index 0000000..50f9df2 --- /dev/null +++ b/backend/src/Console/Command/InitCommand.php @@ -0,0 +1,28 @@ +title('Initialization'); + + $uptimeRet = $this->runCommand($output, 'uptime-wait'); + if ($uptimeRet !== 0) { + return $uptimeRet; + } + + $this->runCommand($output, 'migrate'); + + return 0; + } +} diff --git a/backend/src/Console/Command/MigrateCommand.php b/backend/src/Console/Command/MigrateCommand.php new file mode 100644 index 0000000..10a08e9 --- /dev/null +++ b/backend/src/Console/Command/MigrateCommand.php @@ -0,0 +1,34 @@ +find('migrate'); + + $arguments = [ + 'command' => 'migrate', + '--environment' => 'db', + '--configuration' => $this->environment->getBaseDirectory() . '/phinx.php', + ]; + + return $command->run(new ArrayInput($arguments), $output); + } +} diff --git a/backend/src/Console/Command/SeedCommand.php b/backend/src/Console/Command/SeedCommand.php new file mode 100644 index 0000000..1a7b6da --- /dev/null +++ b/backend/src/Console/Command/SeedCommand.php @@ -0,0 +1,56 @@ +environment->isDev()) { + $io->error('This can only be used in development mode.'); + return 1; + } + + $userCount = $this->db->fetchOne( + <<<'SQL' + SELECT COUNT(*) + FROM web_users + SQL + ); + + if ($userCount > 0) { + $io->warning('Cannot pre-populate database: database already seeded!'); + return 1; + } + + $phinx = new PhinxApplication(); + $command = $phinx->find('seed:run'); + + $arguments = [ + 'command' => 'seed:run', + '--environment' => 'db', + '--configuration' => $this->environment->getBaseDirectory() . '/phinx.php', + ]; + + return $command->run(new ArrayInput($arguments), $output); + } +} diff --git a/backend/src/Console/Command/SyncCommand.php b/backend/src/Console/Command/SyncCommand.php new file mode 100644 index 0000000..2fbfb28 --- /dev/null +++ b/backend/src/Console/Command/SyncCommand.php @@ -0,0 +1,45 @@ +info('Starting sync tasks...'); + + $this->clearUserLoginTokens($io); + + $io->success('Sync tasks completed.'); + return 0; + } + + private function clearUserLoginTokens(SymfonyStyle $io): void + { + $thresholdDate = new \DateTimeImmutable('-2 days', new \DateTimeZone('UTC')); + $this->db->executeQuery( + <<<'SQL' + DELETE FROM web_user_login_tokens + WHERE created_at < :threshold + SQL, + [ + 'threshold' => $thresholdDate->format('Y-m-d H:i:s'), + ] + ); + } +} diff --git a/backend/src/Console/Command/UptimeWaitCommand.php b/backend/src/Console/Command/UptimeWaitCommand.php new file mode 100644 index 0000000..24800a6 --- /dev/null +++ b/backend/src/Console/Command/UptimeWaitCommand.php @@ -0,0 +1,57 @@ +info('Starting services...'); + + $elapsed = 0; + $timeout = 180; + + $connectionParams = [ + 'driver' => 'pdo_mysql', + ...$this->environment->getDatabaseInfo(), + ]; + + while ($elapsed <= $timeout) { + try { + $conn = DriverManager::getConnection($connectionParams); + $pdo = $conn->getNativeConnection(); + + assert($pdo instanceof \PDO); + + $pdo->exec('SELECT 1'); + + $io->success('Services started up and ready!'); + return 0; + } catch (\Throwable $e) { + sleep(1); + $elapsed += 1; + + $io->writeln($e->getMessage()); + } + } + + $io->error('Timed out waiting for services to start.'); + return 1; + } +} diff --git a/backend/src/Controller/Account/ForgotAction.php b/backend/src/Controller/Account/ForgotAction.php new file mode 100644 index 0000000..74393c7 --- /dev/null +++ b/backend/src/Controller/Account/ForgotAction.php @@ -0,0 +1,115 @@ +isLoggedIn()) { + return $response->withRedirect( + $request->getRouter()->urlFor('dashboard') + ); + } + + $error = null; + + if ($request->isPost()) { + try { + $email = $request->getParam('email'); + if (empty($email)) { + throw new \InvalidArgumentException('Must provide e-mail address.'); + } + + $userId = $this->db->fetchOne( + <<<'SQL' + SELECT id + FROM web_users + WHERE LOWER(email) = LOWER(:email) + SQL, + [ + 'email' => $email, + ] + ); + + if ($userId === false) { + throw new \InvalidArgumentException('E-mail address not found!'); + } + + /* + * Create a new "split-token" key for password reset + * per ParagonIE's PHP security recommendations: + * https://paragonie.com/blog/2017/02/split-tokens-token-based-authentication-protocols-without-side-channels + */ + + $randomStr = hash('sha256', random_bytes(32)); + $tokenIdentifier = substr($randomStr, 0, 16); + $tokenVerifier = substr($randomStr, 16, 32); + + $this->db->insert( + 'web_user_login_tokens', + [ + 'id' => $tokenIdentifier, + 'verifier' => hash('sha512', $tokenVerifier), + 'creator' => $userId, + ] + ); + + $token = $tokenIdentifier . ':' . $tokenVerifier; + + $recoverUrl = $request->getRouter()->fullUrlFor( + $request->getUri(), + 'recover', + queryParams: [ + 'token' => $token, + ] + ); + + // Send e-mail + $mailBody = $request->getView()->render( + 'emails/forgot', + [ + 'url' => $recoverUrl, + ] + ); + + $email = (new Email()) + ->from(new Address('noreply@mail.waterwolf.club', 'WaterWolf Community')) + ->subject('Recover your WaterWolf Account') + ->to($email) + ->text($mailBody); + + $this->mailer->send($email); + + $request->getFlash()->success( + 'Recovery code successfully sent! Check your inbox for instructions. If you don\'t see a message, check your "Spam" folder.' + ); + return $response->withRedirect('/'); + } catch (\Throwable $e) { + $error = $e->getMessage(); + } + } + + return $request->getView()->renderToResponse( + $response, + 'account/forgot', + [ + 'error' => $error, + ] + ); + } +} diff --git a/backend/src/Controller/Account/LoginAction.php b/backend/src/Controller/Account/LoginAction.php new file mode 100644 index 0000000..0261098 --- /dev/null +++ b/backend/src/Controller/Account/LoginAction.php @@ -0,0 +1,125 @@ +getSession(); + + if (!$session->has('valid_id') && $request->isPost()) { + try { + $postParams = $request->getParsedBody(); + + $username = $postParams['username'] ?? null; + $userPassword = $postParams['pass'] ?? null; + + if (empty($username) || empty($userPassword)) { + throw new \InvalidArgumentException('Missing username or password!'); + } + + $userRow = $this->db->fetchAssociative( + <<<'SQL' + SELECT id, password, banned + FROM web_users + WHERE LOWER(username) = LOWER(:user) + OR LOWER(email) = LOWER(:user) + SQL, + [ + 'user' => $username, + ] + ); + + if ($userRow === false) { + throw new \InvalidArgumentException('This user does not exist!'); + } + + if ($userRow['banned'] == 1) { + throw new \InvalidArgumentException('You are banned and cannot log in.'); + } + + // Check legacy password. + if (hash_equals($userRow['password'], md5($userPassword))) { + // Migrate to new password. + $newPassword = password_hash($userPassword, PASSWORD_ARGON2ID); + $this->db->update( + 'web_users', + [ + 'password' => $newPassword, + ], + [ + 'id' => $userRow['id'], + ] + ); + } elseif (!password_verify($userPassword, $userRow['password'])) { + $this->db->executeQuery( + <<<'SQL' + UPDATE web_users + SET badpass = badpass + 1 + WHERE id = :id + SQL, + [ + 'id' => $userRow['id'], + ] + ); + + throw new \InvalidArgumentException('Your credentials could not be validated.'); + } + + $this->db->executeQuery( + <<<'SQL' + UPDATE web_users + SET goodpass=goodpass + 1 + WHERE id=:id + SQL, + [ + 'id' => $userRow['id'], + ] + ); + + $session->set('valid_id', $userRow['id']); + $session->set('valid_time', time()); + $session->regenerate(); + + $request->getFlash()->success('Successfully logged in! Welcome!'); + } catch (\Exception $e) { + $error = $e->getMessage(); + } + } + + $return = $request->getParam('return'); + + // Redirect logged in users. + if ($session->has('valid_id')) { + if (empty($return)) { + $return = '/dashboard'; + } + + return $response->withRedirect( + $request->getUri()->withQuery('')->withPath($return) + ); + } + + return $request->getView()->renderToResponse( + $response, + 'account/login', + [ + 'error' => $error, + 'return' => $return, + ] + ); + } +} diff --git a/backend/src/Controller/Account/LogoutAction.php b/backend/src/Controller/Account/LogoutAction.php new file mode 100644 index 0000000..defe204 --- /dev/null +++ b/backend/src/Controller/Account/LogoutAction.php @@ -0,0 +1,40 @@ +getCurrentUser(); + + if ($user !== null) { + $this->db->update( + 'web_users', + [ + 'online' => '0', + ], + [ + 'id' => $user['id'], + ] + ); + } + + $session = $request->getSession(); + + $session->clear(); + $session->regenerate(); + + return $response->withRedirect('/'); + } +} diff --git a/backend/src/Controller/Account/RecoverAction.php b/backend/src/Controller/Account/RecoverAction.php new file mode 100644 index 0000000..0e9363d --- /dev/null +++ b/backend/src/Controller/Account/RecoverAction.php @@ -0,0 +1,123 @@ +isLoggedIn()) { + return $response->withRedirect( + $request->getRouter()->urlFor('dashboard') + ); + } + + $token = $request->getParam('token'); + if (empty($token)) { + throw new \InvalidArgumentException('No token provided!'); + } + + // Look up token + [$tokenId, $tokenVerifier] = explode(':', $token); + + $threshold = new \DateTimeImmutable('-1 day', new \DateTimeZone('UTC')); + $tokenRow = $this->db->fetchAssociative( + <<<'SQL' + SELECT verifier, creator, created_at + FROM web_user_login_tokens + WHERE id=:id + AND created_at > :threshold + SQL, + [ + 'id' => $tokenId, + 'threshold' => $threshold->format('Y-m-d h:i:s'), + ] + ); + + if ($tokenRow === false) { + throw new \InvalidArgumentException('Invalid token!'); + } + + if ( + !hash_equals( + $tokenRow['verifier'], + hash('sha512', $tokenVerifier) + ) + ) { + throw new \InvalidArgumentException('Invalid token!'); + } + + $error = null; + + if ($request->isPost()) { + try { + $postParams = $request->getParsedBody(); + $newPassword = $postParams['new_password'] ?? null; + $newPasswordConfirm = $postParams['new_password_confirm'] ?? null; + + if (empty($newPassword) || empty($newPasswordConfirm)) { + throw new \InvalidArgumentException('Please provide all required fields.'); + } + + if ($newPassword !== $newPasswordConfirm) { + throw new \InvalidArgumentException('New password and confirmation do not match.'); + } + + $newPasswordHash = password_hash($newPassword, \PASSWORD_ARGON2ID); + + // Update the user's password. + $this->db->update( + 'web_users', + [ + 'password' => $newPasswordHash, + ], + [ + 'id' => $tokenRow['creator'], + ] + ); + + // Remove the now-consumed token. + $this->db->delete( + 'web_user_login_tokens', + [ + 'id' => $tokenId, + ] + ); + + $session = $request->getSession(); + $session->set('valid_id', $tokenRow['creator']); + $session->set('valid_time', time()); + $session->regenerate(); + + $request->getFlash()->success( + 'Your password has been reset and you have been logged in.' + ); + + return $response->withRedirect( + $request->getRouter()->urlFor('dashboard') + ); + } catch (\Throwable $e) { + $error = $e->getMessage(); + } + } + + return $request->getView()->renderToResponse( + $response, + 'account/recover', + [ + 'error' => $error, + 'token' => $token, + ] + ); + } +} diff --git a/backend/src/Controller/Account/RegisterAction.php b/backend/src/Controller/Account/RegisterAction.php new file mode 100644 index 0000000..b77073e --- /dev/null +++ b/backend/src/Controller/Account/RegisterAction.php @@ -0,0 +1,112 @@ +isLoggedIn()) { + return $response->withRedirect( + $request->getRouter()->urlFor('dashboard') + ); + } + + $error = null; + $postData = $request->getParams(); + + if ($request->isPost()) { + try { + $regUsername = str_replace(['#', '&', '�'], '', $postData['reg_username'] ?? ''); + $regEmail = filter_var($postData['reg_email'] ?? '', FILTER_SANITIZE_EMAIL); + + if (empty($regUsername) || empty($regEmail)) { + throw new \Exception('Nickname and e-mail cannot be left blank.'); + } + + $regPass = str_replace(' ', '', $postData['reg_pass'] ?? ''); + $regPassConfirm = str_replace(' ', '', $postData['reg_pass_confirm'] ?? ''); + + if ($regPass !== $regPassConfirm) { + throw new \Exception('The password and confirm password boxes do not match. Please try again.'); + } + + $checkUserOrEmail = $this->db->fetchOne( + <<<'SQL' + SELECT username + FROM web_users + WHERE LOWER(username) = LOWER(:username) OR LOWER(email) = LOWER(:email) + SQL, + [ + 'username' => $regUsername, + 'email' => $regEmail, + ] + ); + + if ($checkUserOrEmail !== false) { + throw new \Exception('Username or e-mail address already registered.'); + } + + $this->db->insert( + 'web_users', + [ + 'username' => $regUsername, + 'email' => $regEmail, + 'country' => $postData['reg_country'] ?? null, + 'reg_date' => time(), + 'lastip' => $request->getIp(), + 'password' => password_hash($regPass, PASSWORD_ARGON2ID), + 'vrchat' => $postData['vrchat_username'] ?? null, + 'discord' => $postData['discord_username'] ?? null, + 'ref' => $postData['ref'] ?? null, + ] + ); + + $newUserId = $this->db->lastInsertId(); + + $session = $request->getSession(); + $session->set('valid_id', $newUserId); + $session->set('valid_time', time()); + $session->regenerate(); + + $this->discord->sendMessage( + $this->environment->getDiscordWebookUrl(), + '', + hexdec('00FF00'), + 'New User Created on WaterWolf Website', + 'User ' . $regUsername . ' Has created a new account.', + (string)$request->getUri()->withPath('/static/img/waterwolf_community.png') + ); + + return $response->withRedirect( + $request->getRouter()->urlFor('dashboard') + ); + } catch (\Throwable $e) { + $error = $e->getMessage(); + } + } + + return $request->getView()->renderToResponse( + $response, + 'account/register', + [ + 'error' => $error, + 'data' => $postData, + ] + ); + } +} diff --git a/backend/src/Controller/Api/CommentsController.php b/backend/src/Controller/Api/CommentsController.php new file mode 100644 index 0000000..30812fe --- /dev/null +++ b/backend/src/Controller/Api/CommentsController.php @@ -0,0 +1,101 @@ +db->fetchAllAssociative( + <<<'SQL' + SELECT c.*, u.username + FROM web_comments AS c + JOIN waterwolf.web_users u on c.creator = u.id + WHERE c.location=:location + AND u.banned != 1 + ORDER BY c.id DESC + SQL, + [ + 'location' => $location, + ] + ); + + return $response->withJson($comments); + } + + public function postAction( + ServerRequest $request, + Response $response, + array $params + ): ResponseInterface { + $location = $params['location'] ?? null; + if (empty($location)) { + throw new \InvalidArgumentException('No comment location provided.'); + } + + $currentUser = $request->getCurrentUser(); + assert($currentUser !== null); + + $postData = $request->getParsedBody(); + + $this->db->insert( + 'web_comments', + [ + 'comment' => $postData['comment'], + 'location' => $location, + 'creator' => $currentUser['id'], + 'tstamp' => time(), + ] + ); + + return $response->withJson([ + 'success' => true, + ]); + } + + public function deleteAction( + ServerRequest $request, + Response $response, + array $params + ): ResponseInterface { + $location = $params['location'] ?? null; + if (empty($location)) { + throw new \InvalidArgumentException('No comment location provided.'); + } + + $id = $request->getParam('id'); + if (empty($id)) { + throw new \InvalidArgumentException('No ID provided.'); + } + + $this->db->delete( + 'web_comments', + [ + 'id' => $id, + 'location' => $location, + ] + ); + + return $response->withJson([ + 'success' => true, + ]); + } +} diff --git a/backend/src/Controller/Api/JsonAction.php b/backend/src/Controller/Api/JsonAction.php new file mode 100644 index 0000000..d90bbc7 --- /dev/null +++ b/backend/src/Controller/Api/JsonAction.php @@ -0,0 +1,39 @@ +db->fetchOne( + <<<'SQL' + SELECT count(*) AS user_count + FROM web_users + SQL + ); + + return $response->withJson([ + 'success' => true, + 'total_users' => $userCount, + ]); + } catch (Exception $e) { + return $response->withJson([ + 'success' => false, + 'error' => $e->getMessage(), + ]); + } + } +} diff --git a/backend/src/Controller/Api/VrcApiAction.php b/backend/src/Controller/Api/VrcApiAction.php new file mode 100644 index 0000000..7cbd3bb --- /dev/null +++ b/backend/src/Controller/Api/VrcApiAction.php @@ -0,0 +1,40 @@ +getBody()->getContents(); + $request = json_decode($postdata, true); + + if ($request['type'] == 'notification') { + $notification = $request['content']; + $notification_type = $notification['type']; + + if ($notification_type == 'friendRequest') { + $notification_id = $notification['id']; + + $this->vrcApi->sendRequest( + method: 'PUT', + path: "api/1/auth/user/notifications/$notification_id/accept", + priority: true, + async: true + ); + } + } + + return $response->withStatus(200); + } +} diff --git a/backend/src/Controller/Dashboard/Admin/AddWorldAction.php b/backend/src/Controller/Dashboard/Admin/AddWorldAction.php new file mode 100644 index 0000000..95eb5c5 --- /dev/null +++ b/backend/src/Controller/Dashboard/Admin/AddWorldAction.php @@ -0,0 +1,108 @@ +getCurrentUser(); + assert($currentUser !== null); + + $error = null; + + if ($request->isPost()) { + try { + $worldId = $request->getParam('id'); + if (empty($worldId)) { + throw new \InvalidArgumentException('World ID not specified.'); + } + + // Get the URL from the form submission + $url = 'https://vrchat.com/home/launch?worldId=' . $worldId; + + // Convert JSON data to an associative array + $body = $this->http->get($url) + ->getBody()->getContents(); + + // Regular expression pattern to find Twitter meta tags and their attributes + $pattern = '/ $property) { + $content = $matches[2][$index]; + $worldData[$property] = htmlspecialchars_decode($content); + } + + $worldTitleFull = preg_replace('/[^\w\s.]/', '', substr($worldData['title'], 0, 255)); + $worldDescription = substr($worldData['description'], 0, 255); + + // Split the text using the word "by" + $titleParts = explode(" by ", $worldTitleFull); + + $worldTitle = str_replace(' ', ' ', trim($titleParts[0])); + $worldDbTitle = str_replace(' ', '_', $worldTitle); + $worldCreator = trim($titleParts[1]); + + // Pull the world image + $imageData = (str_starts_with($worldData['image'], 'http')) + ? $this->http->get($worldData['image'])->getBody()->getContents() + : $worldData['image']; + + $imageRelativePath = '/img/worlds/' . $worldDbTitle . '.png'; + $imagePath = mediaPath($imageRelativePath); + + $this->imageManager->read($imageData)->save($imagePath); + + // Add the DB record + $this->db->insert( + 'web_worlds', + [ + 'title' => $worldTitle, + 'creator' => $currentUser['id'], + 'image' => $imageRelativePath, + 'description' => $worldDescription, + 'world_id' => $worldId, + 'world_creator' => $worldCreator, + ] + ); + + $worldDbId = $this->db->lastInsertId(); + + $request->getFlash()->success('World successfully imported!'); + + return $response->withRedirect( + $request->getRouter()->urlFor('world', ['id' => $worldDbId]) + ); + } catch (\Throwable $e) { + $error = $e->getMessage(); + } + } + + return $request->getView()->renderToResponse( + $response, + 'dashboard/admin/add_world', + [ + 'error' => $error, + ] + ); + } +} diff --git a/backend/src/Controller/Dashboard/Admin/UsersAction.php b/backend/src/Controller/Dashboard/Admin/UsersAction.php new file mode 100644 index 0000000..98717e1 --- /dev/null +++ b/backend/src/Controller/Dashboard/Admin/UsersAction.php @@ -0,0 +1,38 @@ +db->fetchAllAssociative( + <<<'SQL' + SELECT * + FROM web_users + ORDER BY id ASC + SQL + ); + + return $request->getView()->renderToResponse( + $response, + 'dashboard/admin/users', + [ + 'users' => $users, + ] + ); + } +} diff --git a/backend/src/Controller/Dashboard/AvatarAction.php b/backend/src/Controller/Dashboard/AvatarAction.php new file mode 100644 index 0000000..e63fe3f --- /dev/null +++ b/backend/src/Controller/Dashboard/AvatarAction.php @@ -0,0 +1,95 @@ +getCurrentUser(); + assert($currentUser !== null); + + $type = $params['type'] ?? $request->getParam('type', 'avatar'); + $avatarField = match ($type) { + 'dj' => 'dj_img', + default => 'user_img' + }; + + $uploadFolder = match ($type) { + 'dj' => '/img/djs', + default => '/img/profile' + }; + + if ($request->isPost()) { + $files = $request->getUploadedFiles(); + + if (empty($files['file'])) { + throw new \InvalidArgumentException('File not uploaded!'); + } + + /** @var UploadedFile $uploadedFile */ + $uploadedFile = $files['file']; + + // Remove existing image if it's set. + if (!empty($currentUser[$avatarField])) { + $currentImage = mediaPath($uploadFolder . '/' . $currentUser[$avatarField]); + if (file_exists($currentImage)) { + $this->fsUtilities->remove($currentImage); + } + } + + $imageFileType = strtolower(pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION)); + $imageBasename = $currentUser['id'] . '-' . time() . '.' . $imageFileType; + + $targetFile = mediaPath($uploadFolder . '/' . $imageBasename); + + // Resize the uploaded image and save it to the destination location. + $image = $this->imageManager->read($uploadedFile->getStream()->getContents()); + $image->cover(332, 364); + $image->save($targetFile); + + // Set the user's image location to the new image. + $this->db->update( + 'web_users', + [ + $avatarField => $imageBasename, + ], + [ + 'id' => $currentUser['id'], + ] + ); + + $request->getFlash()->success('Avatar updated!'); + + return $response->withRedirect( + $request->getRouter()->urlFor('dashboard:profile') + ); + } + + return $request->getView()->renderToResponse( + $response, + 'dashboard/avatar', + [ + 'type' => $type, + ] + ); + } +} diff --git a/backend/src/Controller/Dashboard/DmxController.php b/backend/src/Controller/Dashboard/DmxController.php new file mode 100644 index 0000000..aa58442 --- /dev/null +++ b/backend/src/Controller/Dashboard/DmxController.php @@ -0,0 +1,126 @@ +getCurrentUser(); + assert($currentUser !== null); + + $isTeam = $currentUser->isTeam() || $currentUser->isMod(); + $editRow = null; + + if ($isTeam && $request->isPost()) { + $postData = $request->getParams(); + + // Insert + if (isset($postData['add_fixture'])) { + $this->db->insert( + 'web_dmx_fixtures', + [ + 'fixture_name' => $postData['fixture_name'] ?? null, + 'universe_number' => $postData['universe_number'] ?? null, + 'channel_number' => $postData['channel_number'] ?? null, + 'rig_name' => $postData['rig_name'] ?? null, + ] + ); + } + + // Update + if (isset($postData['update_id'])) { + $this->db->update( + 'web_dmx_fixtures', + [ + 'fixture_name' => $postData['update_fixture_name'] ?? null, + 'universe_number' => $postData['update_universe_number'] ?? null, + 'channel_number' => $postData['update_channel_number'] ?? null, + 'rig_name' => $postData['update_rig_name'] ?? null, + ], + [ + 'id' => $postData['update_id'], + ] + ); + } + + // Delete + if (isset($postData['delete_id'])) { + $this->db->delete( + 'web_dmx_fixtures', + [ + 'id' => $postData['delete_id'], + ] + ); + } + + if (isset($postData['edit_id'])) { + $editRow = $this->db->fetchAssociative( + <<<'SQL' + SELECT * + FROM web_dmx_fixtures + WHERE id = :id + SQL, + [ + 'id' => $postData['edit_id'], + ] + ); + } + } + + $selectedRigName = $request->getParam('rig_name'); + + if (!empty($selectedRigName)) { + $fixtures = $this->db->fetchAllAssociative( + <<<'SQL' + SELECT * + FROM web_dmx_fixtures + WHERE rig_name = :name + SQL, + [ + 'name' => $selectedRigName, + ] + ); + } else { + $fixtures = $this->db->fetchAllAssociative( + <<<'SQL' + SELECT * + FROM web_dmx_fixtures + SQL + ); + } + + $rigLookupRaw = $this->db->fetchAllAssociative( + <<<'SQL' + SELECT DISTINCT rig_name + FROM web_dmx_fixtures + SQL + ); + $rigLookup = array_column($rigLookupRaw, 'rig_name'); + + return $request->getView()->renderToResponse( + $response, + 'dashboard/dmx', + [ + 'isTeam' => $isTeam, + 'editRow' => $editRow, + 'fixtures' => $fixtures, + 'rigLookup' => $rigLookup, + 'selectedRigName' => $selectedRigName, + ] + ); + } +} diff --git a/backend/src/Controller/Dashboard/EditProfileAction.php b/backend/src/Controller/Dashboard/EditProfileAction.php new file mode 100644 index 0000000..537ab6e --- /dev/null +++ b/backend/src/Controller/Dashboard/EditProfileAction.php @@ -0,0 +1,204 @@ +getCurrentUser(); + assert($currentUser !== null); + + $editUserId = $params['id'] ?? $request->getParam('id'); + + if (!empty($editUserId) && $currentUser->isAdmin()) { + $isAdminMode = true; + + $groupsRaw = $this->db->fetchAllAssociative( + <<<'SQL' + SELECT g.id, g.name + FROM web_groups AS g + SQL + ); + $groups = array_column($groupsRaw, 'name', 'id'); + } else { + $isAdminMode = false; + $editUserId = $currentUser['id']; + + $groups = []; + } + + $profile = $this->db->fetchAssociative( + <<<'SQL' + SELECT * + FROM web_users + WHERE id=:id + SQL, + [ + 'id' => $editUserId, + ] + ); + + if ($profile === false) { + throw NotFoundException::user($request); + } + + $userGroupsRaw = $this->db->fetchAllAssociative( + <<<'SQL' + SELECT uhg.group_id + FROM web_user_has_group AS uhg + WHERE uhg.user_id = :id + SQL, + [ + 'id' => $editUserId, + ] + ); + $userGroups = array_column($userGroupsRaw, 'group_id', 'group_id'); + + $error = null; + + if ($request->isPost()) { + try { + $postData = $request->getParsedBody(); + + $updateFields = [ + 'username' => $postData['username'], + 'email' => $postData['email'], + 'discord' => $postData['discord'] ?? null, + 'twitch' => $postData['twitch'] ?? null, + 'vrchat' => $postData['vrchat'] ?? null, + 'vrcdn' => $postData['vrcdn'] ?? null, + 'website' => $postData['website'] ?? null, + 'aboutme' => $postData['aboutme'] ?? null, + 'pronouns' => $postData['pronouns'] ?? null, + 'dj_name' => $postData['dj_name'] ?? null, + 'dj_genre' => $postData['dj_genre'] ?? null, + ]; + + if ($updateFields['email'] !== $profile['email']) { + if (empty($updateFields['email'])) { + throw new \InvalidArgumentException('E-mail address is required.'); + } + + // Check if the new e-mail is a duplicate. + $checkEmail = $this->db->fetchOne( + <<<'SQL' + SELECT username + FROM web_users + WHERE LOWER(email) = LOWER(:email) + AND id != :id + SQL, + [ + 'email' => $updateFields['email'], + 'id' => $editUserId, + ] + ); + + if ($checkEmail !== false) { + throw new \InvalidArgumentException('E-mail address is already in use by another user.'); + } + } + + if ($updateFields['username'] !== $profile['username']) { + if (empty($updateFields['username'])) { + throw new \InvalidArgumentException('Username is required.'); + } + + // Check if the new username is a duplicate. + $checkUsername = $this->db->fetchOne( + <<<'SQL' + SELECT username + FROM web_users + WHERE LOWER(username) = LOWER(:username) + AND id != :id + SQL, + [ + 'username' => $updateFields['username'], + 'id' => $editUserId, + ] + ); + + if ($checkUsername !== false) { + throw new \InvalidArgumentException('Username is already in use by another user.'); + } + } + + if ($profile['is_team'] === 1) { + $updateFields['title'] = $postData['title'] ?? null; + } + + if ($isAdminMode) { + $updateFields = [ + ...$updateFields, + 'banned' => $postData['banned'] ?? 0, + 'is_team' => $postData['is_team'] ?? 0, + 'is_admin' => $postData['is_admin'] ?? 0, + 'is_mod' => $postData['is_mod'] ?? 0, + 'is_dj' => $postData['is_dj'] ?? 0, + ]; + + $this->db->delete( + 'web_user_has_group', + [ + 'user_id' => $editUserId, + ] + ); + + foreach ((array)($postData['groups'] ?? []) as $groupId) { + if (isset($groups[$groupId])) { + $this->db->insert( + 'web_user_has_group', + [ + 'user_id' => $editUserId, + 'group_id' => $groupId, + ] + ); + } + } + } + + $this->db->update( + 'web_users', + $updateFields, + [ + 'id' => $editUserId, + ] + ); + + $request->getFlash()->success('Profile updated!'); + + return $response->withRedirect( + (string)$request->getUri() + ); + } catch (\Throwable $e) { + $error = $e->getMessage(); + } + } + + return $request->getView()->renderToResponse( + $response, + 'dashboard/profile', + [ + 'profile' => $profile, + 'isAdminMode' => $isAdminMode, + 'groups' => $groups, + 'userGroups' => $userGroups, + 'error' => $error, + ] + ); + } +} diff --git a/backend/src/Controller/Dashboard/PasswordAction.php b/backend/src/Controller/Dashboard/PasswordAction.php new file mode 100644 index 0000000..92f44c2 --- /dev/null +++ b/backend/src/Controller/Dashboard/PasswordAction.php @@ -0,0 +1,81 @@ +getCurrentUser(); + assert($currentUser !== null); + + $error = null; + + if ($request->isPost()) { + try { + $postData = $request->getParsedBody(); + + $currentPassword = $postData['current_password'] ?? null; + $newPassword = $postData['new_password'] ?? null; + $newPasswordConfirm = $postData['new_password_confirm'] ?? null; + + if (empty($currentPassword) || empty($newPassword) || empty($newPasswordConfirm)) { + throw new \InvalidArgumentException('Please provide all required fields.'); + } + + if ($newPassword !== $newPasswordConfirm) { + throw new \InvalidArgumentException('New password and confirmation do not match.'); + } + + if (!password_verify($currentPassword, $currentUser['password'])) { + throw new \InvalidArgumentException('Current password is not valid.'); + } + + $newPasswordHash = password_hash($newPassword, \PASSWORD_ARGON2ID); + + $this->db->update( + 'web_users', + [ + 'password' => $newPasswordHash, + ], + [ + 'id' => $currentUser['id'], + ] + ); + + $session = $request->getSession(); + $session->clear(); + $session->regenerate(); + + $request->getFlash()->success('Password reset! Please log in again.'); + + return $response->withRedirect( + $request->getRouter()->urlFor('login') + ); + } catch (\Throwable $e) { + $error = $e->getMessage(); + } + } + + return $request->getView()->renderToResponse( + $response, + 'dashboard/password', + [ + 'error' => $error, + ] + ); + } +} diff --git a/backend/src/Controller/Dashboard/PostersController.php b/backend/src/Controller/Dashboard/PostersController.php new file mode 100644 index 0000000..18ac462 --- /dev/null +++ b/backend/src/Controller/Dashboard/PostersController.php @@ -0,0 +1,418 @@ +getCurrentUser(); + assert($currentUser !== null); + + $types = $this->getTypes(); + + $qb = $this->db->createQueryBuilder() + ->select('p.*, u.username') + ->from('web_posters', 'p') + ->join('p', 'web_users', 'u', 'p.creator = u.id') + ->where('u.banned != 1') + ->orderBy('p.id DESC'); + + $groupLookup = $this->getEditableGroups($request); + + if (!$currentUser->isMod()) { + $qb->andWhere('(p.group_id IS NULL) OR (p.group_id IN (:groups))') + ->setParameter('user_id', $currentUser['id']) + ->setParameter('groups', array_keys($groupLookup), ArrayParameterType::STRING); + } + + $groups = [ + '_mine' => [ + 'name' => 'My Posters', + 'canEdit' => true, + 'posters' => [], + ], + '_community' => [ + 'name' => 'Community Posters', + 'canEdit' => $currentUser->isMod(), + 'posters' => [], + ], + ]; + + foreach ($groupLookup as $groupId => $groupName) { + $groups[$groupId] = [ + 'name' => $groupName, + 'code' => $groupId, + 'canEdit' => true, + 'posters' => [], + ]; + } + + $nowDt = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); + + foreach ($qb->fetchAllAssociative() as $poster) { + // Get poster URL + $tryMediaUrls = [ + '/img/posters/' . urlencode($poster['file']) . '_thumb.jpg', + '/img/posters/' . urlencode($poster['file']) . '_150x200.jpeg', + ]; + + $mediaUrl = '/static/img/no_poster_thumb.jpg'; + foreach ($tryMediaUrls as $tryMediaUrl) { + $mediaPath = mediaPath($tryMediaUrl); + if (file_exists($mediaPath)) { + $mediaUrl = mediaUrl($tryMediaUrl); + break; + } + } + $poster['mediaUrl'] = $mediaUrl; + + // Calculate poster expiration. + if ($poster['expires_at']) { + $expiresAt = new \DateTimeImmutable($poster['expires_at'], new \DateTimeZone('UTC')); + + $poster['isExpired'] = $expiresAt < $nowDt; + $poster['expiresAtText'] = $expiresAt->format('F j, Y g:ia'); + } else { + $poster['isExpired'] = false; + $poster['expiresAtText'] = null; + } + + // Assign poster to the correct group + if (!empty($poster['group_id'])) { + $groups[$poster['group_id']]['posters'][] = $poster; + } elseif ($poster['creator'] === $currentUser['id']) { + $groups['_mine']['posters'][] = $poster; + } else { + $groups['_community']['posters'][] = $poster; + } + } + + return $request->getView()->renderToResponse( + $response, + 'dashboard/posters/list', + [ + 'types' => $types, + 'groups' => $groups, + ] + ); + } + + public function createAction( + ServerRequest $request, + Response $response, + array $params + ): ResponseInterface { + $currentUser = $request->getCurrentUser(); + assert($currentUser !== null); + + $row = []; + $error = null; + + $groups = $this->getEditableGroups($request); + $types = $this->getTypes(); + + if ($request->isPost()) { + try { + $files = $request->getUploadedFiles(); + if (empty($files['fileToUpload'])) { + throw new \InvalidArgumentException('No poster uploaded.'); + } + + /** @var UploadedFileInterface $file */ + $file = $files['fileToUpload']; + + $basename = $this->generateAssets($file); + + $row['creator'] = $currentUser['id']; + $row['file'] = $basename; + + $postData = $request->getParsedBody(); + + $inputType = $postData['type'] ?? null; + if (!empty($inputType) && isset($types[$inputType])) { + $row['type_id'] = $inputType; + } + + $inputGroup = $postData['group'] ?? null; + if (!empty($inputGroup) && isset($groups[$inputGroup])) { + $row['group_id'] = $inputGroup; + } + + $inputCollection = $postData['collection'] ?? null; + if (!empty($inputCollection)) { + $row['collection'] = $inputCollection; + } + + $inputExpiresAt = $postData['expires_at'] ?? null; + if (!empty($inputExpiresAt)) { + $dt = new \DateTimeImmutable($inputExpiresAt, new \DateTimeZone('UTC')); + $row['expires_at'] = $dt->format('Y-m-d H:i:s'); + } + + $this->db->insert('web_posters', $row); + + $request->getFlash()->success('Poster uploaded.'); + return $response->withRedirect( + $request->getRouter()->urlFor('dashboard:posters') + ); + } catch (\Throwable $e) { + $error = $e->getMessage(); + } + } + + return $request->getView()->renderToResponse( + $response, + 'dashboard/posters/edit', + [ + 'isEditMode' => false, + 'row' => $row, + 'error' => $error, + 'groups' => $groups, + 'types' => $types, + ] + ); + } + + public function editAction( + ServerRequest $request, + Response $response, + array $params + ): ResponseInterface { + $id = $params['id'] ?? $request->getParam('pid'); + $row = $this->getEditablePoster($request, $id); + + $groups = $this->getEditableGroups($request); + $types = $this->getTypes(); + + $error = null; + + if ($request->isPost()) { + try { + $files = $request->getUploadedFiles(); + if (isset($files['fileToUpload'])) { + /** @var UploadedFileInterface $file */ + $file = $files['fileToUpload']; + + if ($file->getError() === UPLOAD_ERR_OK) { + $this->deleteAssets($row['file']); + $row['file'] = $this->generateAssets($files['fileToUpload']); + } + } + + $postData = $request->getParsedBody(); + + $inputType = $postData['type'] ?? null; + $row['type_id'] = (!empty($inputType) && isset($types[$inputType])) + ? $inputType + : null; + + $inputGroup = $postData['group'] ?? null; + $row['group_id'] = (!empty($inputGroup) && isset($groups[$inputGroup])) + ? $inputGroup + : null; + + $inputCollection = $postData['collection'] ?? null; + $row['collection'] = (!empty($inputCollection)) + ? $inputCollection + : null; + + $inputExpiresAt = $postData['expires_at'] ?? null; + if (!empty($inputExpiresAt)) { + $dt = new \DateTimeImmutable($inputExpiresAt, new \DateTimeZone('UTC')); + $row['expires_at'] = $dt->format('Y-m-d H:i:s'); + } else { + $row['expires_at'] = null; + } + + $id = $row['id']; + unset($row['id']); + + $this->db->update( + 'web_posters', + $row, + [ + 'id' => $id, + ] + ); + + $request->getFlash()->success('Poster updated.'); + + return $response->withRedirect( + $request->getRouter()->urlFor('dashboard:posters') + ); + } catch (\Throwable $e) { + $error = $e->getMessage(); + } + } + + return $request->getView()->renderToResponse( + $response, + 'dashboard/posters/edit', + [ + 'isEditMode' => true, + 'row' => $row, + 'error' => $error, + 'groups' => $groups, + 'types' => $types, + ] + ); + } + + public function deleteAction( + ServerRequest $request, + Response $response, + array $params + ): ResponseInterface { + $id = $params['id'] ?? $request->getParam('pid'); + $row = $this->getEditablePoster($request, $id); + + $this->deleteAssets($row['file']); + + $this->db->delete( + 'web_posters', + [ + 'id' => $row['id'], + ] + ); + + $request->getFlash()->success('Poster removed.'); + return $response->withRedirect( + $request->getRouter()->urlFor('dashboard:posters') + ); + } + + private function getEditableGroups(ServerRequest $request): array + { + $currentUser = $request->getCurrentUser(); + assert($currentUser !== null); + + if ($currentUser->isMod()) { + $groupsRaw = $this->db->fetchAllAssociative( + <<<'SQL' + SELECT g.id, g.name + FROM web_groups AS g + SQL + ); + } else { + $groupsRaw = $this->db->fetchAllAssociative( + <<<'SQL' + SELECT g.id, g.name + FROM web_groups AS g + JOIN web_user_has_group AS uhg ON g.id = uhg.group_id + WHERE uhg.user_id = :id + SQL, + [ + 'id' => $currentUser['id'], + ] + ); + } + + return array_column($groupsRaw, 'name', 'id'); + } + + private function getTypes(): array + { + $typesRaw = $this->db->fetchAllAssociative( + <<<'SQL' + SELECT id, description + FROM web_poster_types + SQL + ); + + return array_column($typesRaw, 'description', 'id'); + } + + private function getEditablePoster( + ServerRequest $request, + int|null $id + ): array { + if ($id === null) { + throw NotFoundException::poster($request); + } + + $currentUser = $request->getCurrentUser(); + assert($currentUser !== null); + + $qb = $this->db->createQueryBuilder() + ->select('p.*') + ->from('web_posters', 'p') + ->where('id = :id') + ->setParameter('id', $id); + + if (!$currentUser->isMod()) { + $groups = $this->getEditableGroups($request); + + $qb->andWhere('(p.group_id IS NULL AND p.creator = :user_id) OR (p.group_id IN (:groups))') + ->setParameter('user_id', $currentUser['id']) + ->setParameter('groups', array_keys($groups), ArrayParameterType::STRING); + } + + $row = $qb->fetchAssociative(); + + if ($row === false) { + throw NotFoundException::poster($request); + } + + return $row; + } + + private function deleteAssets(string $posterFile): void + { + $sizes = [ + '_thumb.jpg', + '_full.jpg', + '_300x500.jpeg', + '_150x200.jpeg', + '_590x1000.jpeg', + ]; + + foreach ($sizes as $size) { + $filePath = mediaPath('/img/posters/' . $posterFile . $size); + if (file_exists($filePath)) { + unlink($filePath); + } + } + } + + private function generateAssets(UploadedFileInterface $filePath): string + { + $image = $this->imageManager->read($filePath->getStream()->getContents()); + + // Create a random 12 digit hash for file name + $basename = bin2hex(random_bytes(6)); // 6 bytes = 12 hex characters + + $sizes = [ + [118, 200, $basename . '_thumb.jpg'], + [590, 1000, $basename . '_full.jpg'], + ]; + + foreach ($sizes as [$width, $height, $filename]) { + $thumbnail = clone $image; + $thumbnail->cover($width, $height); + + $destPath = mediaPath('/img/posters/' . $filename); + $thumbnail->save($destPath); + } + + return $basename; + } +} diff --git a/backend/src/Controller/Dashboard/ShortUrlsController.php b/backend/src/Controller/Dashboard/ShortUrlsController.php new file mode 100644 index 0000000..fb9a9a0 --- /dev/null +++ b/backend/src/Controller/Dashboard/ShortUrlsController.php @@ -0,0 +1,182 @@ +db->fetchAllAssociative( + <<<'SQL' + SELECT * + FROM web_short_urls + ORDER BY short_url ASC + SQL + ); + + return $request->getView()->renderToResponse( + $response, + 'dashboard/short_urls/list', + [ + 'shortUrls' => $shortUrls, + ] + ); + } + + public function createAction( + ServerRequest $request, + Response $response, + array $params + ): ResponseInterface { + $currentUser = $request->getCurrentUser(); + assert($currentUser !== null); + + $row = []; + $error = null; + + if ($request->isPost()) { + try { + $postData = $request->getParsedBody(); + $row['short_url'] = trim($postData['short_url'] ?? '', '/'); + $row['long_url'] = $postData['long_url'] ?? null; + + if (empty($row['short_url']) || empty($row['long_url'])) { + throw new \InvalidArgumentException('Short and Long URL are required.'); + } + + $this->db->insert( + 'web_short_urls', + [ + 'short_url' => $row['short_url'], + 'long_url' => $row['long_url'], + 'creator' => $currentUser['id'], + ] + ); + + $request->getFlash()->success('Short URL created.'); + + return $response->withRedirect( + $request->getRouter()->urlFor('dashboard:short_urls') + ); + } catch (\Throwable $e) { + $error = $e->getMessage(); + } + } + + return $request->getView()->renderToResponse( + $response, + 'dashboard/short_urls/edit', + [ + 'isEditMode' => false, + 'row' => $row, + 'error' => $error, + ] + ); + } + + public function editAction( + ServerRequest $request, + Response $response, + array $params + ): ResponseInterface { + $id = $params['id'] ?? $request->getParam('id'); + if (empty($id)) { + throw new \InvalidArgumentException('ID is required.'); + } + + $row = $this->db->fetchAssociative( + <<<'SQL' + SELECT * + FROM web_short_urls + WHERE id = :id + SQL, + [ + 'id' => $id, + ] + ); + + if ($row === false) { + throw new \InvalidArgumentException('Record not found!'); + } + + $error = null; + + if ($request->isPost()) { + try { + $postData = $request->getParsedBody(); + $row['short_url'] = trim($postData['short_url'] ?? '', '/'); + $row['long_url'] = $postData['long_url'] ?? null; + + if (empty($row['short_url']) || empty($row['long_url'])) { + throw new \InvalidArgumentException('Short and Long URL are required.'); + } + + $this->db->update( + 'web_short_urls', + [ + 'short_url' => $row['short_url'], + 'long_url' => $row['long_url'], + ], + [ + 'id' => $row['id'], + ] + ); + + $request->getFlash()->success('Short URL updated.'); + + return $response->withRedirect( + $request->getRouter()->urlFor('dashboard:short_urls') + ); + } catch (\Throwable $e) { + $error = $e->getMessage(); + } + } + + return $request->getView()->renderToResponse( + $response, + 'dashboard/short_urls/edit', + [ + 'isEditMode' => true, + 'row' => $row, + 'error' => $error, + ] + ); + } + + public function deleteAction( + ServerRequest $request, + Response $response, + array $params + ): ResponseInterface { + $id = $params['id'] ?? $request->getParam('id'); + if (empty($id)) { + throw new \InvalidArgumentException('ID is required.'); + } + + $this->db->delete( + 'web_short_urls', + [ + 'id' => $id, + ] + ); + + $request->getFlash()->success('Short URL deleted.'); + + return $response->withRedirect( + $request->getRouter()->urlFor('dashboard:short_urls') + ); + } +} diff --git a/backend/src/Controller/Dashboard/SkillsController.php b/backend/src/Controller/Dashboard/SkillsController.php new file mode 100644 index 0000000..3028289 --- /dev/null +++ b/backend/src/Controller/Dashboard/SkillsController.php @@ -0,0 +1,122 @@ +getCurrentUser(); + assert($currentUser !== null); + + $userId = $currentUser['id']; + + $editRow = null; + + if ($request->isPost()) { + $postData = $request->getParsedBody(); + + if (isset($postData['skill'])) { + $add_skill = $postData['skill']; + + $existingSkill = $this->db->fetchOne( + <<<'SQL' + SELECT COUNT(*) + FROM web_user_skills + WHERE skill = :skill + AND creator = :creator + SQL, + [ + 'skill' => $add_skill, + 'creator' => $userId, + ] + ); + + if ($existingSkill == 0) { + $this->db->insert( + 'web_user_skills', + [ + 'skill' => $add_skill, + 'creator' => $userId, + ] + ); + } + } + + if (isset($postData['delete_id'])) { + $this->db->delete( + 'web_user_skills', + [ + 'id' => $postData['delete_id'], + 'creator' => $userId, + ] + ); + } + + // Check for edit request + if (isset($postData['edit_id'])) { + $editRow = $this->db->fetchAssociative( + <<<'SQL' + SELECT * + FROM web_user_skills + WHERE id = :id AND creator = :creator + SQL, + [ + 'id' => $postData['edit_id'], + 'creator' => $userId, + ] + ); + } + } + + $myTalents = $this->db->fetchAllAssociative( + <<<'SQL' + SELECT * + FROM web_user_skills + WHERE creator=:creator + ORDER BY id DESC + SQL, + [ + 'creator' => $userId, + ] + ); + + $communityTalents = $this->db->fetchAllAssociative( + <<<'SQL' + SELECT skill, COUNT(*) as occurrences + FROM web_user_skills + GROUP BY skill + SQL + ); + + // Build a lookup for the autocomplete. + $skillLookup = []; + foreach ($communityTalents as $row) { + $skillLookup[] = $row['skill']; + } + + return $request->getView()->renderToResponse( + $response, + 'dashboard/skills', + [ + 'editRow' => $editRow, + 'myTalents' => $myTalents, + 'communityTalents' => $communityTalents, + 'skillLookup' => $skillLookup, + ] + ); + } +} diff --git a/backend/src/Controller/GetShortUrlAction.php b/backend/src/Controller/GetShortUrlAction.php new file mode 100644 index 0000000..522bcf1 --- /dev/null +++ b/backend/src/Controller/GetShortUrlAction.php @@ -0,0 +1,61 @@ +getParam('url', ''), '/'); + + if (empty($url)) { + return $response->withRedirect('/'); + } + + $shortUrl = $this->db->fetchAssociative( + <<<'SQL' + SELECT id, long_url + FROM web_short_urls + WHERE short_url = :url + SQL, + [ + 'url' => $url, + ] + ); + + if (false === $shortUrl) { + return $response->withRedirect( + $request->getUri()->withQuery('')->withPath('/' . $url) + ); + } + + $this->db->executeQuery( + <<<'SQL' + UPDATE web_short_urls + SET views=views+1 + WHERE id = :id + SQL, + [ + 'id' => $shortUrl['id'], + ] + ); + + return $response->withRedirect($shortUrl['long_url']); + } +} diff --git a/backend/src/Controller/Posters/GetFaqAction.php b/backend/src/Controller/Posters/GetFaqAction.php new file mode 100644 index 0000000..7a929ae --- /dev/null +++ b/backend/src/Controller/Posters/GetFaqAction.php @@ -0,0 +1,48 @@ +getRouter()->fullUrlFor( + $request->getUri(), + 'posters' + ); + + $groups = $this->db->fetchAllAssociative( + <<<'SQL' + SELECT * + FROM web_groups + SQL + ); + + $types = $this->db->fetchAllAssociative( + <<<'SQL' + SELECT * + FROM web_poster_types + SQL + ); + + return $request->getView()->renderToResponse( + $response, + 'posters/faq', + [ + 'baseUrl' => $baseUrl, + 'groups' => $groups, + 'types' => $types, + ] + ); + } +} diff --git a/backend/src/Controller/Posters/GetPosterAction.php b/backend/src/Controller/Posters/GetPosterAction.php new file mode 100644 index 0000000..e9a7bda --- /dev/null +++ b/backend/src/Controller/Posters/GetPosterAction.php @@ -0,0 +1,151 @@ +getParam('pid', $request->getParam('id')); + + $qb = $this->db->createQueryBuilder() + ->select('p.id', 'p.file') + ->from('web_posters', 'p') + ->join('p', 'web_users', 'u', 'p.creator = u.id') + ->where('u.banned != 1') + ->andWhere('(p.expires_at IS NULL OR p.expires_at > CURRENT_TIMESTAMP())'); + + $post = null; + + if (!empty($posterId)) { + // Given an ID, just load the specified poster directly. + $qb->andWhere('p.id = :id') + ->setParameter('id', $posterId) + ->setMaxResults(1); + + $post = $qb->fetchAssociative(); + } else { + /* + * Apply filters to the query or cache. Any filters can be chained. + */ + + $filters = []; + + $queryNickname = $request->getParam('collection'); + if (!empty($queryNickname)) { + $qb->andWhere('LOWER(p.collection) = LOWER(:collection)') + ->setParameter('collection', $queryNickname); + + $filters[] = 'collection:' . $queryNickname; + } + + $queryType = $request->getParam('type'); + if (!empty($queryType)) { + $qb->andWhere('p.type_id = :type') + ->setParameter('type', $queryType); + + $filters[] = 'type:' . $queryType; + } + + $queryGroup = $request->getParam('group'); + if (!empty($queryGroup)) { + $qb->andWhere('p.group_id = :group') + ->setParameter('group', $queryGroup); + + $filters[] = 'group:' . $queryGroup; + } + + /* + * Cache a shuffled list of posters to avoid serving duplicate posters to people + * loading multiple posters in the same world. VRC imposes a delay of 5 seconds + * between image loads, so the cache lifetime has to exceed that. + */ + + $cacheKey = (count($filters) > 0) + ? 'posters_' . $request->getIp() . '_' . md5(implode('_', $filters)) + : 'posters_' . $request->getIp() . '_all'; + + $cacheKey = str_replace(':', '.', $cacheKey); + + if ($this->cache->has($cacheKey)) { + $shuffleQueue = (array)$this->cache->get($cacheKey); + + if (count($shuffleQueue) > 0) { + $post = array_pop($shuffleQueue); + $post['cache'] = 'hit'; + + if (empty($shuffleQueue)) { + $this->cache->delete($cacheKey); + } else { + $this->cache->set($cacheKey, $shuffleQueue, 15); + } + } + } + + if ($post === null) { + $shuffleQueue = $qb + ->orderBy('RAND()') + ->setMaxResults(30) + ->fetchAllAssociative(); + + if (!empty($shuffleQueue)) { + $post = array_pop($shuffleQueue); + $post['cache'] = 'miss'; + + if (!empty($shuffleQueue)) { + $this->cache->set($cacheKey, $shuffleQueue, 15); + } + } + } + } + + $imagePath = $this->environment->getBaseDirectory() . '/web/static/img/no_poster.jpg'; + + if (!empty($post)) { + $tryPaths = [ + '/img/posters/' . $post['file'] . '_full.jpg', + '/img/posters/' . $post['file'] . '_590x1000.jpeg', + ]; + + foreach ($tryPaths as $tryPath) { + $postPath = mediaPath($tryPath); + if (file_exists($postPath)) { + $imagePath = $postPath; + break; + } + } + + // Update view count + $this->db->executeQuery( + <<<'SQL' + UPDATE web_posters + SET views=views+1, last_viewed = UNIX_TIMESTAMP() + WHERE id = :id + SQL, + [ + 'id' => $post['id'], + ] + ); + } + + return $response->renderFile($imagePath) + ->withHeader('Content-Disposition', 'inline'); + } +} diff --git a/backend/src/Controller/ProfileAction.php b/backend/src/Controller/ProfileAction.php new file mode 100644 index 0000000..960f6e9 --- /dev/null +++ b/backend/src/Controller/ProfileAction.php @@ -0,0 +1,50 @@ +getParam('user', $request->getParam('id')); + + $profile = $this->db->fetchAssociative( + <<<'SQL' + SELECT * + FROM web_users + WHERE username = :username + AND banned != 1 + LIMIT 1 + SQL, + [ + 'username' => $username, + ] + ); + + if ($profile === false) { + throw NotFoundException::user($request); + } + + return $request->getView()->renderToResponse( + $response, + 'profile', + [ + 'profile' => $profile, + ] + ); + } +} diff --git a/backend/src/Controller/TalentAction.php b/backend/src/Controller/TalentAction.php new file mode 100644 index 0000000..1f34902 --- /dev/null +++ b/backend/src/Controller/TalentAction.php @@ -0,0 +1,40 @@ +db->fetchAllAssociative( + <<<'SQL' + SELECT s.skill, COUNT(s.id) as occurrences + FROM web_user_skills s + JOIN web_users u ON s.creator = u.id + WHERE u.banned != 1 + GROUP BY skill + SQL + ); + + return $request->getView()->renderToResponse( + $response, + 'talent', + [ + 'skills' => $skills, + ] + ); + } +} diff --git a/backend/src/Controller/TeamAction.php b/backend/src/Controller/TeamAction.php new file mode 100644 index 0000000..0560c46 --- /dev/null +++ b/backend/src/Controller/TeamAction.php @@ -0,0 +1,40 @@ +db->fetchAllAssociative( + <<<'SQL' + SELECT id, username, title, pronouns, user_img + FROM web_users + WHERE is_team = 1 + AND banned != 1 + ORDER BY id ASC + SQL + ); + + return $request->getView()->renderToResponse( + $response, + 'team', + [ + 'team_members' => $teamMembers, + ] + ); + } +} diff --git a/backend/src/Controller/WorldsController.php b/backend/src/Controller/WorldsController.php new file mode 100644 index 0000000..943a572 --- /dev/null +++ b/backend/src/Controller/WorldsController.php @@ -0,0 +1,93 @@ +db->fetchAllAssociative( + <<<'SQL' + SELECT * + FROM web_worlds + ORDER BY id DESC + SQL + ); + + return $request->getView()->renderToResponse( + $response, + 'worlds', + [ + 'worlds' => $worlds, + ] + ); + } + + public function getAction( + ServerRequest $request, + Response $response, + array $params + ): ResponseInterface { + $id = $params['id'] ?? $request->getParam('v', $request->getParam('id')); + + $world = $this->db->fetchAssociative( + <<<'SQL' + SELECT * + FROM web_worlds + WHERE id = :id + LIMIT 1 + SQL, + [ + 'id' => $id, + ] + ); + + if ($world === false) { + throw NotFoundException::world($request); + } + + // Log visit to this page. + $this->db->executeQuery( + <<<'SQL' + UPDATE web_worlds + SET hits=hits+1 + WHERE id = :id + SQL, + [ + 'id' => $world['id'], + ] + ); + + $sidebar_worlds = $this->db->fetchAllAssociative( + <<<'SQL' + SELECT * + FROM web_worlds + ORDER BY RAND() + LIMIT 15 + SQL + ); + + return $request->getView()->renderToResponse( + $response, + 'world', + [ + 'world' => $world, + 'sidebar_worlds' => $sidebar_worlds, + ] + ); + } +} diff --git a/backend/src/Entity/User.php b/backend/src/Entity/User.php new file mode 100644 index 0000000..de37a9f --- /dev/null +++ b/backend/src/Entity/User.php @@ -0,0 +1,67 @@ + + */ +final class User implements ArrayAccess +{ + public function __construct( + private readonly array $data + ) { + } + + public function isAdmin(): bool + { + return $this->data['is_admin'] === 1; + } + + public function isMod(bool $strict = false): bool + { + if (!$strict && $this->isAdmin()) { + return true; + } + + return $this->data['is_mod'] === 1; + } + + public function isTeam(): bool + { + return $this->data['is_team'] === 1; + } + + public function isDj(): bool + { + return $this->data['is_dj'] === 1; + } + + public function toArray(): array + { + return $this->data; + } + + public function offsetExists(mixed $offset): bool + { + return isset($this->data[$offset]); + } + + public function offsetGet(mixed $offset): mixed + { + return $this->data[$offset] ?? null; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + throw new \LogicException('Cannot modify user object.'); + } + + public function offsetUnset(mixed $offset): void + { + throw new \LogicException('Cannot modify user object.'); + } +} diff --git a/backend/src/Environment.php b/backend/src/Environment.php new file mode 100644 index 0000000..4e84c08 --- /dev/null +++ b/backend/src/Environment.php @@ -0,0 +1,117 @@ +data[self::APPLICATION_ENV] ?? 'production'; + } + + public function isProduction(): bool + { + return $this->getApplicationEnv() === 'production'; + } + + public function isDev(): bool + { + return $this->getApplicationEnv() !== 'production'; + } + + public function isCli(): bool + { + return ('cli' === PHP_SAPI); + } + + public function getBaseDirectory(): string + { + return dirname(__DIR__, 2); + } + + public function getParentDirectory(): string + { + return dirname($this->getBaseDirectory()); + } + + public function getTempDirectory(): string + { + return $this->getParentDirectory() . '/www_tmp'; + } + + public function getDatabaseInfo(): array + { + return [ + 'host' => $this->data[self::DB_HOST], + 'user' => $this->data[self::DB_USER], + 'password' => $this->data[self::DB_PASS], + 'dbname' => $this->data[self::DB_NAME], + ]; + } + + public function getMailerDsn(): string + { + return $this->data[self::MAILER_DSN] + ?? throw new \RuntimeException('Mailer not configured.'); + } + + public function getMediaUrl(): string + { + return $this->isDev() + ? '/media/site' + : $this->data[self::MEDIA_SITE_URL]; + } + + public function getMediaPath(): string + { + return $this->isDev() + ? $this->getBaseDirectory() . '/web/media/site' + : $this->data[self::PHP_MEDIA_PATH]; + } + + public function getVrcApiKey(): string + { + return $this->data[self::VRCHAT_API_KEY] + ?? throw new \RuntimeException('VRChat API key not configured.'); + } + + public function getDiscordWebookUrl(): string + { + return $this->data[self::DISCORD_WEBHOOK_URL] + ?? throw new \RuntimeException('Discord webhook URL not configured.'); + } + + public static function getInstance(): Environment + { + return self::$instance; + } + + public static function setInstance(Environment $instance): void + { + self::$instance = $instance; + } +} diff --git a/backend/src/Exception/NotFoundException.php b/backend/src/Exception/NotFoundException.php new file mode 100644 index 0000000..469a872 --- /dev/null +++ b/backend/src/Exception/NotFoundException.php @@ -0,0 +1,34 @@ +getCallableResolver(), $app->getResponseFactory(), $logger); + } + + public function __invoke( + ServerRequestInterface $request, + Throwable $exception, + bool $displayErrorDetails, + bool $logErrors, + bool $logErrorDetails + ): ResponseInterface { + $this->showDetailed = $this->environment->isDev() && !($exception instanceof HttpException); + + if ($exception instanceof HttpException) { + $this->loggerLevel = Level::Info; + } + + $this->returnJson = $this->shouldReturnJson($request); + + return parent::__invoke($request, $exception, $displayErrorDetails, $logErrors, $logErrorDetails); + } + + private function shouldReturnJson(ServerRequestInterface $req): bool + { + $xhr = $req->getHeaderLine('X-Requested-With') === 'XMLHttpRequest'; + + if ($xhr || $this->environment->isCli()) { + return true; + } + + if ($req->hasHeader('Accept')) { + $accept = $req->getHeaderLine('Accept'); + if (false !== stripos($accept, 'application/json')) { + return true; + } + } + + return false; + } + + /** + * @inheritDoc + */ + protected function writeToErrorLog(): void + { + $context = [ + 'file' => $this->exception->getFile(), + 'line' => $this->exception->getLine(), + 'code' => $this->exception->getCode(), + ]; + + if ($this->showDetailed) { + $context['trace'] = array_slice($this->exception->getTrace(), 0, 5); + } + + $this->logger->log($this->loggerLevel, $this->exception->getMessage(), $context); + } + + protected function respond(): ResponseInterface + { + if (!($this->request instanceof ServerRequest)) { + return parent::respond(); + } + + /** @var Response $response */ + $response = $this->responseFactory->createResponse($this->statusCode); + + // Special handling for cURL requests. + $ua = $this->request->getHeaderLine('User-Agent'); + + if (false !== stripos($ua, 'curl')) { + $response->getBody()->write( + sprintf( + 'Error: %s on %s L%s', + $this->exception->getMessage(), + $this->exception->getFile(), + $this->exception->getLine() + ) + ); + + return $response; + } + + if ($this->returnJson) { + return $response->withJson([ + 'code' => $this->exception->getCode(), + 'type' => (new \ReflectionClass($this->exception))->getShortName(), + 'message' => $this->exception->getMessage(), + ]); + } + + // Redirect to login page for not-logged-in users. + if ($this->exception instanceof NotLoggedInException) { + // Inject the session for subsequent handlers. + $sessionPersistence = $this->injectSession->getSessionPersistence(); + + /** @var Session $session */ + $session = $sessionPersistence->initializeSessionFromRequest($this->request); + + $flash = new Flash($session); + $flash->error($this->exception->getMessage()); + + // Set referrer for login redirection. + $session->set('login_referrer', $this->request->getUri()->getPath()); + + return $sessionPersistence->persistSession( + $session, + $response->withRedirect('/login?return=' . $this->request->getUri()->getPath()) + ); + } + + // Bounce back to homepage for permission-denied users. + if ($this->exception instanceof PermissionDeniedException) { + // Inject the session for subsequent handlers. + $sessionPersistence = $this->injectSession->getSessionPersistence(); + + /** @var Session $session */ + $session = $sessionPersistence->initializeSessionFromRequest($this->request); + + $flash = new Flash($session); + $flash->error($this->exception->getMessage()); + + return $sessionPersistence->persistSession( + $session, + $response->withRedirect('/') + ); + } + + if ($this->showDetailed && class_exists(Run::class)) { + // Register error-handler. + $handler = new PrettyPageHandler(); + $handler->setPageTitle('An error occurred!'); + + $run = new Run(); + $run->prependHandler($handler); + + return $response->write($run->handleException($this->exception)); + } + + try { + $view = $this->view->setRequest($this->request); + + return $view->renderToResponse( + $response, + ($this->exception instanceof HttpException) + ? 'errors/http' + : 'errors/generic', + [ + 'exception' => $this->exception, + ] + ); + } catch (Throwable) { + return parent::respond(); + } + } +} diff --git a/backend/src/Http/HttpFactory.php b/backend/src/Http/HttpFactory.php new file mode 100644 index 0000000..f5045b8 --- /dev/null +++ b/backend/src/Http/HttpFactory.php @@ -0,0 +1,90 @@ +httpFactory = new GuzzleHttpFactory(); + } + + public function createUploadedFile(...$args): UploadedFileInterface + { + return $this->httpFactory->createUploadedFile(...$args); + } + + public function createStream(string $content = ''): StreamInterface + { + return $this->httpFactory->createStream($content); + } + + public function createStreamFromFile(...$args): StreamInterface + { + return $this->httpFactory->createStreamFromFile(...$args); + } + + public function createStreamFromResource($resource): StreamInterface + { + return $this->httpFactory->createStreamFromResource($resource); + } + + public function createServerRequest(...$args): ServerRequestInterface + { + $serverRequest = $this->httpFactory->createServerRequest(...$args); + return $this->decorateServerRequest($serverRequest); + } + + public function createServerRequestFromGlobals(): ServerRequestInterface + { + return $this->decorateServerRequest(GuzzleServerRequest::fromGlobals()); + } + + private function decorateServerRequest(ServerRequestInterface $request): ServerRequestInterface + { + return new ServerRequest($request); + } + + public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface + { + $response = $this->httpFactory->createResponse($code, $reasonPhrase); + return new Response($response, $this->httpFactory); + } + + public function createRequest(string $method, $uri): RequestInterface + { + return $this->httpFactory->createRequest($method, $uri); + } + + public function createUri(string $uri = ''): UriInterface + { + return $this->httpFactory->createUri($uri); + } +} diff --git a/backend/src/Http/Response.php b/backend/src/Http/Response.php new file mode 100644 index 0000000..da5e64a --- /dev/null +++ b/backend/src/Http/Response.php @@ -0,0 +1,165 @@ +response + ->withHeader('Pragma', 'no-cache') + ->withHeader('Expires', gmdate('D, d M Y H:i:s \G\M\T', 0)) + ->withHeader('Cache-Control', 'private, no-cache, no-store') + ->withHeader('X-Accel-Expires', '0'); // CloudFlare + + return new static($response, $this->streamFactory); + } + + /** + * Send headers that expire the content in the specified number of seconds. + * + * @param int $seconds + * + * @return static + */ + public function withCacheLifetime(int $seconds = self::CACHE_ONE_MONTH): static + { + $response = $this->response + ->withHeader('Pragma', '') + ->withHeader('Expires', gmdate('D, d M Y H:i:s \G\M\T', time() + $seconds)) + ->withHeader('Cache-Control', 'public, must-revalidate, max-age=' . $seconds) + ->withHeader('X-Accel-Expires', (string)$seconds); // CloudFlare + + return new static($response, $this->streamFactory); + } + + /** + * Returns whether the request has a "cache lifetime" assigned to it. + */ + public function hasCacheLifetime(): bool + { + if ($this->response->hasHeader('Pragma')) { + return (!str_contains($this->response->getHeaderLine('Pragma'), 'no-cache')); + } + + return (!str_contains($this->response->getHeaderLine('Cache-Control'), 'no-cache')); + } + + /** + * Don't escape forward slashes by default on JSON responses. + * + * @param mixed $data + * @param int|null $status + * @param int $options + * @param int $depth + */ + public function withJson($data, ?int $status = null, int $options = 0, int $depth = 512): ResponseInterface + { + $options |= JSON_UNESCAPED_SLASHES; + $options |= JSON_UNESCAPED_UNICODE; + + return parent::withJson($data, $status, $options, $depth); + } + + /** + * Stream the contents of a file directly through to the response. + * + * @param string $file_path + * @param null $file_name + * + * @return static + */ + public function renderFile(string $file_path, $file_name = null): static + { + set_time_limit(600); + + if (null === $file_name) { + $file_name = basename($file_path); + } + + $stream = $this->streamFactory->createStreamFromFile($file_path); + + $response = $this->response + ->withHeader('Pragma', 'public') + ->withHeader('Expires', '0') + ->withHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') + ->withHeader('Content-Type', mime_content_type($file_path) ?: '') + ->withHeader('Content-Length', (string)filesize($file_path)) + ->withHeader('Content-Disposition', 'attachment; filename=' . $file_name) + ->withBody($stream); + + return new static($response, $this->streamFactory); + } + + /** + * Write a string of file data to the response as if it is a file for download. + * + * @param string $file_data + * @param string $content_type + * @param string|null $file_name + * + * @return static + */ + public function renderStringAsFile(string $file_data, string $content_type, ?string $file_name = null): static + { + $response = $this->response + ->withHeader('Pragma', 'public') + ->withHeader('Expires', '0') + ->withHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') + ->withHeader('Content-Type', $content_type); + + if ($file_name !== null) { + $response = $response->withHeader('Content-Disposition', 'attachment; filename=' . $file_name); + } + + $response->getBody()->write($file_data); + + return new static($response, $this->streamFactory); + } + + /** + * Write a stream to the response as if it is a file for download. + * + * @param StreamInterface $fileStream + * @param string $contentType + * @param string|null $fileName + * + * @return static + */ + public function renderStreamAsFile( + StreamInterface $fileStream, + string $contentType, + ?string $fileName = null + ): static { + set_time_limit(600); + + $response = $this->response + ->withHeader('Pragma', 'public') + ->withHeader('Expires', '0') + ->withHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') + ->withHeader('Content-Type', $contentType); + + if ($fileName !== null) { + $response = $response->withHeader('Content-Disposition', 'attachment; filename=' . $fileName); + } + + $response = $response->withBody($fileStream); + + return new static($response, $this->streamFactory); + } +} diff --git a/backend/src/Http/ServerRequest.php b/backend/src/Http/ServerRequest.php new file mode 100644 index 0000000..40815e2 --- /dev/null +++ b/backend/src/Http/ServerRequest.php @@ -0,0 +1,103 @@ +getAttribute(self::ATTR_IP, 'UNKNOWN'); + } + + public function getView(): View + { + return $this->getAttributeOfClass(self::ATTR_VIEW, View::class); + } + + public function getSession(): SessionInterface + { + return $this->getAttributeOfClass(self::ATTR_SESSION, SessionInterface::class); + } + + public function getCsrf(): Csrf + { + return $this->getAttributeOfClass(self::ATTR_SESSION_CSRF, Csrf::class); + } + + public function getFlash(): Flash + { + return $this->getAttributeOfClass(self::ATTR_SESSION_FLASH, Flash::class); + } + + public function getRouter(): RouteParserInterface + { + return $this->getAttributeOfClass(RouteContext::ROUTE_PARSER, RouteParserInterface::class); + } + + public function getRoute(): ?RouteInterface + { + return $this->getAttribute(RouteContext::ROUTE); + } + + public function getCurrentUser(): ?User + { + return $this->getAttribute(self::ATTR_CURRENT_USER); + } + + public function isLoggedIn(): bool + { + $currentUser = $this->getCurrentUser(); + return null !== $currentUser; + } + + /** + * @param string $attr + * @param string $class_name + * + * @throws InvalidArgumentException + */ + private function getAttributeOfClass(string $attr, string $class_name): mixed + { + $object = $this->serverRequest->getAttribute($attr); + + if (empty($object)) { + throw new InvalidArgumentException( + sprintf( + 'Attribute "%s" is required and is empty in this request', + $attr + ) + ); + } + + if (!($object instanceof $class_name)) { + throw new InvalidArgumentException( + sprintf( + 'Attribute "%s" must be of type "%s".', + $attr, + $class_name + ) + ); + } + + return $object; + } +} diff --git a/backend/src/Middleware/Auth/RequireAdmin.php b/backend/src/Middleware/Auth/RequireAdmin.php new file mode 100644 index 0000000..33b45a1 --- /dev/null +++ b/backend/src/Middleware/Auth/RequireAdmin.php @@ -0,0 +1,35 @@ +getAttribute(ServerRequest::ATTR_CURRENT_USER); + + if ($user === null) { + throw new NotLoggedInException($request); + } + + if (!$user->isAdmin()) { + throw new PermissionDeniedException($request); + } + + return $handler->handle($request); + } +} diff --git a/backend/src/Middleware/Auth/RequireLoggedIn.php b/backend/src/Middleware/Auth/RequireLoggedIn.php new file mode 100644 index 0000000..9bb94d7 --- /dev/null +++ b/backend/src/Middleware/Auth/RequireLoggedIn.php @@ -0,0 +1,28 @@ +getAttribute(ServerRequest::ATTR_CURRENT_USER); + + if ($user === null) { + throw new NotLoggedInException($request); + } + + return $handler->handle($request); + } +} diff --git a/backend/src/Middleware/Auth/RequireMod.php b/backend/src/Middleware/Auth/RequireMod.php new file mode 100644 index 0000000..1f40e6b --- /dev/null +++ b/backend/src/Middleware/Auth/RequireMod.php @@ -0,0 +1,35 @@ +getAttribute(ServerRequest::ATTR_CURRENT_USER); + + if ($user === null) { + throw new NotLoggedInException($request); + } + + if (!$user->isMod()) { + throw new PermissionDeniedException($request); + } + + return $handler->handle($request); + } +} diff --git a/backend/src/Middleware/EnableSession.php b/backend/src/Middleware/EnableSession.php new file mode 100644 index 0000000..4d6cf1f --- /dev/null +++ b/backend/src/Middleware/EnableSession.php @@ -0,0 +1,77 @@ +environment->isCli()) { + $psr6Cache = new ArrayAdapter(); + } + + $this->cachePool = new ProxyAdapter($psr6Cache, 'session.'); + } + + public function getSessionPersistence(): CacheSessionPersistence + { + return new CacheSessionPersistence( + cache: $this->cachePool, + cookieName: 'app_session', + cookiePath: '/', + cacheLimiter: 'nocache', + cacheExpire: 43200, + lastModified: time(), + persistent: true, + cookieSecure: $this->environment->isProduction(), + cookieHttpOnly: true + ); + } + + public function injectSession( + SessionInterface $session, + ServerRequestInterface $request + ): ServerRequestInterface { + $csrf = new Csrf($session); + $flash = new Flash($session); + + return $request->withAttribute(ServerRequest::ATTR_SESSION, $session) + ->withAttribute(ServerRequest::ATTR_SESSION_CSRF, $csrf) + ->withAttribute(ServerRequest::ATTR_SESSION_FLASH, $flash); + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $sessionPersistence = $this->getSessionPersistence(); + $session = new LazySession($sessionPersistence, $request); + + $request = $this->injectSession($session, $request); + $response = $handler->handle($request); + + return $sessionPersistence->persistSession($session, $response); + } +} diff --git a/backend/src/Middleware/EnableView.php b/backend/src/Middleware/EnableView.php new file mode 100644 index 0000000..3d51fb8 --- /dev/null +++ b/backend/src/Middleware/EnableView.php @@ -0,0 +1,37 @@ +view->setRequest($request); + + $request = $request->withAttribute( + ServerRequest::ATTR_VIEW, + $view + ); + + return $handler->handle($request); + } +} diff --git a/backend/src/Middleware/GetCurrentUser.php b/backend/src/Middleware/GetCurrentUser.php new file mode 100644 index 0000000..1eda6f5 --- /dev/null +++ b/backend/src/Middleware/GetCurrentUser.php @@ -0,0 +1,88 @@ +db = $di->get(Connection::class); + } + + public function process( + ServerRequestInterface $request, + RequestHandlerInterface $handler + ): ResponseInterface { + $session = $request->getAttribute(ServerRequest::ATTR_SESSION); + + if (!($session instanceof SessionInterface)) { + throw new \InvalidArgumentException('User check is before session init.'); + } + + if ($session->has('valid_id')) { + $uid = $session->get('valid_id'); + + $row = $this->db->fetchAssociative( + <<<'SQL' + SELECT * + FROM web_users + WHERE id = :id + SQL, + [ + 'id' => $uid, + ] + ); + + // Check for banned status on all page loads. + if ($row === false || $row['banned'] === 1) { + $session->clear(); + $session->regenerate(); + + return (new HttpFactory()) + ->createResponse(302) + ->withHeader('Location', '/'); + } + + $session->set('valid_time', time()); + + $currentUser = new User($row); + + $this->db->update( + 'web_users', + [ + 'lastactive' => time(), + 'online' => '1', + 'lastip' => $request->getAttribute(ServerRequest::ATTR_IP, 'UNKNOWN'), + ], + [ + 'id' => $uid, + ] + ); + } else { + $currentUser = null; + } + + $request = $request->withAttribute(ServerRequest::ATTR_CURRENT_USER, $currentUser); + + return $handler->handle($request); + } +} diff --git a/backend/src/Middleware/GetRemoteIp.php b/backend/src/Middleware/GetRemoteIp.php new file mode 100644 index 0000000..df74ac2 --- /dev/null +++ b/backend/src/Middleware/GetRemoteIp.php @@ -0,0 +1,34 @@ +withAttribute( + ServerRequest::ATTR_IP, + $ip + ); + + return $handler->handle($request); + } +} diff --git a/backend/src/Middleware/RemoveSlashes.php b/backend/src/Middleware/RemoveSlashes.php new file mode 100644 index 0000000..bba9cfb --- /dev/null +++ b/backend/src/Middleware/RemoveSlashes.php @@ -0,0 +1,43 @@ +getUri(); + $path = $uri->getPath(); + + if ($path !== '/') { + if (str_ends_with($path, '/')) { + while (str_ends_with($path, '/')) { + $path = substr($path, 0, -1); + } + } + + if (str_ends_with($path, '.php')) { + $path = substr($path, 0, -4); + } + + $uri = $uri->withPath($path); + return $handler->handle( + $request->withUri($uri) + ); + } + + return $handler->handle($request); + } +} diff --git a/backend/src/Service/Discord.php b/backend/src/Service/Discord.php new file mode 100644 index 0000000..2ee52ea --- /dev/null +++ b/backend/src/Service/Discord.php @@ -0,0 +1,58 @@ +logger->debug( + 'Sending Discord message...', + func_get_args() + ); + + if (!$this->environment->isProduction()) { + return ''; + } + + $data = [ + 'content' => $message, + 'embeds' => [ + [ + 'title' => $title_set, + 'description' => $desc, + 'color' => $color, + 'thumbnail' => [ + 'url' => $img_url, + ], + ], + ], + ]; + + $response = $this->http->post( + $webhookUrl, + [ + 'json' => $data, + ] + ); + + return $response->getBody()->getContents(); + } +} diff --git a/backend/src/Service/VrcApi.php b/backend/src/Service/VrcApi.php new file mode 100644 index 0000000..38d7de2 --- /dev/null +++ b/backend/src/Service/VrcApi.php @@ -0,0 +1,69 @@ + [ + 'Authorization' => $this->environment->getVrcApiKey(), + ], + ]; + + if ($priority) { + $requestConfig['headers']['X-Priority'] = 'high'; + } + if ($async) { + $requestConfig['headers']['X-Background'] = '1'; + } + + if ($body !== null) { + $requestConfig['json'] = $body; + } + + $response = $this->http->request( + $method, + $uri, + $requestConfig + ); + + if ($response->getHeaderLine('Content-Type') === 'application/json') { + return json_decode( + $response->getBody()->getContents(), + true, + JSON_THROW_ON_ERROR + ); + } else { + return $response->getBody()->getContents(); + } + } +} diff --git a/backend/src/Session/Csrf.php b/backend/src/Session/Csrf.php new file mode 100644 index 0000000..a764e39 --- /dev/null +++ b/backend/src/Session/Csrf.php @@ -0,0 +1,95 @@ +_csrf_lifetime. + * + * @param string $namespace + */ + public function generate(string $namespace = self::DEFAULT_NAMESPACE): string + { + $sessionKey = $this->getSessionIdentifier($namespace); + if ($this->session->has($sessionKey)) { + $csrf = $this->session->get($sessionKey); + if (null !== $csrf) { + return $csrf; + } + } + + $key = $this->randomString(); + $this->session->set($sessionKey, $key); + + return $key; + } + + /** + * Verify a supplied CSRF token against the tokens stored in the session. + * + * @param string $key + * @param string $namespace + */ + public function verify(string $key, string $namespace = self::DEFAULT_NAMESPACE): void + { + if (empty($key)) { + throw new \InvalidArgumentException('A CSRF token is required for this request.'); + } + + if (strlen($key) !== self::CODE_LENGTH) { + throw new \InvalidArgumentException('Malformed CSRF token supplied.'); + } + + $sessionIdentifier = $this->getSessionIdentifier($namespace); + if (!$this->session->has($sessionIdentifier)) { + throw new \InvalidArgumentException('No CSRF token supplied for this namespace.'); + } + + $sessionKey = $this->session->get($sessionIdentifier); + if (0 !== strcmp($key, $sessionKey)) { + throw new \InvalidArgumentException('Invalid CSRF token supplied.'); + } + } + + /** + * Generates a random string of given $length. + * + * @param int $length The string length. + * + * @return string The randomly generated string. + */ + private function randomString(int $length = self::CODE_LENGTH): string + { + $seed = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijqlmnopqrtsuvwxyz0123456789'; + $max = strlen($seed) - 1; + + $string = ''; + for ($i = 0; $i < $length; ++$i) { + $string .= $seed[random_int(0, $max)]; + } + + return $string; + } + + private function getSessionIdentifier(string $namespace): string + { + return 'csrf_' . $namespace; + } +} diff --git a/backend/src/Session/Flash.php b/backend/src/Session/Flash.php new file mode 100644 index 0000000..cf32623 --- /dev/null +++ b/backend/src/Session/Flash.php @@ -0,0 +1,102 @@ +addMessage($message, FlashLevels::Success, $saveInSession); + } + + public function warning( + string $message, + bool $saveInSession = true + ): void { + $this->addMessage($message, FlashLevels::Warning, $saveInSession); + } + + public function error( + string $message, + bool $saveInSession = true + ): void { + $this->addMessage($message, FlashLevels::Error, $saveInSession); + } + + public function info( + string $message, + bool $saveInSession = true + ): void { + $this->addMessage($message, FlashLevels::Info, $saveInSession); + } + + public function addMessage( + string $message, + FlashLevels $level = FlashLevels::Info, + bool $saveInSession = true + ): void { + $messageRow = [ + 'text' => $message, + 'color' => $level->value, + ]; + + $this->getMessages(); + $this->messages[] = $messageRow; + + if ($saveInSession) { + $messages = (array)$this->session->get(self::SESSION_KEY); + $messages[] = $messageRow; + + $this->session->set(self::SESSION_KEY, $messages); + } + } + + /** + * Indicate whether messages are currently pending display. + */ + public function hasMessages(): bool + { + $messages = $this->getMessages(); + return (count($messages) > 0); + } + + /** + * Return all messages, removing them from the internal storage in the process. + * + * @return array + */ + public function getMessages(): array + { + if (null === $this->messages) { + if ($this->session->has(self::SESSION_KEY)) { + $this->messages = (array)$this->session->get(self::SESSION_KEY); + $this->session->unset(self::SESSION_KEY); + } else { + $this->messages = []; + } + } + + return $this->messages; + } +} diff --git a/backend/src/Session/FlashLevels.php b/backend/src/Session/FlashLevels.php new file mode 100644 index 0000000..3bedc88 --- /dev/null +++ b/backend/src/Session/FlashLevels.php @@ -0,0 +1,13 @@ +sections = new GlobalSections(); + + $this->addFolder('layouts', dirname(__DIR__) . '/templates/layouts'); + + $this->addData( + [ + 'sections' => $this->sections, + 'environment' => $environment, + 'router' => $router, + ] + ); + } + + public function setRequest(ServerRequestInterface $request): self + { + $view = clone $this; + + $view->addData([ + 'request' => $request, + ]); + + if ($request instanceof ServerRequest) { + $view->addData([ + 'route' => $request->getAttribute(RouteContext::ROUTE), + 'session' => $request->getAttribute(ServerRequest::ATTR_SESSION), + 'csrf' => $request->getAttribute(ServerRequest::ATTR_SESSION_CSRF), + 'flash' => $request->getAttribute(ServerRequest::ATTR_SESSION_FLASH), + 'user' => $request->getCurrentUser(), + 'is_logged_in' => $request->isLoggedIn(), + ]); + } + + return $view; + } + + public function reset(): void + { + $this->sections = new GlobalSections(); + $this->data = new Data(); + } + + /** + * @param string $name + * @param array $data + */ + public function fetch(string $name, array $data = []): string + { + return $this->render($name, $data); + } + + /** + * Trigger rendering of template and write it directly to the PSR-7 compatible Response object. + * + * @param ResponseInterface $response + * @param string $templateName + * @param array $templateArgs + */ + public function renderToResponse( + ResponseInterface $response, + string $templateName, + array $templateArgs = [] + ): ResponseInterface { + $response->getBody()->write( + $this->render($templateName, $templateArgs) + ); + return $response->withHeader('Content-type', 'text/html; charset=utf-8'); + } + + public static function staticPage( + string $templateName, + array $templateArgs = [] + ): callable { + return function ( + ServerRequest $request, + Response $response + ) use ( + $templateName, + $templateArgs + ): ResponseInterface { + return $request->getView()->renderToResponse( + $response, + $templateName, + $templateArgs + ); + }; + } +} diff --git a/backend/src/View/GlobalSections.php b/backend/src/View/GlobalSections.php new file mode 100644 index 0000000..3bbae51 --- /dev/null +++ b/backend/src/View/GlobalSections.php @@ -0,0 +1,115 @@ + + */ +final class GlobalSections implements ArrayAccess +{ + private int $sectionMode = Template::SECTION_MODE_REWRITE; + private array $sections = []; + private ?string $sectionName = null; + + public function has(string $section): bool + { + return !empty($this->sections[$section]); + } + + public function offsetExists(mixed $offset): bool + { + return $this->has((string)$offset); + } + + public function get(string $section, ?string $default = null): ?string + { + return $this->sections[$section] ?? $default; + } + + public function offsetGet(mixed $offset): mixed + { + return $this->get((string)$offset); + } + + public function unset(string $section): void + { + unset($this->sections[$section]); + } + + public function offsetUnset(mixed $offset): void + { + $this->unset((string)$offset); + } + + public function set( + string $section, + ?string $value, + int $mode = Template::SECTION_MODE_REWRITE + ): void { + $initialValue = $this->sections[$section] ?? ''; + + $this->sections[$section] = match ($mode) { + Template::SECTION_MODE_PREPEND => $value . $initialValue, + Template::SECTION_MODE_APPEND => $initialValue . $value, + default => $value + }; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + $this->set((string)$offset, (string)$value); + } + + public function prepend(string $section, ?string $value): void + { + $this->set($section, $value, Template::SECTION_MODE_PREPEND); + } + + public function append(string $section, ?string $value): void + { + $this->set($section, $value, Template::SECTION_MODE_APPEND); + } + + public function start(string $name): void + { + if ($this->sectionName) { + throw new LogicException('You cannot nest sections within other sections.'); + } + + $this->sectionName = $name; + + ob_start(); + } + + public function appendStart(string $name): void + { + $this->sectionMode = Template::SECTION_MODE_APPEND; + $this->start($name); + } + + public function prependStart(string $name): void + { + $this->sectionMode = Template::SECTION_MODE_PREPEND; + $this->start($name); + } + + public function end(): void + { + if (is_null($this->sectionName)) { + throw new LogicException( + 'You must start a section before you can stop it.' + ); + } + + $this->set($this->sectionName, ob_get_clean() ?: null, $this->sectionMode); + + $this->sectionName = null; + $this->sectionMode = Template::SECTION_MODE_REWRITE; + } +} diff --git a/backend/templates/about.phtml b/backend/templates/about.phtml new file mode 100644 index 0000000..5e8d4a3 --- /dev/null +++ b/backend/templates/about.phtml @@ -0,0 +1,88 @@ +layout('layouts::site'); +?> + +
+

About WaterWolf

+
+
+

Organization Structure

+ Organizational chart. + +

WaterWolf Mission Statement

+

WaterWolf exists at the forefront of VRChat adventures, + pioneering a realm where the possibilities of virtual reality are not just explored but + reimagined. Founded by the dynamic duo, Isaac and Cat, our mission is to revolutionize the + VRChat experience, creating a world that is both enchanting and welcoming to all. +

At the heart of WaterWolf lies a community-driven ethos. We believe in the power of + collaboration and creativity, where every player's feedback becomes a cornerstone in our + journey of innovation. Our adventures are more than just virtual experiences; they are + meticulously crafted journeys, rich in detail and imagination, designed to captivate, + engage, and inspire.

+

Our founders, Isaac and Cat, embody the spirit of partnership and unity. Their unique + talents + and perspectives blend seamlessly to offer unparalleled VRChat adventures. This synergy is + not just within our leadership but extends to every member of our community. We are a + tapestry of diverse voices, each adding depth and color to the WaterWolf experience.

+

In the world of WaterWolf, innovation is continuous. We are committed to pushing the + boundaries of what is possible in virtual reality, ensuring that each adventure is fresh, + unique, and tailored to the evolving desires of our players. Our vision is to make the VR + landscape not only more engaging but also a haven for friendship, discovery, and shared + experiences.

+

Led by Isaac and Cat, our community is the driving force behind an ever-expanding + empire of + kindness and connection. At WaterWolf, we don't just create virtual worlds; we foster an + environment where every individual feels valued, heard, and excited to be a part of + something groundbreaking.

+

Join us in this extraordinary journey. With WaterWolf, every step into VRChat becomes a + step + into a world of endless possibilities, a world where imagination knows no bounds, and where + every adventure is a gateway to new horizons.

+ +

WaterWolf: Embracing a New Horizon as a TV Network

+

WaterWolf, known for pioneering the realm of VRChat adventures, is excited to announce + our latest + venture as a TV network, dedicated to broadcasting our unique media content on the internet. + This expansion aligns with our mission to revolutionize the VRChat experience and to share our + enchanting and welcoming world with a broader audience.

+ +

Community-Driven Ethos

+

At the core of WaterWolf is our belief in collaboration and creativity. As a TV + network, we aim + to bring this community spirit to a wider audience, showcasing the rich, player-influenced + journeys that have defined us.

+ +

Creative Leadership

+

Our founders, Isaac and Cat, have always been the driving force behind our innovative + adventures. + Their vision will now extend to our TV network, promising broadcasts that are as captivating and + imaginative as our VRChat experiences.

+ +

Continuous Innovation

+

Just as we've continuously pushed the boundaries in virtual reality, our TV network + will strive + for groundbreaking content, offering fresh and unique perspectives to the world of online + media.

+ +

Expanding Our Community

+

This new venture into broadcasting is more than a platform expansion; it's an + invitation for more + individuals to experience the WaterWolf world. We aim to foster an environment where everyone + feels valued and part of something transformative, whether they join us in VRChat or through our + broadcasts.

+ +

Join us in this extraordinary new chapter. With WaterWolf, every adventure, whether in + VRChat or + through our TV network, is a gateway to new horizons and limitless possibilities.

+ + +
+
+
diff --git a/backend/templates/account/forgot.phtml b/backend/templates/account/forgot.phtml new file mode 100644 index 0000000..2ef6a7b --- /dev/null +++ b/backend/templates/account/forgot.phtml @@ -0,0 +1,41 @@ +layout('layouts::site'); +?> + +
+
+
+
+

+ Forgot My Password +

+

+ Enter your e-mail address below and a recovery code will be sent to you. +

+ + +
+ Error: +
+ + +
+ + +
+ +
+ +
+
+
+
+
diff --git a/backend/templates/account/login.phtml b/backend/templates/account/login.phtml new file mode 100644 index 0000000..3566a04 --- /dev/null +++ b/backend/templates/account/login.phtml @@ -0,0 +1,66 @@ +layout('layouts::site'); +?> + + + +
+
+

Log In

+
+ +
+
+
diff --git a/backend/templates/account/recover.phtml b/backend/templates/account/recover.phtml new file mode 100644 index 0000000..d5eb9ca --- /dev/null +++ b/backend/templates/account/recover.phtml @@ -0,0 +1,66 @@ +layout('layouts::site'); +?> + +
+
+
+
+

+ Reset Account Password +

+

+ Enter a new password below to reset your account password and log in. +

+ + +
+ Error: +
+ + +
+ +
+
+
+ +
+ +
+ +
+ + + +
+ +
+
+
+
+
+ + diff --git a/backend/templates/account/register.phtml b/backend/templates/account/register.phtml new file mode 100644 index 0000000..6ba44e1 --- /dev/null +++ b/backend/templates/account/register.phtml @@ -0,0 +1,129 @@ +layout('layouts::site'); +?> + + + +
+ +
+
You have been referred by:
+
+

+ +

+
+
+ + +
+
+

Register to WaterWolf

+
+
+ +
+ Error: +
+ + +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + + +
+
+
+ +
+
+ +
+
+ + +
+
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+
+
+ + diff --git a/backend/templates/calendar.phtml b/backend/templates/calendar.phtml new file mode 100644 index 0000000..132043d --- /dev/null +++ b/backend/templates/calendar.phtml @@ -0,0 +1,17 @@ +layout('layouts::site'); +?> + +
+
+
+
+ +
+
+
+
diff --git a/backend/templates/common/breadcrumbs.phtml b/backend/templates/common/breadcrumbs.phtml new file mode 100644 index 0000000..2968307 --- /dev/null +++ b/backend/templates/common/breadcrumbs.phtml @@ -0,0 +1,21 @@ + + + diff --git a/backend/templates/common/countries.phtml b/backend/templates/common/countries.phtml new file mode 100644 index 0000000..2b5361a --- /dev/null +++ b/backend/templates/common/countries.phtml @@ -0,0 +1,251 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/templates/common/frontend_assets.phtml b/backend/templates/common/frontend_assets.phtml new file mode 100644 index 0000000..0d56d2d --- /dev/null +++ b/backend/templates/common/frontend_assets.phtml @@ -0,0 +1,90 @@ +isProduction()) { + // Convert from the Vite manifest format into HTML header tags. + $manifestFile = $environment->getBaseDirectory() . '/web/static/dist/.vite/manifest.json'; + $vueComponents = json_decode( + file_get_contents($manifestFile), + true, + 512, + \JSON_THROW_ON_ERROR + ); + + $includes = [ + 'js' => $assetRoot . '/' . $vueComponents[$component]['file'], + 'css' => [], + 'prefetch' => [], + ]; + + $visitedNodes = []; + $fetchCss = function ($component) use ( + $vueComponents, + $assetRoot, + &$includes, + &$fetchCss, + &$visitedNodes + ): void { + if (!isset($vueComponents[$component]) || isset($visitedNodes[$component])) { + return; + } + + $visitedNodes[$component] = true; + + $componentInfo = $vueComponents[$component]; + if (isset($componentInfo['css'])) { + foreach ($componentInfo['css'] as $css) { + $includes['css'][] = $assetRoot . '/' . $css; + } + } + + if (isset($componentInfo['file'])) { + $fileUrl = $assetRoot . '/' . $componentInfo['file']; + if ($fileUrl !== $includes['js']) { + $includes['prefetch'][] = $fileUrl; + } + } + + if (isset($componentInfo['imports'])) { + foreach ($componentInfo['imports'] as $import) { + $fetchCss($import); + } + } + }; + + $fetchCss($component); +} else { + $includes = [ + 'js' => $assetRoot . '/' . $component, + 'css' => [], + 'prefetch' => [], + ]; +} + +$sections->appendStart('head'); + +echo << +HTML; + +foreach ($includes['prefetch'] as $prefetchDep) { + echo << + HTML; +} + +foreach ($includes['css'] as $cssDep) { + echo << + HTML; +} + +$sections->end(); +?> diff --git a/backend/templates/common/genres.phtml b/backend/templates/common/genres.phtml new file mode 100644 index 0000000..acf0b4a --- /dev/null +++ b/backend/templates/common/genres.phtml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/templates/common/global_comments.phtml b/backend/templates/common/global_comments.phtml new file mode 100644 index 0000000..f679693 --- /dev/null +++ b/backend/templates/common/global_comments.phtml @@ -0,0 +1,28 @@ + $comment_id, + 'isLoggedIn' => $is_logged_in, + 'isMod' => ($is_logged_in && $user?->isMod()), +]; +?> + +fetch('common/frontend_assets', [ + 'component' => 'frontend/vue/comments.js', +]) ?> + +
 
+ + + + diff --git a/backend/templates/common/pronouns.phtml b/backend/templates/common/pronouns.phtml new file mode 100644 index 0000000..a8e2328 --- /dev/null +++ b/backend/templates/common/pronouns.phtml @@ -0,0 +1,5 @@ + + + + + diff --git a/backend/templates/dashboard/admin/add_world.phtml b/backend/templates/dashboard/admin/add_world.phtml new file mode 100644 index 0000000..1861511 --- /dev/null +++ b/backend/templates/dashboard/admin/add_world.phtml @@ -0,0 +1,35 @@ +layout('layouts::dashboard'); +?> + +fetch('common/breadcrumbs', [ + 'urls' => [ + $router->urlFor('dashboard') => 'My Dashboard', + ], + 'current' => 'Add VRChat World', +]) ?> + +

Add VRChat World

+ + +
+ Error: +
+ + +
+
+ + +
+ + +
diff --git a/backend/templates/dashboard/admin/users.phtml b/backend/templates/dashboard/admin/users.phtml new file mode 100644 index 0000000..c12fc04 --- /dev/null +++ b/backend/templates/dashboard/admin/users.phtml @@ -0,0 +1,120 @@ +layout('layouts::dashboard'); +?> + +fetch('common/breadcrumbs', [ + 'urls' => [ + $router->urlFor('dashboard') => 'My Dashboard', + ], + 'current' => 'User Management', +]) ?> + +

User Management

+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
UIDUsernameAdmin?Mod?Team?DJ?Banned?Action
YesNoYesNoYesNoYesNoYesNo + + Edit + + + View + +
+ + diff --git a/backend/templates/dashboard/avatar.phtml b/backend/templates/dashboard/avatar.phtml new file mode 100644 index 0000000..dc401e4 --- /dev/null +++ b/backend/templates/dashboard/avatar.phtml @@ -0,0 +1,95 @@ +layout('layouts::dashboard'); +?> + +fetch('common/breadcrumbs', [ + 'urls' => [ + $router->urlFor('dashboard') => 'My Dashboard', + $router->urlFor('dashboard:profile') => 'Edit My Profile', + ], + 'current' => ($type === 'dj') + ? 'Upload DJ Avatar' + : 'Upload Avatar', +]) ?> + + +

Upload DJ Avatar

+ +

Upload Avatar

+ + +
+
+
+
+ + +
+
+ +
+ +
+
+
+
+ + diff --git a/backend/templates/dashboard/dmx.phtml b/backend/templates/dashboard/dmx.phtml new file mode 100644 index 0000000..9fac1ad --- /dev/null +++ b/backend/templates/dashboard/dmx.phtml @@ -0,0 +1,296 @@ +layout('layouts::dashboard'); +?> + +fetch('common/breadcrumbs', [ + 'urls' => [ + $router->urlFor('dashboard') => 'My Dashboard', + ], + 'current' => 'DMX Lights', +]) ?> + +

DMX Lights

+ + + + + + + + + + +
+ + + +
+ + +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDFixture NameUniverse NumberChannel NumberRig NameActions
+
+ + +
+
+ + +
+
+ + diff --git a/backend/templates/dashboard/index.phtml b/backend/templates/dashboard/index.phtml new file mode 100644 index 0000000..f610a87 --- /dev/null +++ b/backend/templates/dashboard/index.phtml @@ -0,0 +1,17 @@ +layout('layouts::dashboard'); +?> + +

Welcome to WaterWolf

+ + diff --git a/backend/templates/dashboard/password.phtml b/backend/templates/dashboard/password.phtml new file mode 100644 index 0000000..064e250 --- /dev/null +++ b/backend/templates/dashboard/password.phtml @@ -0,0 +1,79 @@ +layout('layouts::dashboard'); +?> + +fetch('common/breadcrumbs', [ + 'urls' => [ + $router->urlFor('dashboard') => 'My Dashboard', + ], + 'current' => 'Change My Password', +]) ?> + +

Change My Password

+ + +
+ Error: +
+ + +
+
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ +
+
+
+ + +
+
+
+ + diff --git a/backend/templates/dashboard/posters/edit.phtml b/backend/templates/dashboard/posters/edit.phtml new file mode 100644 index 0000000..5e3c205 --- /dev/null +++ b/backend/templates/dashboard/posters/edit.phtml @@ -0,0 +1,151 @@ +layout('layouts::dashboard'); +?> + +fetch('common/breadcrumbs', [ + 'urls' => [ + $router->urlFor('dashboard') => 'My Dashboard', + $router->urlFor('dashboard:posters') => 'Poster Network', + ], + 'current' => ($isEditMode) ? 'Edit Poster' : 'Upload New Poster', +]) ?> + +

Poster Network

+ +
+

+ + Edit Poster + + Upload New Poster + +

+
+ +
+ Error: +
+ + +
+
+
+
+ + + + + + + +

+ To replace the existing poster, upload a new file here. Otherwise, leave this field + blank. +

+ +

+ Most image formats accepted.
+ The picture will be converted to a JPG and scaled and cropped to 590x1000. +

+ +
+ +
+ + fetch( + 'form/select', + [ + 'name' => 'type', + 'options' => [ + '' => 'No Type', + ...$types, + ], + 'selected' => $row['type_id'] ?? '', + ] + ) ?> +

+ Optionally specify the type of poster so worlds can choose whether to display it. +

+
+ +
+ + +

+ Optionally set a time when the poster will automatically expire and won't be displayed. +

+
+
+
+
+ + +

+ Optionally specify the name of a collection that this poster is a part of. Multiple posters + can be in a collection, and world builders can filter posters by the collection name. +

+
+ + +
+ + fetch( + 'form/select', + [ + 'name' => 'group', + 'options' => [ + '' => 'No Group', + ...$groups, + ], + 'selected' => $row['group_id'] ?? '', + ] + ) ?> +

+ Optionally upload this poster under a group so everyone in that group can manage it. +

+
+ +
+
+ + +
+
+
diff --git a/backend/templates/dashboard/posters/list.phtml b/backend/templates/dashboard/posters/list.phtml new file mode 100644 index 0000000..9ba96be --- /dev/null +++ b/backend/templates/dashboard/posters/list.phtml @@ -0,0 +1,129 @@ +layout('layouts::dashboard'); +?> + +fetch('common/breadcrumbs', [ + 'urls' => [ + $router->urlFor('dashboard') => 'My Dashboard', + ], + 'current' => 'Poster Network', +]) ?> + +

Poster Network

+ +
+
+

View Posters

+ + +
+ +
+ $group) : ?> +
+
+ +
+ +
+ +
+ + Poster + + + +
Creator:
+ + + +
Expired
+ +
Expires
+ + + +
Collection:
+ + + +
Type:
+ + + +
Views:
+ + + +
+
+
+ +
+
+ +
+
diff --git a/backend/templates/dashboard/profile.phtml b/backend/templates/dashboard/profile.phtml new file mode 100644 index 0000000..9296b79 --- /dev/null +++ b/backend/templates/dashboard/profile.phtml @@ -0,0 +1,477 @@ +layout('layouts::dashboard'); + +if ($isAdminMode) { + echo $this->fetch('common/breadcrumbs', [ + 'urls' => [ + $router->urlFor('dashboard') => 'My Dashboard', + $router->urlFor('dashboard:admin:users') => 'User Management', + ], + 'current' => 'Edit User: ' . $profile['username'], + ]); +} else { + echo $this->fetch('common/breadcrumbs', [ + 'urls' => [ + $router->urlFor('dashboard') => 'My Dashboard', + ], + 'current' => 'Edit My Profile', + ]); +} +?> + + +

Edit User:

+ +

Edit My Profile

+ + + +
+ Error: +
+ + +
+
+
+ +
+
+ +
+
+
+
+ + Avatar +
+
+ +
+
+ + + Avatar + + +
+
+ +
+ + +
+ +
+
+ +
+
+

Basic Details

+
+
+ + +
+ +
+ + +
+ +
+ + + + fetch('common/pronouns') ?> + +
+ +
+ + + +
+
+
+
+ +
+ +
+

Team Profile

+
+
+ + +
+
+
+ + +
+

Social Media

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

DJ Avatar

+ + DJ Avatar +
+
+ +
+
+ + + DJ Avatar + + +
+
+ +
+ +

+ Your DJ avatar is your regular user avatar unless you upload a custom DJ one. +

+ + +
+ +
+
+ +
+
+

DJ Profile Info

+
+
+ + +
+ +
+ + +
+ +
+ + + + fetch('common/genres') ?> + +
+
+
+
+
+
+ + + +
+
+
+
+

Referral Roster

+
+ The referred friend list is currently in development. +
+
+
+
+
+

Referral Link

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

VRChat Settings

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

User Flags

+
+
+ checked="checked"> + +
+
+
+ checked="checked"> + +

Lists user on the "Team" page.

+
+
+ checked="checked"> + +

Grants content moderation permissions.

+
+
+ checked="checked"> + +

Grants full admin permissions.

+
+
+
+ checked="checked"> + +
+
+
+
+
+
+

Group Membership

+
+ $groupName) : ?> +
+ checked> + +
+ +
+
+ +
+

User Details

+
+
+
Registered on:
+
+ +
Country of Origin:
+
+ +
Last Active:
+
+ +
Last Login:
+
+ +
Last Logged In IP:
+
+ +
Successful Logins:
+
+ +
Unsuccessful Logins:
+
+
+
+
+
+
+
+ + +
+
+
diff --git a/backend/templates/dashboard/short_urls/edit.phtml b/backend/templates/dashboard/short_urls/edit.phtml new file mode 100644 index 0000000..e37c040 --- /dev/null +++ b/backend/templates/dashboard/short_urls/edit.phtml @@ -0,0 +1,62 @@ +layout('layouts::dashboard'); +?> + +fetch('common/breadcrumbs', [ + 'urls' => [ + $router->urlFor('dashboard') => 'My Dashboard', + $router->urlFor('dashboard:short_urls') => 'Short URLs', + ], + 'current' => ($isEditMode) ? 'Edit Short URL' : 'Create Short URL', +]) ?> + +

Short URLs

+ +
+

+ + Edit Short URL + + Create Short URL + +

+
+
+
+
+
+ + +

The portion after the base URL (i.e. foo for + wtr.wf/foo). Don't include a leading or trailing slash (/).

+
+
+ + +

You can use a relative (i.e. /dashboard) or an absolute (i.e. + https://example.com) URL.

+
+
+
+ +
+ +
+
+
+
diff --git a/backend/templates/dashboard/short_urls/list.phtml b/backend/templates/dashboard/short_urls/list.phtml new file mode 100644 index 0000000..61d72d3 --- /dev/null +++ b/backend/templates/dashboard/short_urls/list.phtml @@ -0,0 +1,61 @@ +layout('layouts::dashboard'); +?> + +fetch('common/breadcrumbs', [ + 'urls' => [ + $router->urlFor('dashboard') => 'My Dashboard', + ], + 'current' => 'Short URLs', +]) ?> + +

Short URLs

+ +
+

View Short URLs

+
+ + + + + + + + + + + + + + + + + + + + + +
Short URLLong URLViewsActions
+ + Edit + + + Delete + +
+
+
diff --git a/backend/templates/dashboard/skills.phtml b/backend/templates/dashboard/skills.phtml new file mode 100644 index 0000000..afb5551 --- /dev/null +++ b/backend/templates/dashboard/skills.phtml @@ -0,0 +1,139 @@ +layout('layouts::dashboard'); +?> + +fetch('common/breadcrumbs', [ + 'urls' => [ + $router->urlFor('dashboard') => 'My Dashboard', + ], + 'current' => 'My Skills', +]) ?> + +

My Skills

+ + + + + + +
+ +
+
+

Your Talent Pool

+ + + + +
+
+

Community Talent Pool

+ 0) { + // output data of each row + foreach ($communityTalents as $row) { ?> + + +
+
diff --git a/backend/templates/defective/index.phtml b/backend/templates/defective/index.phtml new file mode 100644 index 0000000..d20ec30 --- /dev/null +++ b/backend/templates/defective/index.phtml @@ -0,0 +1,186 @@ +layout('layouts::site'); + +// Times in the configured zone below (currently EST/EDT) +$tz = 'America/New_York'; + +/** + * @var array $djs + */ +$djs = [ + [ + 'name' => 'Beamflare', + 'time' => '2024-03-09 15:00', + ], + [ + 'name' => 'Birds of Prey', + 'time' => '2024-03-09 16:00', + ], + [ + 'name' => 'Blaze/Poptart', + 'time' => '2024-03-09 17:00', + ], + [ + 'name' => 'Beeb', + 'time' => '2024-03-09 18:00', + ], + [ + 'name' => 'Draven', + 'time' => '2024-03-09 19:00', + ], + [ + 'name' => 'P I N K', + 'time' => '2024-03-09 20:00', + ], + [ + 'name' => 'DJ Altro', + 'time' => '2024-03-09 21:00', + ], + [ + 'name' => 'GOM8 (feat. Special Guest)', + 'time' => '2024-03-09 22:00', + ], + [ + 'name' => 'Physwolf', + 'time' => '2024-03-09 23:00', + ], + [ + 'name' => 'Morganite', + 'time' => '2024-03-10 00:00', + ], + [ + 'name' => 'Warned1', + 'time' => '2024-03-10 01:00', + ], + [ + 'name' => 'Sebris', + 'time' => '2024-03-10 03:00', + ], +]; + +foreach ($djs as &$dj) { + $dj['datetime'] = new \DateTimeImmutable($dj['time'], new DateTimeZone($tz)); + $dj['timestamp'] = $dj['datetime']->getTimestamp(); + $dj['time'] = $dj['datetime']->format('g:ia'); +} +unset($dj); + +$startDate = $djs[0]['timestamp']; +?> + + + +
+
+ + Defective Equipment + +
+
A VRChat Club Experience by WaterWolf
+
March 9, 2024
+
+
+ + +
+ +
+
+
+ +
+
+ +
+

DJ Lineup

+
+ + + + + + + + + + + + + + + +
DJTime
+ + + +
+
+
+
+
+ + diff --git a/backend/templates/donate.phtml b/backend/templates/donate.phtml new file mode 100644 index 0000000..07c84f0 --- /dev/null +++ b/backend/templates/donate.phtml @@ -0,0 +1,44 @@ +layout('layouts::site'); +?> + +
+

Donate

+
+
+

WaterWolf - The Future of VRChat Adventures

+

+ Imagine a place where the boundaries of virtual reality are constantly pushed, where adventures are + crafted with meticulous care, and where your feedback becomes an integral part of the creation. + Welcome to WaterWolf - a beacon of creativity and innovation in the vast universe of VRChat. +

+

+ WaterWolf is more than just a name; it's a community, a collaboration of two exceptionally talented + creators. Their unique styles and specialties converge to offer players an unparalleled journey. + Yet, the magic doesn't stop there. The diversity and creativity embedded in WaterWolf's DNA set it + apart. As players, we're always hungry for new adventures, new worlds, and novel experiences. + WaterWolf not only satiates this hunger but goes above and beyond, serving up experiences that are + fresh, captivating, and tailor-made, thanks to the invaluable feedback from its dedicated player + base. +

+

+ WaterWolf needs your support to continue its operations. By supporting WaterWolf, you're helping to + ensure that we can continue our operations into the future. We support one-time donations via PayPal or + monthly recurring donations via Discord's Club VIP role. +

+ +

Join the Revolution!

+

Join us in this exciting journey. Donate to WaterWolf today and become a part of + the revolution in VRChat adventures!

+
+ + +
+
diff --git a/backend/templates/emails/forgot.phtml b/backend/templates/emails/forgot.phtml new file mode 100644 index 0000000..9f9bc54 --- /dev/null +++ b/backend/templates/emails/forgot.phtml @@ -0,0 +1,11 @@ +Hewwo! + +A request has been submitted to recover your password on the WaterWolf web site. + +If you submitted this request, click the link below to choose a new password: + + + +If you did not submit this request, you do not need to take any action at this time. + + - The WaterWolf Team diff --git a/backend/templates/errors/generic.phtml b/backend/templates/errors/generic.phtml new file mode 100644 index 0000000..51b2da8 --- /dev/null +++ b/backend/templates/errors/generic.phtml @@ -0,0 +1,20 @@ +layout('layouts::minimal'); +?> + +
+
+
+

Error

+
+
+

getMessage()) ?>

+
+
+
+ diff --git a/backend/templates/errors/http.phtml b/backend/templates/errors/http.phtml new file mode 100644 index 0000000..1088838 --- /dev/null +++ b/backend/templates/errors/http.phtml @@ -0,0 +1,19 @@ +layout('layouts::minimal'); +?> + +
+
+
+

HTTP Error getCode()) ?>

+
+
+

getMessage()) ?>

+
+
+
diff --git a/backend/templates/form/select.phtml b/backend/templates/form/select.phtml new file mode 100644 index 0000000..1c98ab2 --- /dev/null +++ b/backend/templates/form/select.phtml @@ -0,0 +1,27 @@ + $options + * @var mixed|null $selected + */ + +$id ??= $name; +$selected ??= ''; +?> + + diff --git a/backend/templates/foxxcon/index.phtml b/backend/templates/foxxcon/index.phtml new file mode 100644 index 0000000..4c0ea52 --- /dev/null +++ b/backend/templates/foxxcon/index.phtml @@ -0,0 +1,403 @@ +layout('layouts::site'); +?> + +
+
+ +
+ +
+
+ + + + + + + + + + + + +

+

+
+
+

The VRChat club experience by WaterWolf

+

October 20th - 23rd, 2023

+
+ + + +
+
+

Foxxcon Cyberpunk

+
+
+

Dive into an immersive world of music and technology within VRChat. Foxxcon Cyberpunk, + the DJ club event created by WaterWolf team, invites you to experience three days of + high energy music—all completely free of charge.

+
+
+
+ +
+
+
+
+

Event badge

+
+
+

Prepare to immerse yourself in a futuristic world of music and technology as we bring the + cyberpunk vibes straight to your avatar. The Foxxcon badge is not just a symbol; it's a + statement, a testament to your unwavering passion for music, tech, and all things + cyberpunk.

+

We are thrilled to unveil the latest addition to the Foxxcon experience: the Cyber-pass + Badge! How to get it? It's simple, free and comes in only 2 pieces!

+

ID - Visit waterwolf.club/badge to create an + image with your name, pronouns, and photo.

+

BADGE - Download the Unity + package of the badge

+
+
+ +
+
+
+

DJ Lineup

+
+ +
+ +

DAY 1 -

October 20th, 2023 +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TimeDJGenre
11 AM EDTLaserLasershow Opening Ceremony
12 PM EDTDJ TechSaneDance Classics
1 PM EDTWnitesFlowDance EDM Hardstyle
2 PM EDTD-Fox & Ta-Fel-PootHardstyle / Rawstyle
3 PM EDTF3NN & RackoonExperimental Heavy Bass
4 PM EDTValant & DJ GoofyBreakbeat / Drum & Bass
5 PM EDTDJ Boe & BinoTechno / Acid Techno
6 PM EDTPuppoBandit, Ash Darkfire, CoryKumaTrance
7 PM EDTToberal & JustCallMeLettyTech House / Funk House
8 PM EDTPhriendlyPhantomMultigenre / Genrefluid
9 PM EDTADA & FliXMetal / MetalCore
10 PM EDTTIM3K33P3R & HEADAUXHeavy Bass Music
11 PM EDTTakumTechno / Trance
+

DAY 2 - October 21th, 2023

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TimeDJGenre
12 PM EDTKittz & Operator BurstupGlitch Hop / Hip Hop / Funk
01 PM EDTDJ Kusaki & DJ VulpBass House and EDM Mashups
02 PM EDTSchlickTechno
03 PM EDTMagus vs. P I N KDrum & Bass
04 PM EDTMOONEmo / Punk / Metal
05 PM EDTOyeM8 & GalaxisBig Room & Electro Pop
06 PM EDTKA!JU, KHALIBER & ADJACENT SILHOUETTETrap / Dubstep
07 PM EDTMorganite & WhittyMelodic Bass / Psytrance
08 PM EDTStitcharoo & JohCawElectro Clash, Bass & Swing
09 PM EDTScalie Bloke & ValueFactoryMulti-Genre
10 PM EDT2N (loprov & kazuki)Genrefluid
11 PM EDTSanfear & ekkofoxBaile Funk & Jersey Club
+

DAY 3 - October 22th, 2023

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TimeDJGenre
12 PM EDT♥ GOJII ♥ & Gaba NeonUK Garage
01 PM EDTDJ SAD GIRL & DJ HyperionFEMTECH, Uplifting Trance / Psytrance
02 PM EDTDJ Reignz & FoveroHeavy Dubstep / Colour Bass / Hardstyle / Rawstyle
03 PM EDTRisnorWolf & Ki-ReiHardstyle
04 PM EDTDAEGOTH & SPINESHATTERRiddim / Tearout
05 PM EDTWarneD1 & AmyotteTop 40 M.L.E.B.S.
06 PM EDTRu & Starport55Psytrance
07 PM EDTStrawberryProtatoMelodic House / MidTempo
08 PM EDTLaser Ray & RetroWaveTech House
09 PM EDTLinksy & stickypaw & Vicerion & ValantDrum & Bass
10 PM EDTDJ Venix & DJ ToxixxHeavy Dubstep & Metalstep
11 PM EDTPhyswolf & DJ FoxyHardstyle & Hardcore
+
+ +
+
+
+
+ + diff --git a/backend/templates/foxxcon/instances.phtml b/backend/templates/foxxcon/instances.phtml new file mode 100644 index 0000000..85b9490 --- /dev/null +++ b/backend/templates/foxxcon/instances.phtml @@ -0,0 +1,41 @@ +layout('layouts::site'); +?> + + diff --git a/backend/templates/index.phtml b/backend/templates/index.phtml new file mode 100644 index 0000000..dd24405 --- /dev/null +++ b/backend/templates/index.phtml @@ -0,0 +1,91 @@ +layout('layouts::site'); +?> + + + +
+
+
+ +
+
+
+

We are WaterWolf - The Open-Source MakerSpace of VRCHAT

+
+
+ +
+
+
+

How Do I Join?

+
+

Joining WaterWolf is quick easy and without obligation. Just join the discord and come hang + out with us! +

+ +
+ +
+ + +

Join the WaterWolf network, Make an account and get connected to tools to make + your VRC + experience amazing! +

+
+ +
+ +
+
+
+
+
+

What is WaterWolf?

+
+

WaterWolf is a community in VRChat focused on building, exploring and promoting + immersive and enjoyable worlds.

+ +

The WaterWolf community is active in many aspects of the VRChat world. Our skilled world + and avatar builders excel at creating the worlds, avatars, assets and technologies that + power some of the best VRChat experiences. Our community also explores the wider world of + VRChat, visiting and curating particularly interesting places.

+ +

WaterWolf offers a wide range of adventures to anyone who joins our community. Whether you're + making new friends, networking with talented world builders, or grooving to the beat at one + of our events, you're sure to find adventure and friendship as part of the WaterWolf family.

+
+
+
+

Dive into WaterWolf Adventures!

+
+

Ever dreamt of unparalleled adventures in VR? Welcome to WaterWolf. A mosaic + of diversity & creativity, this VRChat haven offers escapades that defy the ordinary.

+ +

Seeking memories that last? Craving resonating experiences? WaterWolf beckons. By hitting the + donate button, you're not just supporting - you're joining a story. A + chapter waiting to be written. +

+ + Donate Now +
+
+
+
+
+
diff --git a/backend/templates/layouts/dashboard.phtml b/backend/templates/layouts/dashboard.phtml new file mode 100644 index 0000000..3d97e07 --- /dev/null +++ b/backend/templates/layouts/dashboard.phtml @@ -0,0 +1,46 @@ +fetch('common/frontend_assets', [ + 'component' => 'frontend/layout.js', +]) +?> + + + + + + + + + + + + + +
+
+
+ +
+ + section('content') ?> +
+
+ +
+ +
+ + + + diff --git a/backend/templates/layouts/minimal.phtml b/backend/templates/layouts/minimal.phtml new file mode 100644 index 0000000..8e6c282 --- /dev/null +++ b/backend/templates/layouts/minimal.phtml @@ -0,0 +1,36 @@ +fetch('common/frontend_assets', [ + 'component' => 'frontend/layout.js', +]) +?> + + + + + + + +
+ +
+ +
+ section('content') ?> +
+ +
+ +
+ + diff --git a/backend/templates/layouts/sections/dashboard_sidebar.phtml b/backend/templates/layouts/sections/dashboard_sidebar.phtml new file mode 100644 index 0000000..27215f4 --- /dev/null +++ b/backend/templates/layouts/sections/dashboard_sidebar.phtml @@ -0,0 +1,82 @@ + + + diff --git a/backend/templates/layouts/sections/footer.phtml b/backend/templates/layouts/sections/footer.phtml new file mode 100644 index 0000000..290f7a6 --- /dev/null +++ b/backend/templates/layouts/sections/footer.phtml @@ -0,0 +1,41 @@ + +
+
+
+ WaterWolf Family: We Build Adventure and Friendship +
+
+

WaterWolf is a community in VRChat that focuses on creating adventures for players + to experience.

+

The community is made up of many creators, each with their own unique style and + specialties.

+
+ +
+
diff --git a/backend/templates/layouts/sections/header.phtml b/backend/templates/layouts/sections/header.phtml new file mode 100644 index 0000000..3537d88 --- /dev/null +++ b/backend/templates/layouts/sections/header.phtml @@ -0,0 +1,55 @@ + + +WaterWolf Community + + + + + + + + + + + +isProduction()): ?> + + + + + + + + + + + + + + + + + + + + + + +get('head') ?> + + diff --git a/backend/templates/layouts/sections/nav.phtml b/backend/templates/layouts/sections/nav.phtml new file mode 100644 index 0000000..94d6742 --- /dev/null +++ b/backend/templates/layouts/sections/nav.phtml @@ -0,0 +1,124 @@ +getName(); +} else { + $here = null; +} +?> + +
+ +
diff --git a/backend/templates/layouts/sections/notifications.phtml b/backend/templates/layouts/sections/notifications.phtml new file mode 100644 index 0000000..617731f --- /dev/null +++ b/backend/templates/layouts/sections/notifications.phtml @@ -0,0 +1,29 @@ + +
+ getMessages(); + if (count($messages) > 0): + foreach ($messages as $message): + ?> + + +
diff --git a/backend/templates/layouts/site.phtml b/backend/templates/layouts/site.phtml new file mode 100644 index 0000000..6f58f4c --- /dev/null +++ b/backend/templates/layouts/site.phtml @@ -0,0 +1,30 @@ +fetch('common/frontend_assets', [ + 'component' => 'frontend/layout.js', +]) +?> + + + + + + + + +
+ section('content') ?> +
+ +
+ +
+ + + + diff --git a/backend/templates/layouts/wwradio.phtml b/backend/templates/layouts/wwradio.phtml new file mode 100644 index 0000000..01942b1 --- /dev/null +++ b/backend/templates/layouts/wwradio.phtml @@ -0,0 +1,17 @@ +fetch('common/frontend_assets', [ + 'component' => 'frontend/wwradio.js', +]) +?> + + + + + + +section('content') ?> + + diff --git a/backend/templates/live.phtml b/backend/templates/live.phtml new file mode 100644 index 0000000..0e71a76 --- /dev/null +++ b/backend/templates/live.phtml @@ -0,0 +1,117 @@ +layout('layouts::site'); +?> + +
+

WaterWolf Live

+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+ + + + + + diff --git a/backend/templates/portals.phtml b/backend/templates/portals.phtml new file mode 100644 index 0000000..3fbe10c --- /dev/null +++ b/backend/templates/portals.phtml @@ -0,0 +1,118 @@ +layout('layouts::site'); +?> + +
+

Portals

+
+
+
+
Connecting the Party Through a Portal
+
+
+ +
+
+ +
+ +
+
Gallery
+
+ +
+
+
+
+
+
About Our Portals
+
+ + +

+ The WaterWolf team is passionate about bringing virtual spaces to life. + We collaborate with event hosts to offer Virtual Reality portals connecting + the virtual world with in-person events. +

+ +

+ If you're looking to host a VR portal at your event, let our skilled team + of professionals build a powerful, custom experience. With our low-latency + streaming technology and one-to-one recreations of event venues inside VRChat, + our experiences are compelling for people on both sides of the portal, allowing + your event to reach thousands of individuals who might not have had the + opportunity to participate otherwise. +

+ +

+ Our team has hosted, or will host, portals at the following events: +

+
    +
  • Everfree Northwest 2023
  • +
  • Anthro Northwest 2024
  • +
+ +

+ We also bring VR avatars to life with talented on-site VR dancers. + Our team is volunteering, or has volunteered, at these events: +

+
    +
  • Furry Weekend Atlanta 2023
  • +
  • Everfree Northwest 2023
  • +
  • Anthrocon 2023
  • +
  • Biggest Little Fur Con 2023
  • +
  • Anthro Northwest 2024
  • +
+
+
+
+
+
diff --git a/backend/templates/posters/faq.phtml b/backend/templates/posters/faq.phtml new file mode 100644 index 0000000..e6506cd --- /dev/null +++ b/backend/templates/posters/faq.phtml @@ -0,0 +1,138 @@ +layout('layouts::site'); +?> + +
+

The WaterWolf Poster Network

+ +
+
+ + +

The WaterWolf poster network allows you to easily upload and manage posters that are in + a uniform size and + format and can be used in VRChat worlds, promotional materials, and more.

+ +

Using the Poster Network

+ +
+
+

Loading a Random Poster

+ +

If you want to load a completely random poster from the uploaded collection, + load the + base URL with no query parameters:

+ +

+ + + +

+
+
+

Loading a Specific Poster by ID

+ +

You can reference a specific poster by adding its ID:

+ +

+ + ?pid=123 + +

+
+
+
+
+

Filtering by Group

+ +

Posters can be uploaded for, and managed by, specific groups. You can filter + posters by which group owns it:

+ +

+ + ?group= + +

+ +

These groups are available:

+ +
    + +
  • :
  • + +
+ +

Filtering by Poster Type

+ +

You can limit your selection to specific types of posters:

+ +

+ + ?type= + +

+ +

These types are available:

+ +
    + +
  • : +
  • + +
+
+
+

Filtering by Collection

+ +

Posters can optionally be part of a "collection" to make them easy to reference + in your worlds:

+ +

+ + ?collection=ww_events + +

+ +

More than one poster be in a collection; the poster that appears will be + shuffled between them. Collections aren't unique to groups, but you can chain filters + (see below) to select only posters owned by a group with a given collection.

+ +

Chaining Filters

+ +

You can combine the queries above to narrow your filter, for example, to only + show + posters that meet multiple criteria:

+ +

+ + ?group=&type= + +

+
+
+
+
+
diff --git a/backend/templates/profile.phtml b/backend/templates/profile.phtml new file mode 100644 index 0000000..487f539 --- /dev/null +++ b/backend/templates/profile.phtml @@ -0,0 +1,103 @@ +layout('layouts::site'); + +$website = $profile['website'] ?? ''; +$websiteDisplay = str_replace(['http://', 'https://', 'www.'], ['', '', ''], $website); +if (!empty($website) && !str_starts_with($website, 'http')) { + $website = 'https://' . $website; +} +?> + +
+
+
+
+
+
+
+
+
+
+

+
+ + + + + + + + + + +
+
+
+
+
+ +
+
    + +
  • +
    + Discord: +
    +
    + +
    +
  • + + +
  • +
    + Website: +
    +
    + + + +
    +
  • + +
+
+
+
+
+

About Me

+
+ +
+
+ + fetch( + 'common/global_comments', + [ + 'comment_id' => 'profile_comment_' . $profile['username'], + ] + ) ?> +
+
+
+
diff --git a/backend/templates/talent.phtml b/backend/templates/talent.phtml new file mode 100644 index 0000000..a745dc5 --- /dev/null +++ b/backend/templates/talent.phtml @@ -0,0 +1,54 @@ +layout('layouts::site'); +?> + +
+
Our Community Talent Pool
+

Our community is filled with incredible talent and skill, Here is a collective list of every submitted + talent in WaterWolf.

+
+ + +
+
+
+
+
+
+

+

Share This Skill.

+ + +
+ + +
+ +
+
+
+
+
+
+
+ + +
+
diff --git a/backend/templates/team.phtml b/backend/templates/team.phtml new file mode 100644 index 0000000..3095fbd --- /dev/null +++ b/backend/templates/team.phtml @@ -0,0 +1,57 @@ +layout('layouts::site'); +?> + +
+

Our Team

+ +
diff --git a/backend/templates/world.phtml b/backend/templates/world.phtml new file mode 100644 index 0000000..bac50ac --- /dev/null +++ b/backend/templates/world.phtml @@ -0,0 +1,76 @@ +layout('layouts::site'); +?> + +
+ fetch('common/breadcrumbs', [ + 'urls' => [ + $router->urlFor('worlds') => 'Worlds', + ], + 'current' => $world['title'], + ]) ?> + +

+

by

+ +
+
+
+ +
+ +
+
+

+
+
+ + fetch( + 'common/global_comments', + [ + 'comment_id' => 'world_comment_' . $world['id'], + ] + ) ?> +
+ +
+
+
Join The World
+
+ +
+
+ + +
+
+
+ + + +
+ +
+ +
+
+
+ +
+
+
diff --git a/backend/templates/worlds.phtml b/backend/templates/worlds.phtml new file mode 100644 index 0000000..668d53b --- /dev/null +++ b/backend/templates/worlds.phtml @@ -0,0 +1,120 @@ +layout('layouts::site'); +?> + +
+
+
+

World Spotlight

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

+ +

+ +
Views
+ + +
+
+
+ +
+
+
+
+
+ +
+
+ + + +
+
+

+ +

+ +
Views
+
+ +
+
+ +
+
+
+
+
diff --git a/backend/templates/wwradio/index.phtml b/backend/templates/wwradio/index.phtml new file mode 100644 index 0000000..106e588 --- /dev/null +++ b/backend/templates/wwradio/index.phtml @@ -0,0 +1,41 @@ +layout('layouts::wwradio'); +?> +
+ +
+

Now Playing


+


+

+
+ + +
+ +
+ + +
+
+
+
+
+ +
Loading ...
+ + + +
+
+ + diff --git a/backend/templates/wwradio/info.phtml b/backend/templates/wwradio/info.phtml new file mode 100644 index 0000000..df08114 --- /dev/null +++ b/backend/templates/wwradio/info.phtml @@ -0,0 +1,61 @@ +layout('layouts::wwradio'); +?> + +
+

Server Details - WaterWolf Radio

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DetailsValue
Online:
Online:
AutoDJ Online:
Source:
Current Listeners:
Song:
Song Raw Meta:
Time:
Server Type:
+

Refreshing in:

+
+
+ + diff --git a/build/cron b/build/cron new file mode 100644 index 0000000..8b74434 --- /dev/null +++ b/build/cron @@ -0,0 +1 @@ +0 0 * * * * * app_cli sync diff --git a/build/dev/Caddyfile b/build/dev/Caddyfile new file mode 100644 index 0000000..b50546c --- /dev/null +++ b/build/dev/Caddyfile @@ -0,0 +1,21 @@ +{ + servers { + trusted_proxies static 172.17.0.0/16 + } +} + +:8080 { + root * /var/app/www/web + + encode gzip + php_fastcgi 127.0.0.1:9000 + file_server + + route /static/dist/* { + reverse_proxy * 127.0.0.1:5173 + } + + log { + output stdout + } +} diff --git a/build/dev/entrypoint.sh b/build/dev/entrypoint.sh new file mode 100644 index 0000000..0966358 --- /dev/null +++ b/build/dev/entrypoint.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Write environment variables to script +declare -p | grep -Ev 'BASHOPTS|BASH_VERSINFO|EUID|PPID|SHELLOPTS|UID' > /container.env +chmod 744 /container.env + +# Clear Temp +shopt -s dotglob +rm -rf /var/app/www_tmp/* + +# Composer install +cd /var/app/www + +su-exec app composer install +su-exec app npm ci + +app_cli init + +app_cli seed + +exec "$@" diff --git a/build/dev/services/vite.conf b/build/dev/services/vite.conf new file mode 100644 index 0000000..b04b284 --- /dev/null +++ b/build/dev/services/vite.conf @@ -0,0 +1,9 @@ +[program:vite] +command=npm run serve +user=app +priority=400 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=true diff --git a/build/php.ini b/build/php.ini new file mode 100644 index 0000000..3cacb47 --- /dev/null +++ b/build/php.ini @@ -0,0 +1,10 @@ +[php] +post_max_size=50M +upload_max_filesize=50M +memory_limit=256M +max_execution_time=30 +short_open_tag=0 + +[opcache] +opcache.enable=1 +opcache.enable_cli=1 diff --git a/build/phpfpmpool.conf b/build/phpfpmpool.conf new file mode 100644 index 0000000..fedb297 --- /dev/null +++ b/build/phpfpmpool.conf @@ -0,0 +1,23 @@ +[global] +error_log = /dev/stderr +daemonize = no + +[www] +user = app +group = app + +listen = 0.0.0.0:9000 + +pm = ondemand +pm.max_children = 3 +pm.start_servers = 1 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +pm.max_requests = 100 +pm.process_idle_timeout = 60s + +chdir = / + +clear_env=No +catch_workers_output = yes +decorate_workers_output = no diff --git a/build/prod/Caddyfile b/build/prod/Caddyfile new file mode 100644 index 0000000..aad618d --- /dev/null +++ b/build/prod/Caddyfile @@ -0,0 +1,13 @@ +:8080 { + root * /var/app/www/web + + encode gzip + php_fastcgi 127.0.0.1:9000 + file_server + + header /static/dist/* Cache-Control max-age=5184000 + + log { + output stdout + } +} diff --git a/build/prod/entrypoint.sh b/build/prod/entrypoint.sh new file mode 100644 index 0000000..88874a4 --- /dev/null +++ b/build/prod/entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Write environment variables to script +declare -p | grep -Ev 'BASHOPTS|BASH_VERSINFO|EUID|PPID|SHELLOPTS|UID' > /container.env +chmod 744 /container.env + +# Clear Temp +shopt -s dotglob +rm -rf /var/app/www_tmp/* + +# App startup +cd /var/app/www +app_cli init + +exec "$@" diff --git a/build/scripts/app_cli b/build/scripts/app_cli new file mode 100644 index 0000000..e07181a --- /dev/null +++ b/build/scripts/app_cli @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +if [ `whoami` != 'app' ]; then + exec su-exec app app_cli "$@" +fi + +cd /var/app/www +exec php backend/bin/console "$@" diff --git a/build/services/caddy.conf b/build/services/caddy.conf new file mode 100644 index 0000000..bff2ec9 --- /dev/null +++ b/build/services/caddy.conf @@ -0,0 +1,8 @@ +[program:caddy] +command=caddy run --config /etc/Caddyfile +priority=700 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=true diff --git a/build/services/cron.conf b/build/services/cron.conf new file mode 100644 index 0000000..f4c38a7 --- /dev/null +++ b/build/services/cron.conf @@ -0,0 +1,9 @@ +[program:cron] +command=supercronic /etc/cron.d/app +user=app +priority=900 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=true diff --git a/build/services/php_fpm.conf b/build/services/php_fpm.conf new file mode 100644 index 0000000..75bb1b7 --- /dev/null +++ b/build/services/php_fpm.conf @@ -0,0 +1,9 @@ +[program:php-fpm] +command=php-fpm -F +priority=300 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=true + diff --git a/build/supervisord.conf b/build/supervisord.conf new file mode 100644 index 0000000..9e5da55 --- /dev/null +++ b/build/supervisord.conf @@ -0,0 +1,9 @@ +[supervisord] +nodaemon=true +user=root +logfile=/dev/null +logfile_maxbytes=0 +pidfile=/run/supervisord/supervisord.pid + +[include] +files = /etc/supervisor.d/* diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..021ccf0 --- /dev/null +++ b/composer.json @@ -0,0 +1,73 @@ +{ + "name": "waterwolfdev/waterwolf-site", + "type": "project", + "description": "The new WaterWolf web site.", + "require": { + "php": "^8.2", + "ext-curl": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-pdo": "*", + "doctrine/dbal": "^4", + "google/recaptcha": "1.3", + "guzzlehttp/guzzle": "^7.8", + "intervention/image": "^3.3", + "laminas/laminas-escaper": "^2.13", + "league/plates": "^3.5", + "mezzio/mezzio-session": "^1.14", + "mezzio/mezzio-session-cache": "^1.12", + "monolog/monolog": "^3.5", + "php-di/php-di": "^7.0", + "robmorgan/phinx": "^0.16.0", + "slim/http": "^1.3", + "slim/slim": "^4.12", + "symfony/amazon-mailer": "^7.0", + "symfony/cache": "^7.0", + "symfony/console": "^7.0", + "symfony/filesystem": "^7.0", + "symfony/mailer": "^7.0" + }, + "require-dev": { + "filp/whoops": "^2.15", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/phpstan": "^1.10", + "roave/security-advisories": "dev-latest", + "slevomat/coding-standard": "^8.14", + "squizlabs/php_codesniffer": "^3.8", + "symfony/var-dumper": "^7.0" + }, + "config": { + "discard-changes": true, + "preferred-install": "dist", + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + }, + "autoload": { + "files": [ + "backend/bootstrap/functions.php" + ], + "psr-4": { + "App\\": "backend/src" + } + }, + "scripts": { + "cleanup-and-test": [ + "@phpcbf", + "@phplint", + "@phpstan", + "@phpcs" + ], + "test": [ + "@composer install", + "@phplint", + "@phpstan", + "@phpcs" + ], + "phpcbf": "phpcbf", + "phpcs": "phpcs", + "phplint": "parallel-lint . --exclude vendor", + "phpstan": "phpstan analyze --memory-limit=-1" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..0ca842d --- /dev/null +++ b/composer.lock @@ -0,0 +1,5901 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "69ff12f408a8702d5ecd09b7f0173561", + "packages": [ + { + "name": "async-aws/core", + "version": "1.21.0", + "source": { + "type": "git", + "url": "https://github.com/async-aws/core.git", + "reference": "eb23cc59515d8d529163313bbf9e02f666e515ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/async-aws/core/zipball/eb23cc59515d8d529163313bbf9e02f666e515ef", + "reference": "eb23cc59515d8d529163313bbf9e02f666e515ef", + "shasum": "" + }, + "require": { + "ext-hash": "*", + "ext-json": "*", + "ext-simplexml": "*", + "php": "^7.2.5 || ^8.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/http-client": "^4.4.16 || ^5.1.7 || ^6.0 || ^7.0", + "symfony/http-client-contracts": "^1.1.8 || ^2.0 || ^3.0", + "symfony/service-contracts": "^1.0 || ^2.0 || ^3.0" + }, + "conflict": { + "async-aws/s3": "<1.1", + "symfony/http-client": "5.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.21-dev" + } + }, + "autoload": { + "psr-4": { + "AsyncAws\\Core\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Core package to integrate with AWS. This is a lightweight AWS SDK provider by AsyncAws.", + "keywords": [ + "amazon", + "async-aws", + "aws", + "sdk", + "sts" + ], + "support": { + "source": "https://github.com/async-aws/core/tree/1.21.0" + }, + "funding": [ + { + "url": "https://github.com/jderusse", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-02-27T14:44:12+00:00" + }, + { + "name": "async-aws/ses", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/async-aws/ses.git", + "reference": "3aed25813b21aeab96a60a658edf1e23531d2011" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/async-aws/ses/zipball/3aed25813b21aeab96a60a658edf1e23531d2011", + "reference": "3aed25813b21aeab96a60a658edf1e23531d2011", + "shasum": "" + }, + "require": { + "async-aws/core": "^1.9", + "ext-json": "*", + "php": "^7.2.5 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "AsyncAws\\Ses\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "SES client, part of the AWS SDK provided by AsyncAws.", + "keywords": [ + "amazon", + "async-aws", + "aws", + "sdk", + "ses" + ], + "support": { + "source": "https://github.com/async-aws/ses/tree/1.7.0" + }, + "funding": [ + { + "url": "https://github.com/jderusse", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2023-12-13T20:52:19+00:00" + }, + { + "name": "cakephp/chronos", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/cakephp/chronos.git", + "reference": "9cb035acd10152a6b74df936986f15c4e6015bd3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/chronos/zipball/9cb035acd10152a6b74df936986f15c4e6015bd3", + "reference": "9cb035acd10152a6b74df936986f15c4e6015bd3", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "cakephp/cakephp-codesniffer": "^5.0", + "phpunit/phpunit": "^10.1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cake\\Chronos\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "http://nesbot.com" + }, + { + "name": "The CakePHP Team", + "homepage": "https://cakephp.org" + } + ], + "description": "A simple API extension for DateTime.", + "homepage": "https://cakephp.org", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "issues": "https://github.com/cakephp/chronos/issues", + "source": "https://github.com/cakephp/chronos" + }, + "time": "2023-10-17T07:41:48+00:00" + }, + { + "name": "cakephp/core", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/cakephp/core.git", + "reference": "87a2eba6c7484ba28ba0acc31a20f1b6598d6627" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/core/zipball/87a2eba6c7484ba28ba0acc31a20f1b6598d6627", + "reference": "87a2eba6c7484ba28ba0acc31a20f1b6598d6627", + "shasum": "" + }, + "require": { + "cakephp/utility": "^5.0", + "league/container": "^4.2", + "php": ">=8.1", + "psr/container": "^1.1 || ^2.0" + }, + "provide": { + "psr/container-implementation": "^2.0" + }, + "suggest": { + "cakephp/cache": "To use Configure::store() and restore().", + "cakephp/event": "To use PluginApplicationInterface or plugin applications.", + "league/container": "To use Container and ServiceProvider classes" + }, + "type": "library", + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Cake\\Core\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/core/graphs/contributors" + } + ], + "description": "CakePHP Framework Core classes", + "homepage": "https://cakephp.org", + "keywords": [ + "cakephp", + "core", + "framework" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/core" + }, + "time": "2024-01-07T19:02:23+00:00" + }, + { + "name": "cakephp/database", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/cakephp/database.git", + "reference": "f2574fce6f92f9d720e03019cdde008cdb225bfe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/database/zipball/f2574fce6f92f9d720e03019cdde008cdb225bfe", + "reference": "f2574fce6f92f9d720e03019cdde008cdb225bfe", + "shasum": "" + }, + "require": { + "cakephp/chronos": "^3.0.2", + "cakephp/core": "^5.0", + "cakephp/datasource": "^5.0", + "php": ">=8.1", + "psr/log": "^3.0" + }, + "require-dev": { + "cakephp/i18n": "^5.0", + "cakephp/log": "^5.0", + "phpstan/phpstan": "^1.10" + }, + "suggest": { + "cakephp/i18n": "If you are using locale-aware datetime formats.", + "cakephp/log": "If you want to use query logging without providing a logger yourself." + }, + "type": "library", + "autoload": { + "psr-4": { + "Cake\\Database\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/database/graphs/contributors" + } + ], + "description": "Flexible and powerful Database abstraction library with a familiar PDO-like API", + "homepage": "https://cakephp.org", + "keywords": [ + "abstraction", + "cakephp", + "database", + "database abstraction", + "pdo" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/database" + }, + "time": "2024-01-27T02:34:31+00:00" + }, + { + "name": "cakephp/datasource", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/cakephp/datasource.git", + "reference": "848bf92854b91876c57ed2c2dc70e0cc846ec309" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/datasource/zipball/848bf92854b91876c57ed2c2dc70e0cc846ec309", + "reference": "848bf92854b91876c57ed2c2dc70e0cc846ec309", + "shasum": "" + }, + "require": { + "cakephp/core": "^5.0", + "php": ">=8.1", + "psr/simple-cache": "^2.0 || ^3.0" + }, + "require-dev": { + "cakephp/cache": "^5.0", + "cakephp/collection": "^5.0", + "cakephp/utility": "^5.0", + "phpstan/phpstan": "^1.10" + }, + "suggest": { + "cakephp/cache": "If you decide to use Query caching.", + "cakephp/collection": "If you decide to use ResultSetInterface.", + "cakephp/utility": "If you decide to use EntityTrait." + }, + "type": "library", + "autoload": { + "psr-4": { + "Cake\\Datasource\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/datasource/graphs/contributors" + } + ], + "description": "Provides connection managing and traits for Entities and Queries that can be reused for different datastores", + "homepage": "https://cakephp.org", + "keywords": [ + "cakephp", + "connection management", + "datasource", + "entity", + "query" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/datasource" + }, + "time": "2024-01-23T20:49:53+00:00" + }, + { + "name": "cakephp/utility", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/cakephp/utility.git", + "reference": "f7dea1e1c618b4765ba95af8094677a1ce977fe1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/utility/zipball/f7dea1e1c618b4765ba95af8094677a1ce977fe1", + "reference": "f7dea1e1c618b4765ba95af8094677a1ce977fe1", + "shasum": "" + }, + "require": { + "cakephp/core": "^5.0", + "php": ">=8.1" + }, + "suggest": { + "ext-intl": "To use Text::transliterate() or Text::slug()", + "lib-ICU": "To use Text::transliterate() or Text::slug()" + }, + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Cake\\Utility\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/utility/graphs/contributors" + } + ], + "description": "CakePHP Utility classes such as Inflector, String, Hash, and Security", + "homepage": "https://cakephp.org", + "keywords": [ + "cakephp", + "hash", + "inflector", + "security", + "string", + "utility" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/utility" + }, + "time": "2024-01-07T19:02:23+00:00" + }, + { + "name": "dflydev/fig-cookies", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-fig-cookies.git", + "reference": "ebe6c15c9895fc490efe620ad734c8ef4a85bdb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-fig-cookies/zipball/ebe6c15c9895fc490efe620ad734c8ef4a85bdb0", + "reference": "ebe6c15c9895fc490efe620ad734c8ef4a85bdb0", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.0.1 || ^2" + }, + "require-dev": { + "doctrine/coding-standard": "^8", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12.16", + "phpunit/phpunit": "^7.2.6 || ^9", + "scrutinizer/ocular": "^1.8", + "squizlabs/php_codesniffer": "^3.3", + "vimeo/psalm": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\FigCookies\\": "src/Dflydev/FigCookies" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Beau Simensen", + "email": "beau@dflydev.com" + } + ], + "description": "Cookies for PSR-7 HTTP Message Interface.", + "keywords": [ + "cookies", + "psr-7", + "psr7" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-fig-cookies/issues", + "source": "https://github.com/dflydev/dflydev-fig-cookies/tree/v3.1.0" + }, + "time": "2023-07-18T20:41:43+00:00" + }, + { + "name": "doctrine/dbal", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "53df8c432978b716a805143eb701436d54ec705e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/53df8c432978b716a805143eb701436d54ec705e", + "reference": "53df8c432978b716a805143eb701436d54ec705e", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^0.5.3|^1", + "php": "^8.1", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "12.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.2", + "phpstan/phpstan": "1.10.57", + "phpstan/phpstan-phpunit": "1.3.15", + "phpstan/phpstan-strict-rules": "^1.5", + "phpunit/phpunit": "10.5.9", + "psalm/plugin-phpunit": "0.18.4", + "slevomat/coding-standard": "8.13.1", + "squizlabs/php_codesniffer": "3.8.1", + "symfony/cache": "^6.3.8|^7.0", + "symfony/console": "^5.4|^6.3|^7.0", + "vimeo/psalm": "5.16.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/4.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2024-02-03T19:11:19+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "phpstan/phpstan": "1.4.10 || 1.10.15", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "0.18.4", + "psr/log": "^1 || ^2 || ^3", + "vimeo/psalm": "4.30.0 || 5.12.0" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.3" + }, + "time": "2024-01-30T19:34:25+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ebaaf5be6c0286928352e054f2d5125608e5405e", + "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.2" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2023-10-06T06:47:41+00:00" + }, + { + "name": "google/recaptcha", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/google/recaptcha.git", + "reference": "d59a801e98a4e9174814a6d71bbc268dff1202df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/google/recaptcha/zipball/d59a801e98a4e9174814a6d71bbc268dff1202df", + "reference": "d59a801e98a4e9174814a6d71bbc268dff1202df", + "shasum": "" + }, + "require": { + "php": ">=8" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.14", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "ReCaptcha\\": "src/ReCaptcha" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Client library for reCAPTCHA, a free service that protects websites from spam and abuse.", + "homepage": "https://www.google.com/recaptcha/", + "keywords": [ + "Abuse", + "captcha", + "recaptcha", + "spam" + ], + "support": { + "forum": "https://groups.google.com/forum/#!forum/recaptcha", + "issues": "https://github.com/google/recaptcha/issues", + "source": "https://github.com/google/recaptcha" + }, + "time": "2023-02-18T17:41:46+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:35:24+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:19:20+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.6.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:05:35+00:00" + }, + { + "name": "intervention/gif", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/Intervention/gif.git", + "reference": "c2b07d1f69709e196c8b4ced423449a7e0f3b925" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/gif/zipball/c2b07d1f69709e196c8b4ced423449a7e0f3b925", + "reference": "c2b07d1f69709e196c8b4ced423449a7e0f3b925", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^1", + "phpunit/phpunit": "^9", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Gif\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "Native PHP GIF Encoder/Decoder", + "homepage": "https://github.com/intervention/gif", + "keywords": [ + "animation", + "gd", + "gif", + "image" + ], + "support": { + "issues": "https://github.com/Intervention/gif/issues", + "source": "https://github.com/Intervention/gif/tree/4.0.2" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + } + ], + "time": "2024-02-18T15:36:58+00:00" + }, + { + "name": "intervention/image", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/Intervention/image.git", + "reference": "fe1b0e2e64157133322974c28b44c25c2770a0c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/image/zipball/fe1b0e2e64157133322974c28b44c25c2770a0c5", + "reference": "fe1b0e2e64157133322974c28b44c25c2770a0c5", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "intervention/gif": "^4.0.1", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "phpstan/phpstan": "^1", + "phpunit/phpunit": "^9", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "suggest": { + "ext-exif": "Recommended to be able to read EXIF data properly." + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Image\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "PHP image manipulation", + "homepage": "https://image.intervention.io/", + "keywords": [ + "gd", + "image", + "imagick", + "resize", + "thumbnail", + "watermark" + ], + "support": { + "issues": "https://github.com/Intervention/image/issues", + "source": "https://github.com/Intervention/image/tree/3.4.0" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + } + ], + "time": "2024-02-14T15:11:21+00:00" + }, + { + "name": "laminas/laminas-escaper", + "version": "2.13.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-escaper.git", + "reference": "af459883f4018d0f8a0c69c7a209daef3bf973ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/af459883f4018d0f8a0c69c7a209daef3bf973ba", + "reference": "af459883f4018d0f8a0c69c7a209daef3bf973ba", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-mbstring": "*", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0" + }, + "conflict": { + "zendframework/zend-escaper": "*" + }, + "require-dev": { + "infection/infection": "^0.27.0", + "laminas/laminas-coding-standard": "~2.5.0", + "maglnet/composer-require-checker": "^3.8.0", + "phpunit/phpunit": "^9.6.7", + "psalm/plugin-phpunit": "^0.18.4", + "vimeo/psalm": "^5.9" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Escaper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs", + "homepage": "https://laminas.dev", + "keywords": [ + "escaper", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-escaper/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-escaper/issues", + "rss": "https://github.com/laminas/laminas-escaper/releases.atom", + "source": "https://github.com/laminas/laminas-escaper" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2023-10-10T08:35:13+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v1.3.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "3dbf8a8e914634c48d389c1234552666b3d43754" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/3dbf8a8e914634c48d389c1234552666b3d43754", + "reference": "3dbf8a8e914634c48d389c1234552666b3d43754", + "shasum": "" + }, + "require": { + "php": "^7.3|^8.0" + }, + "require-dev": { + "nesbot/carbon": "^2.61", + "pestphp/pest": "^1.21.3", + "phpstan/phpstan": "^1.8.2", + "symfony/var-dumper": "^5.4.11" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2023-11-08T14:08:06+00:00" + }, + { + "name": "league/container", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/container.git", + "reference": "375d13cb828649599ef5d48a339c4af7a26cd0ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/container/zipball/375d13cb828649599ef5d48a339c4af7a26cd0ab", + "reference": "375d13cb828649599ef5d48a339c4af7a26cd0ab", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "psr/container": "^1.1 || ^2.0" + }, + "provide": { + "psr/container-implementation": "^1.0" + }, + "replace": { + "orno/di": "~2.0" + }, + "require-dev": { + "nette/php-generator": "^3.4", + "nikic/php-parser": "^4.10", + "phpstan/phpstan": "^0.12.47", + "phpunit/phpunit": "^8.5.17", + "roave/security-advisories": "dev-latest", + "scrutinizer/ocular": "^1.8", + "squizlabs/php_codesniffer": "^3.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev", + "dev-4.x": "4.x-dev", + "dev-3.x": "3.x-dev", + "dev-2.x": "2.x-dev", + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Container\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Phil Bennett", + "email": "mail@philbennett.co.uk", + "role": "Developer" + } + ], + "description": "A fast and intuitive dependency injection container.", + "homepage": "https://github.com/thephpleague/container", + "keywords": [ + "container", + "dependency", + "di", + "injection", + "league", + "provider", + "service" + ], + "support": { + "issues": "https://github.com/thephpleague/container/issues", + "source": "https://github.com/thephpleague/container/tree/4.2.0" + }, + "funding": [ + { + "url": "https://github.com/philipobenito", + "type": "github" + } + ], + "time": "2021-11-16T10:29:06+00:00" + }, + { + "name": "league/plates", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/plates.git", + "reference": "a6a3238e46c6e19af7318fdc36bfbe49b0620231" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/plates/zipball/a6a3238e46c6e19af7318fdc36bfbe49b0620231", + "reference": "a6a3238e46c6e19af7318fdc36bfbe49b0620231", + "shasum": "" + }, + "require": { + "php": "^7.0|^8.0" + }, + "require-dev": { + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Plates\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Reinink", + "email": "jonathan@reinink.ca", + "role": "Developer" + }, + { + "name": "RJ Garcia", + "email": "ragboyjr@icloud.com", + "role": "Developer" + } + ], + "description": "Plates, the native PHP template system that's fast, easy to use and easy to extend.", + "homepage": "https://platesphp.com", + "keywords": [ + "league", + "package", + "templates", + "templating", + "views" + ], + "support": { + "issues": "https://github.com/thephpleague/plates/issues", + "source": "https://github.com/thephpleague/plates/tree/v3.5.0" + }, + "time": "2023-01-16T20:25:45+00:00" + }, + { + "name": "mezzio/mezzio-session", + "version": "1.14.1", + "source": { + "type": "git", + "url": "https://github.com/mezzio/mezzio-session.git", + "reference": "8e5d85644c6749149e3ef2de91a82ae0bd363b3d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mezzio/mezzio-session/zipball/8e5d85644c6749149e3ef2de91a82ae0bd363b3d", + "reference": "8e5d85644c6749149e3ef2de91a82ae0bd363b3d", + "shasum": "" + }, + "require": { + "dflydev/fig-cookies": "^3.0", + "ext-json": "*", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "psr/container": "^1.0 || ^2.0", + "psr/http-server-middleware": "^1.0" + }, + "conflict": { + "zendframework/zend-expressive-session": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~2.5.0", + "laminas/laminas-diactoros": "^3.3.0", + "phpunit/phpunit": "^10.4.2", + "psalm/plugin-phpunit": "^0.18.4", + "vimeo/psalm": "^5.15" + }, + "suggest": { + "mezzio/mezzio-csrf": "^1.0 || ^1.0-dev for CSRF protection capabilities", + "mezzio/mezzio-flash": "^1.0 || ^1.0-dev for flash message capabilities", + "mezzio/mezzio-session-ext": "^1.0 || ^1.0-dev for an ext-session persistence adapter" + }, + "type": "library", + "extra": { + "laminas": { + "config-provider": "Mezzio\\Session\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Mezzio\\Session\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Session container and middleware for PSR-7 applications", + "homepage": "https://mezzio.dev", + "keywords": [ + "laminas", + "mezzio", + "middleware", + "psr-7", + "session" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.mezzio.dev/mezzio-session/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/mezzio/mezzio-session/issues", + "rss": "https://github.com/mezzio/mezzio-session/releases.atom", + "source": "https://github.com/mezzio/mezzio-session" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2024-01-13T12:11:07+00:00" + }, + { + "name": "mezzio/mezzio-session-cache", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/mezzio/mezzio-session-cache.git", + "reference": "28509a56da8e007825e42320306383eb29b75c8e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mezzio/mezzio-session-cache/zipball/28509a56da8e007825e42320306383eb29b75c8e", + "reference": "28509a56da8e007825e42320306383eb29b75c8e", + "shasum": "" + }, + "require": { + "dflydev/fig-cookies": "^3.0.0", + "mezzio/mezzio-session": "^1.4", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "psr/container": "^1.0 || ^2.0 || ^3.0" + }, + "conflict": { + "zendframework/zend-expressive-session-cache": "*" + }, + "require-dev": { + "laminas/laminas-cache": "^3.9.1", + "laminas/laminas-cache-storage-adapter-apcu": "^2.4", + "laminas/laminas-coding-standard": "~2.5.0", + "laminas/laminas-diactoros": "^3.3", + "phpunit/phpunit": "^10.4.2", + "psalm/plugin-phpunit": "^0.18.4", + "vimeo/psalm": "^5.15" + }, + "suggest": { + "psr/cache-implementation": "This package requires a PSR-6 CacheItemPoolInterface implementation." + }, + "type": "library", + "extra": { + "laminas": { + "config-provider": "Mezzio\\Session\\Cache\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Mezzio\\Session\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "PSR-6 session persistence adapter for mezzio-session.", + "homepage": "https://mezzio.dev", + "keywords": [ + "cache", + "laminas", + "mezzio", + "psr-6", + "session" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.mezzio.dev/mezzio-session-cache/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/mezzio/mezzio-session-cache/issues", + "rss": "https://github.com/mezzio/mezzio-session-cache/releases.atom", + "source": "https://github.com/mezzio/mezzio-session-cache" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2024-01-18T15:03:07+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "c915e2634718dbc8a4a15c61b0e62e7a44e14448" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c915e2634718dbc8a4a15c61b0e62e7a44e14448", + "reference": "c915e2634718dbc8a4a15c61b0e62e7a44e14448", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-strict-rules": "^1.4", + "phpunit/phpunit": "^10.1", + "predis/predis": "^1.1 || ^2", + "ruflin/elastica": "^7", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.5.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2023-10-27T15:32:31+00:00" + }, + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "FastRoute\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "support": { + "issues": "https://github.com/nikic/FastRoute/issues", + "source": "https://github.com/nikic/FastRoute/tree/master" + }, + "time": "2018-02-13T20:26:39+00:00" + }, + { + "name": "php-di/invoker", + "version": "2.3.4", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/Invoker.git", + "reference": "33234b32dafa8eb69202f950a1fc92055ed76a86" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/33234b32dafa8eb69202f950a1fc92055ed76a86", + "reference": "33234b32dafa8eb69202f950a1fc92055ed76a86", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "psr/container": "^1.0|^2.0" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "mnapoli/hard-mode": "~0.3.0", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Invoker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Generic and extensible callable invoker", + "homepage": "https://github.com/PHP-DI/Invoker", + "keywords": [ + "callable", + "dependency", + "dependency-injection", + "injection", + "invoke", + "invoker" + ], + "support": { + "issues": "https://github.com/PHP-DI/Invoker/issues", + "source": "https://github.com/PHP-DI/Invoker/tree/2.3.4" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2023-09-08T09:24:21+00:00" + }, + { + "name": "php-di/php-di", + "version": "7.0.6", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/PHP-DI.git", + "reference": "8097948a89f6ec782839b3e958432f427cac37fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/8097948a89f6ec782839b3e958432f427cac37fd", + "reference": "8097948a89f6ec782839b3e958432f427cac37fd", + "shasum": "" + }, + "require": { + "laravel/serializable-closure": "^1.0", + "php": ">=8.0", + "php-di/invoker": "^2.0", + "psr/container": "^1.1 || ^2.0" + }, + "provide": { + "psr/container-implementation": "^1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3", + "friendsofphp/proxy-manager-lts": "^1", + "mnapoli/phpunit-easymock": "^1.3", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.6" + }, + "suggest": { + "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "DI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The dependency injection container for humans", + "homepage": "https://php-di.org/", + "keywords": [ + "PSR-11", + "container", + "container-interop", + "dependency injection", + "di", + "ioc", + "psr11" + ], + "support": { + "issues": "https://github.com/PHP-DI/PHP-DI/issues", + "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.6" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/php-di/php-di", + "type": "tidelift" + } + ], + "time": "2023-11-02T10:04:50+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "e616d01114759c4c489f93b099585439f795fe35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", + "reference": "e616d01114759c4c489f93b099585439f795fe35", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + }, + "time": "2023-04-10T20:10:41+00:00" + }, + { + "name": "psr/http-message", + "version": "1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, + "time": "2023-04-04T09:50:52+00:00" + }, + { + "name": "psr/http-server-handler", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" + }, + "time": "2023-04-10T20:06:20+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" + }, + "time": "2023-04-11T06:14:47+00:00" + }, + { + "name": "psr/log", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.0" + }, + "time": "2021-07-14T16:46:02+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "robmorgan/phinx", + "version": "0.16.0", + "source": { + "type": "git", + "url": "https://github.com/cakephp/phinx.git", + "reference": "e039a723e9fe33e406102ac1c3dc0a54c031152f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/phinx/zipball/e039a723e9fe33e406102ac1c3dc0a54c031152f", + "reference": "e039a723e9fe33e406102ac1c3dc0a54c031152f", + "shasum": "" + }, + "require": { + "cakephp/database": "^5.0.2", + "php-64bit": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/config": "^3.4|^4.0|^5.0|^6.0|^7.0", + "symfony/console": "^6.0|^7.0" + }, + "require-dev": { + "cakephp/cakephp": "^5.0.2", + "cakephp/cakephp-codesniffer": "^5.0", + "ext-json": "*", + "ext-pdo": "*", + "phpunit/phpunit": "^9.5.19", + "symfony/yaml": "^3.4|^4.0|^5.0|^6.0|^7.0" + }, + "suggest": { + "ext-json": "Install if using JSON configuration format", + "ext-pdo": "PDO extension is needed", + "symfony/yaml": "Install if using YAML configuration format" + }, + "bin": [ + "bin/phinx" + ], + "type": "library", + "autoload": { + "psr-4": { + "Phinx\\": "src/Phinx/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Morgan", + "email": "robbym@gmail.com", + "homepage": "https://robmorgan.id.au", + "role": "Lead Developer" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com", + "homepage": "https://shadowhand.me", + "role": "Developer" + }, + { + "name": "Richard Quadling", + "email": "rquadling@gmail.com", + "role": "Developer" + }, + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/phinx/graphs/contributors", + "role": "Developer" + } + ], + "description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.", + "homepage": "https://phinx.org", + "keywords": [ + "database", + "database migrations", + "db", + "migrations", + "phinx" + ], + "support": { + "issues": "https://github.com/cakephp/phinx/issues", + "source": "https://github.com/cakephp/phinx/tree/0.16.0" + }, + "time": "2024-01-24T05:06:44+00:00" + }, + { + "name": "slim/http", + "version": "1.3", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim-Http.git", + "reference": "9542edbe469b7c0f3084c2467efea972387f5beb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim-Http/zipball/9542edbe469b7c0f3084c2467efea972387f5beb", + "reference": "9542edbe469b7c0f3084c2467efea972387f5beb", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-simplexml": "*", + "php": "^7.4 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "adriansuter/php-autoload-override": "^1.3", + "laminas/laminas-diactoros": "^2.17", + "nyholm/psr7": "^1.5", + "php-http/psr7-integration-tests": "dev-master", + "phpstan/phpstan": "^1.8", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\Http\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "http://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "http://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "http://akrabat.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "http://www.lgse.com" + } + ], + "description": "Slim PSR-7 Object Decorators", + "homepage": "http://slimframework.com", + "keywords": [ + "http", + "psr-7", + "psr7" + ], + "support": { + "issues": "https://github.com/slimphp/Slim-Http/issues", + "source": "https://github.com/slimphp/Slim-Http/tree/1.3" + }, + "time": "2022-09-14T15:45:07+00:00" + }, + { + "name": "slim/slim", + "version": "4.12.0", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim.git", + "reference": "e9e99c2b24398b967841c6c4c3048622cc7e2b18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/e9e99c2b24398b967841c6c4c3048622cc7e2b18", + "reference": "e9e99c2b24398b967841c6c4c3048622cc7e2b18", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nikic/fast-route": "^1.3", + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "adriansuter/php-autoload-override": "^1.4", + "ext-simplexml": "*", + "guzzlehttp/psr7": "^2.5", + "httpsoft/http-message": "^1.1", + "httpsoft/http-server-request": "^1.1", + "laminas/laminas-diactoros": "^2.17", + "nyholm/psr7": "^1.8", + "nyholm/psr7-server": "^1.0", + "phpspec/prophecy": "^1.17", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.6", + "slim/http": "^1.3", + "slim/psr7": "^1.6", + "squizlabs/php_codesniffer": "^3.7" + }, + "suggest": { + "ext-simplexml": "Needed to support XML format in BodyParsingMiddleware", + "ext-xml": "Needed to support XML format in BodyParsingMiddleware", + "php-di/php-di": "PHP-DI is the recommended container library to be used with Slim", + "slim/psr7": "Slim PSR-7 implementation. See https://www.slimframework.com/docs/v4/start/installation.html for more information." + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\": "Slim" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "http://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "http://akrabat.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "http://www.lgse.com" + }, + { + "name": "Gabriel Manricks", + "email": "gmanricks@me.com", + "homepage": "http://gabrielmanricks.com" + } + ], + "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", + "homepage": "https://www.slimframework.com", + "keywords": [ + "api", + "framework", + "micro", + "router" + ], + "support": { + "docs": "https://www.slimframework.com/docs/v4/", + "forum": "https://discourse.slimframework.com/", + "irc": "irc://irc.freenode.net:6667/slimphp", + "issues": "https://github.com/slimphp/Slim/issues", + "rss": "https://www.slimframework.com/blog/feed.rss", + "slack": "https://slimphp.slack.com/", + "source": "https://github.com/slimphp/Slim", + "wiki": "https://github.com/slimphp/Slim/wiki" + }, + "funding": [ + { + "url": "https://opencollective.com/slimphp", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slim/slim", + "type": "tidelift" + } + ], + "time": "2023-07-23T04:54:29+00:00" + }, + { + "name": "symfony/amazon-mailer", + "version": "v7.0.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/amazon-mailer.git", + "reference": "89a68082e68535c953819c572f54f483c6280211" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/amazon-mailer/zipball/89a68082e68535c953819c572f54f483c6280211", + "reference": "89a68082e68535c953819c572f54f483c6280211", + "shasum": "" + }, + "require": { + "async-aws/ses": "^1.0", + "php": ">=8.2", + "symfony/mailer": "^6.4|^7.0" + }, + "require-dev": { + "symfony/http-client": "^6.4|^7.0" + }, + "type": "symfony-mailer-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\Bridge\\Amazon\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Amazon Mailer Bridge", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/amazon-mailer/tree/v7.0.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T15:02:46+00:00" + }, + { + "name": "symfony/cache", + "version": "v7.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "fc822951dd360a593224bb2cef90a087d0dff60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/fc822951dd360a593224bb2cef90a087d0dff60f", + "reference": "fc822951dd360a593224bb2cef90a087d0dff60f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.4|^7.0" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/dependency-injection": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/var-dumper": "<6.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/filesystem": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v7.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-22T20:27:20+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "1d74b127da04ffa87aa940abe15446fa89653778" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/1d74b127da04ffa87aa940abe15446fa89653778", + "reference": "1d74b127da04ffa87aa940abe15446fa89653778", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-09-25T12:52:38+00:00" + }, + { + "name": "symfony/config", + "version": "v7.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "44deeba7233f08f383185ffa37dace3b3bc87364" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/44deeba7233f08f383185ffa37dace3b3bc87364", + "reference": "44deeba7233f08f383185ffa37dace3b3bc87364", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^6.4|^7.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v7.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-26T07:52:39+00:00" + }, + { + "name": "symfony/console", + "version": "v7.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "6b099f3306f7c9c2d2786ed736d0026b2903205f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/6b099f3306f7c9c2d2786ed736d0026b2903205f", + "reference": "6b099f3306f7c9c2d2786ed736d0026b2903205f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^6.4|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-22T20:27:20+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.0.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "834c28d533dd0636f910909d01b9ff45cc094b5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/834c28d533dd0636f910909d01b9ff45cc094b5e", + "reference": "834c28d533dd0636f910909d01b9ff45cc094b5e", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.0.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T15:02:46+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/a76aed96a42d2b521153fb382d418e30d18b59df", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.0.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "2890e3a825bc0c0558526c04499c13f83e1b6b12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/2890e3a825bc0c0558526c04499c13f83e1b6b12", + "reference": "2890e3a825bc0c0558526c04499c13f83e1b6b12", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.0.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T15:02:46+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "8384876f49a2316a63f88a9cd12436de6936bee6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/8384876f49a2316a63f88a9cd12436de6936bee6", + "reference": "8384876f49a2316a63f88a9cd12436de6936bee6", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/http-client-contracts": "^3", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/amp": "^2.5", + "amphp/http-client": "^4.2.1", + "amphp/http-tunnel": "^1.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v7.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-15T11:33:06+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "1ee70e699b41909c209a0c930f11034b93578654" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/1ee70e699b41909c209a0c930f11034b93578654", + "reference": "1ee70e699b41909c209a0c930f11034b93578654", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-30T20:28:31+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "72e16d87bf50a3ce195b9470c06bb9d7b816ea85" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/72e16d87bf50a3ce195b9470c06bb9d7b816ea85", + "reference": "72e16d87bf50a3ce195b9470c06bb9d7b816ea85", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/twig-bridge": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-03T21:34:19+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.0.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "c1ffe24ba6fdc3e3f0f3fcb93519103b326a3716" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/c1ffe24ba6fdc3e3f0f3fcb93519103b326a3716", + "reference": "c1ffe24ba6fdc3e3f0f3fcb93519103b326a3716", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.0.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-30T08:34:29+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "a287ed7475f85bf6f61890146edbc932c0fff919" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a287ed7475f85bf6f61890146edbc932c0fff919", + "reference": "a287ed7475f85bf6f61890146edbc932c0fff919", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/861391a8da9a04cbad2d232ddd9e4893220d6e25", + "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.4.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0", + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.4.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-12-26T14:02:43+00:00" + }, + { + "name": "symfony/string", + "version": "v7.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/f5832521b998b0bec40bee688ad5de98d4cf111b", + "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-01T13:17:36+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v7.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "dfb0acb6803eb714f05d97dd4c5abe6d5fa9fe41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/dfb0acb6803eb714f05d97dd4c5abe6d5fa9fe41", + "reference": "dfb0acb6803eb714f05d97dd4c5abe6d5fa9fe41", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-26T10:35:24+00:00" + } + ], + "packages-dev": [ + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "4be43904336affa5c2f70744a348312336afd0da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", + "reference": "4be43904336affa5c2f70744a348312336afd0da", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "*", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpcompatibility/php-compatibility": "^9.0", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "franck.nijhof@dealerdirect.com", + "homepage": "http://www.frenck.nl", + "role": "Developer / IT Manager" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://www.dealerdirect.com", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "time": "2023-01-05T11:28:13+00:00" + }, + { + "name": "filp/whoops", + "version": "2.15.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/a139776fa3f5985a50b509f2a02ff0f709d2a546", + "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546", + "shasum": "" + }, + "require": { + "php": "^5.5.9 || ^7.0 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^0.9 || ^1.0", + "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.15.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2023-11-03T12:00:00+00:00" + }, + { + "name": "php-parallel-lint/php-parallel-lint", + "version": "v1.3.2", + "source": { + "type": "git", + "url": "https://github.com/php-parallel-lint/PHP-Parallel-Lint.git", + "reference": "6483c9832e71973ed29cf71bd6b3f4fde438a9de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-parallel-lint/PHP-Parallel-Lint/zipball/6483c9832e71973ed29cf71bd6b3f4fde438a9de", + "reference": "6483c9832e71973ed29cf71bd6b3f4fde438a9de", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.3.0" + }, + "replace": { + "grogy/php-parallel-lint": "*", + "jakub-onderka/php-parallel-lint": "*" + }, + "require-dev": { + "nette/tester": "^1.3 || ^2.0", + "php-parallel-lint/php-console-highlighter": "0.* || ^1.0", + "squizlabs/php_codesniffer": "^3.6" + }, + "suggest": { + "php-parallel-lint/php-console-highlighter": "Highlight syntax in code snippet" + }, + "bin": [ + "parallel-lint" + ], + "type": "library", + "autoload": { + "classmap": [ + "./src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Jakub Onderka", + "email": "ahoj@jakubonderka.cz" + } + ], + "description": "This tool check syntax of PHP files about 20x faster than serial check.", + "homepage": "https://github.com/php-parallel-lint/PHP-Parallel-Lint", + "support": { + "issues": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/issues", + "source": "https://github.com/php-parallel-lint/PHP-Parallel-Lint/tree/v1.3.2" + }, + "time": "2022-02-21T12:50:22+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.26.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "231e3186624c03d7e7c890ec662b81e6b0405227" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/231e3186624c03d7e7c890ec662b81e6b0405227", + "reference": "231e3186624c03d7e7c890ec662b81e6b0405227", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.26.0" + }, + "time": "2024-02-23T16:05:55+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.10.59", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "e607609388d3a6d418a50a49f7940e8086798281" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e607609388d3a6d418a50a49f7940e8086798281", + "reference": "e607609388d3a6d418a50a49f7940e8086798281", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2024-02-20T13:59:13+00:00" + }, + { + "name": "roave/security-advisories", + "version": "dev-latest", + "source": { + "type": "git", + "url": "https://github.com/Roave/SecurityAdvisories.git", + "reference": "cca68bc8c5e35d0bc657c6c47bd40d28184abd87" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/cca68bc8c5e35d0bc657c6c47bd40d28184abd87", + "reference": "cca68bc8c5e35d0bc657c6c47bd40d28184abd87", + "shasum": "" + }, + "conflict": { + "3f/pygmentize": "<1.2", + "admidio/admidio": "<4.2.13", + "adodb/adodb-php": "<=5.20.20|>=5.21,<=5.21.3", + "aheinze/cockpit": "<2.2", + "aimeos/aimeos-typo3": "<19.10.12|>=20,<20.10.5", + "airesvsg/acf-to-rest-api": "<=3.1", + "akaunting/akaunting": "<2.1.13", + "akeneo/pim-community-dev": "<5.0.119|>=6,<6.0.53", + "alextselegidis/easyappointments": "<1.5", + "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1", + "amazing/media2click": ">=1,<1.3.3", + "amphp/artax": "<1.0.6|>=2,<2.0.6", + "amphp/http": "<1.0.1", + "amphp/http-client": ">=4,<4.4", + "anchorcms/anchor-cms": "<=0.12.7", + "andreapollastri/cipi": "<=3.1.15", + "andrewhaine/silverstripe-form-capture": ">=0.2,<=0.2.3|>=1,<1.0.2|>=2,<2.2.5", + "apache-solr-for-typo3/solr": "<2.8.3", + "apereo/phpcas": "<1.6", + "api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6|>=2.6,<2.7.10|>=3,<3.0.12|>=3.1,<3.1.3", + "appwrite/server-ce": "<=1.2.1", + "arc/web": "<3", + "area17/twill": "<1.2.5|>=2,<2.5.3", + "artesaos/seotools": "<0.17.2", + "asymmetricrypt/asymmetricrypt": "<9.9.99", + "athlon1600/php-proxy": "<=5.1", + "athlon1600/php-proxy-app": "<=3", + "austintoddj/canvas": "<=3.4.2", + "automad/automad": "<=1.10.9", + "awesome-support/awesome-support": "<=6.0.7", + "aws/aws-sdk-php": "<3.288.1", + "azuracast/azuracast": "<0.18.3", + "backdrop/backdrop": "<1.24.2", + "backpack/crud": "<3.4.9", + "bacula-web/bacula-web": "<8.0.0.0-RC2-dev", + "badaso/core": "<2.7", + "bagisto/bagisto": "<1.3.2", + "barrelstrength/sprout-base-email": "<1.2.7", + "barrelstrength/sprout-forms": "<3.9", + "barryvdh/laravel-translation-manager": "<0.6.2", + "barzahlen/barzahlen-php": "<2.0.1", + "baserproject/basercms": "<5.0.9", + "bassjobsen/bootstrap-3-typeahead": ">4.0.2", + "bigfork/silverstripe-form-capture": ">=3,<3.1.1", + "billz/raspap-webgui": "<2.9.5", + "bk2k/bootstrap-package": ">=7.1,<7.1.2|>=8,<8.0.8|>=9,<9.0.4|>=9.1,<9.1.3|>=10,<10.0.10|>=11,<11.0.3", + "bmarshall511/wordpress_zero_spam": "<5.2.13", + "bolt/bolt": "<3.7.2", + "bolt/core": "<=4.2", + "bottelet/flarepoint": "<2.2.1", + "bref/bref": "<2.1.13", + "brightlocal/phpwhois": "<=4.2.5", + "brotkrueml/codehighlight": "<2.7", + "brotkrueml/schema": "<1.13.1|>=2,<2.5.1", + "brotkrueml/typo3-matomo-integration": "<1.3.2", + "buddypress/buddypress": "<7.2.1", + "bugsnag/bugsnag-laravel": ">=2,<2.0.2", + "bytefury/crater": "<6.0.2", + "cachethq/cachet": "<2.5.1", + "cakephp/cakephp": "<3.10.3|>=4,<4.0.10|>=4.1,<4.1.4|>=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10", + "cakephp/database": ">=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10", + "cardgate/magento2": "<2.0.33", + "cardgate/woocommerce": "<=3.1.15", + "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4", + "cartalyst/sentry": "<=2.1.6", + "catfan/medoo": "<1.7.5", + "cecil/cecil": "<7.47.1", + "centreon/centreon": "<22.10.0.0-beta1", + "cesnet/simplesamlphp-module-proxystatistics": "<3.1", + "chriskacerguis/codeigniter-restserver": "<=2.7.1", + "civicrm/civicrm-core": ">=4.2,<4.2.9|>=4.3,<4.3.3", + "ckeditor/ckeditor": "<4.24", + "cockpit-hq/cockpit": "<=2.6.3", + "codeception/codeception": "<3.1.3|>=4,<4.1.22", + "codeigniter/framework": "<3.1.9", + "codeigniter4/framework": "<=4.4.2", + "codeigniter4/shield": "<1.0.0.0-beta8", + "codiad/codiad": "<=2.8.4", + "composer/composer": "<1.10.27|>=2,<2.2.23|>=2.3,<2.7", + "concrete5/concrete5": "<9.2.5", + "concrete5/core": "<8.5.8|>=9,<9.1", + "contao-components/mediaelement": ">=2.14.2,<2.21.1", + "contao/contao": ">=4,<4.4.56|>=4.5,<4.9.40|>=4.10,<4.11.7|>=4.13,<4.13.21|>=5.1,<5.1.4", + "contao/core": ">=2,<3.5.39", + "contao/core-bundle": ">=3,<3.5.35|>=4,<4.9.42|>=4.10,<4.13.28|>=5,<5.1.10", + "contao/listing-bundle": ">=4,<4.4.8", + "contao/managed-edition": "<=1.5", + "corveda/phpsandbox": "<1.3.5", + "cosenary/instagram": "<=2.3", + "craftcms/cms": "<4.6.2", + "croogo/croogo": "<4", + "cuyz/valinor": "<0.12", + "czproject/git-php": "<4.0.3", + "darylldoyle/safe-svg": "<1.9.10", + "datadog/dd-trace": ">=0.30,<0.30.2", + "datatables/datatables": "<1.10.10", + "david-garcia/phpwhois": "<=4.3.1", + "dbrisinajumi/d2files": "<1", + "dcat/laravel-admin": "<=2.1.3.0-beta", + "derhansen/fe_change_pwd": "<2.0.5|>=3,<3.0.3", + "derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1|>=7,<7.4", + "desperado/xml-bundle": "<=0.1.7", + "directmailteam/direct-mail": "<6.0.3|>=7,<7.0.3|>=8,<9.5.2", + "doctrine/annotations": "<1.2.7", + "doctrine/cache": ">=1,<1.3.2|>=1.4,<1.4.2", + "doctrine/common": "<2.4.3|>=2.5,<2.5.1", + "doctrine/dbal": ">=2,<2.0.8|>=2.1,<2.1.2|>=3,<3.1.4", + "doctrine/doctrine-bundle": "<1.5.2", + "doctrine/doctrine-module": "<=0.7.1", + "doctrine/mongodb-odm": "<1.0.2", + "doctrine/mongodb-odm-bundle": "<3.0.1", + "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4", + "dolibarr/dolibarr": "<18.0.2", + "dompdf/dompdf": "<2.0.4", + "doublethreedigital/guest-entries": "<3.1.2", + "drupal/core": ">=6,<6.38|>=7,<7.96|>=8,<10.1.8|>=10.2,<10.2.2", + "drupal/drupal": ">=5,<5.11|>=6,<6.38|>=7,<7.80|>=8,<8.9.16|>=9,<9.1.12|>=9.2,<9.2.4", + "duncanmcclean/guest-entries": "<3.1.2", + "dweeves/magmi": "<=0.7.24", + "ec-cube/ec-cube": "<2.4.4", + "ecodev/newsletter": "<=4", + "ectouch/ectouch": "<=2.7.2", + "elefant/cms": "<2.0.7", + "elgg/elgg": "<3.3.24|>=4,<4.0.5", + "elijaa/phpmemcacheadmin": "<=1.3", + "encore/laravel-admin": "<=1.8.19", + "endroid/qr-code-bundle": "<3.4.2", + "enhavo/enhavo-app": "<=0.13.1", + "enshrined/svg-sanitize": "<0.15", + "erusev/parsedown": "<1.7.2", + "ether/logs": "<3.0.4", + "evolutioncms/evolution": "<=3.2.3", + "exceedone/exment": "<4.4.3|>=5,<5.0.3", + "exceedone/laravel-admin": "<2.2.3|==3", + "ezsystems/demobundle": ">=5.4,<5.4.6.1-dev", + "ezsystems/ez-support-tools": ">=2.2,<2.2.3", + "ezsystems/ezdemo-ls-extension": ">=5.4,<5.4.2.1-dev", + "ezsystems/ezfind-ls": ">=5.3,<5.3.6.1-dev|>=5.4,<5.4.11.1-dev|>=2017.12,<2017.12.0.1-dev", + "ezsystems/ezplatform": "<=1.13.6|>=2,<=2.5.24", + "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6|>=1.5,<1.5.29|>=2.3,<2.3.26", + "ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1", + "ezsystems/ezplatform-graphql": ">=1.0.0.0-RC1-dev,<1.0.13|>=2.0.0.0-beta1,<2.3.12", + "ezsystems/ezplatform-kernel": "<1.2.5.1-dev|>=1.3,<1.3.34", + "ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<1.3.8", + "ezsystems/ezplatform-richtext": ">=2.3,<2.3.7.1-dev", + "ezsystems/ezplatform-solr-search-engine": ">=1.7,<1.7.12|>=2,<2.0.2|>=3.3,<3.3.15", + "ezsystems/ezplatform-user": ">=1,<1.0.1", + "ezsystems/ezpublish-kernel": "<6.13.8.2-dev|>=7,<7.5.31", + "ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.6,<=2019.03.5.1", + "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3", + "ezsystems/repository-forms": ">=2.3,<2.3.2.1-dev|>=2.5,<2.5.15", + "ezyang/htmlpurifier": "<4.1.1", + "facade/ignition": "<1.16.15|>=2,<2.4.2|>=2.5,<2.5.2", + "facturascripts/facturascripts": "<=2022.08", + "feehi/cms": "<=2.1.1", + "feehi/feehicms": "<=2.1.1", + "fenom/fenom": "<=2.12.1", + "filegator/filegator": "<7.8", + "firebase/php-jwt": "<6", + "fixpunkt/fp-masterquiz": "<2.2.1|>=3,<3.5.2", + "fixpunkt/fp-newsletter": "<1.1.1|>=2,<2.1.2|>=2.2,<3.2.6", + "flarum/core": "<1.8.5", + "flarum/framework": "<1.8.5", + "flarum/mentions": "<1.6.3", + "flarum/sticky": ">=0.1.0.0-beta14,<=0.1.0.0-beta15", + "flarum/tags": "<=0.1.0.0-beta13", + "floriangaerber/magnesium": "<0.3.1", + "fluidtypo3/vhs": "<5.1.1", + "fof/byobu": ">=0.3.0.0-beta2,<1.1.7", + "fof/upload": "<1.2.3", + "foodcoopshop/foodcoopshop": ">=3.2,<3.6.1", + "fooman/tcpdf": "<6.2.22", + "forkcms/forkcms": "<5.11.1", + "fossar/tcpdf-parser": "<6.2.22", + "francoisjacquet/rosariosis": "<11", + "frappant/frp-form-answers": "<3.1.2|>=4,<4.0.2", + "friendsofsymfony/oauth2-php": "<1.3", + "friendsofsymfony/rest-bundle": ">=1.2,<1.2.2", + "friendsofsymfony/user-bundle": ">=1.2,<1.3.5", + "friendsoftypo3/mediace": ">=7.6.2,<7.6.5", + "friendsoftypo3/openid": ">=4.5,<4.5.31|>=4.7,<4.7.16|>=6,<6.0.11|>=6.1,<6.1.6", + "froala/wysiwyg-editor": "<3.2.7|>=4.0.1,<=4.1.1", + "froxlor/froxlor": "<=2.1.1", + "fuel/core": "<1.8.1", + "funadmin/funadmin": "<=3.2|>=3.3.2,<=3.3.3", + "gaoming13/wechat-php-sdk": "<=1.10.2", + "genix/cms": "<=1.1.11", + "getgrav/grav": "<1.7.44", + "getkirby/cms": "<4.1.1", + "getkirby/kirby": "<=2.5.12", + "getkirby/panel": "<2.5.14", + "getkirby/starterkit": "<=3.7.0.2", + "gilacms/gila": "<=1.15.4", + "gleez/cms": "<=1.2|==2", + "globalpayments/php-sdk": "<2", + "gogentooss/samlbase": "<1.2.7", + "google/protobuf": "<3.15", + "gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3", + "gree/jose": "<2.2.1", + "gregwar/rst": "<1.0.3", + "grumpydictator/firefly-iii": "<6.1.7", + "gugoan/economizzer": "<=0.9.0.0-beta1", + "guzzlehttp/guzzle": "<6.5.8|>=7,<7.4.5", + "guzzlehttp/psr7": "<1.9.1|>=2,<2.4.5", + "haffner/jh_captcha": "<=2.1.3|>=3,<=3.0.2", + "harvesthq/chosen": "<1.8.7", + "helloxz/imgurl": "<=2.31", + "hhxsv5/laravel-s": "<3.7.36", + "hillelcoren/invoice-ninja": "<5.3.35", + "himiklab/yii2-jqgrid-widget": "<1.0.8", + "hjue/justwriting": "<=1", + "hov/jobfair": "<1.0.13|>=2,<2.0.2", + "httpsoft/http-message": "<1.0.12", + "hyn/multi-tenant": ">=5.6,<5.7.2", + "ibexa/admin-ui": ">=4.2,<4.2.3", + "ibexa/core": ">=4,<4.0.7|>=4.1,<4.1.4|>=4.2,<4.2.3|>=4.5,<4.5.4", + "ibexa/graphql": ">=2.5,<2.5.31|>=3.3,<3.3.28|>=4.2,<4.2.3", + "ibexa/post-install": "<=1.0.4", + "ibexa/solr": ">=4.5,<4.5.4", + "ibexa/user": ">=4,<4.4.3", + "icecoder/icecoder": "<=8.1", + "idno/known": "<=1.3.1", + "illuminate/auth": "<5.5.10", + "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<=4.1.99999|>=4.2,<=4.2.99999|>=5,<=5.0.99999|>=5.1,<=5.1.99999|>=5.2,<=5.2.99999|>=5.3,<=5.3.99999|>=5.4,<=5.4.99999|>=5.5,<=5.5.49|>=5.6,<=5.6.99999|>=5.7,<=5.7.99999|>=5.8,<=5.8.99999|>=6,<6.18.31|>=7,<7.22.4", + "illuminate/database": "<6.20.26|>=7,<7.30.5|>=8,<8.40", + "illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15", + "illuminate/view": "<6.20.42|>=7,<7.30.6|>=8,<8.75", + "impresscms/impresscms": "<=1.4.5", + "impresspages/impresspages": "<=1.0.12", + "in2code/femanager": "<5.5.3|>=6,<6.3.4|>=7,<7.2.3", + "in2code/ipandlanguageredirect": "<5.1.2", + "in2code/lux": "<17.6.1|>=18,<24.0.2", + "innologi/typo3-appointments": "<2.0.6", + "intelliants/subrion": "<4.2.2", + "islandora/islandora": ">=2,<2.4.1", + "ivankristianto/phpwhois": "<=4.3", + "jackalope/jackalope-doctrine-dbal": "<1.7.4", + "james-heinrich/getid3": "<1.9.21", + "james-heinrich/phpthumb": "<1.7.12", + "jasig/phpcas": "<1.3.3", + "jcbrand/converse.js": "<3.3.3", + "joomla/application": "<1.0.13", + "joomla/archive": "<1.1.12|>=2,<2.0.1", + "joomla/filesystem": "<1.6.2|>=2,<2.0.1", + "joomla/filter": "<1.4.4|>=2,<2.0.1", + "joomla/framework": "<1.5.7|>=2.5.4,<=3.8.12", + "joomla/input": ">=2,<2.0.2", + "joomla/joomla-cms": ">=2.5,<3.9.12", + "joomla/session": "<1.3.1", + "joyqi/hyper-down": "<=2.4.27", + "jsdecena/laracom": "<2.0.9", + "jsmitty12/phpwhois": "<5.1", + "juzaweb/cms": "<=3.4", + "kazist/phpwhois": "<=4.2.6", + "kelvinmo/simplexrd": "<3.1.1", + "kevinpapst/kimai2": "<1.16.7", + "khodakhah/nodcms": "<=3", + "kimai/kimai": "<2.1", + "kitodo/presentation": "<3.2.3|>=3.3,<3.3.4", + "klaviyo/magento2-extension": ">=1,<3", + "knplabs/knp-snappy": "<=1.4.2", + "kohana/core": "<3.3.3", + "krayin/laravel-crm": "<1.2.2", + "kreait/firebase-php": ">=3.2,<3.8.1", + "la-haute-societe/tcpdf": "<6.2.22", + "laminas/laminas-diactoros": "<2.18.1|==2.19|==2.20|==2.21|==2.22|==2.23|>=2.24,<2.24.2|>=2.25,<2.25.2", + "laminas/laminas-form": "<2.17.1|>=3,<3.0.2|>=3.1,<3.1.1", + "laminas/laminas-http": "<2.14.2", + "laravel/fortify": "<1.11.1", + "laravel/framework": "<6.20.44|>=7,<7.30.6|>=8,<8.75", + "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10", + "latte/latte": "<2.10.8", + "lavalite/cms": "<=9", + "lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5", + "league/commonmark": "<0.18.3", + "league/flysystem": "<1.1.4|>=2,<2.1.1", + "league/oauth2-server": ">=8.3.2,<8.4.2|>=8.5,<8.5.3", + "lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3", + "librenms/librenms": "<2017.08.18", + "liftkit/database": "<2.13.2", + "limesurvey/limesurvey": "<3.27.19", + "livehelperchat/livehelperchat": "<=3.91", + "livewire/livewire": ">2.2.4,<2.2.6", + "lms/routes": "<2.1.1", + "localizationteam/l10nmgr": "<7.4|>=8,<8.7|>=9,<9.2", + "luyadev/yii-helpers": "<1.2.1", + "magento/community-edition": "<2.4.3.0-patch3|>=2.4.4,<2.4.5", + "magento/core": "<=1.9.4.5", + "magento/magento1ce": "<1.9.4.3-dev", + "magento/magento1ee": ">=1,<1.14.4.3-dev", + "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2.0-patch2", + "magneto/core": "<1.9.4.4-dev", + "maikuolan/phpmussel": ">=1,<1.6", + "mainwp/mainwp": "<=4.4.3.3", + "mantisbt/mantisbt": "<2.26.1", + "marcwillmann/turn": "<0.3.3", + "matyhtf/framework": "<3.0.6", + "mautic/core": "<4.3", + "mediawiki/core": "<1.36.2", + "mediawiki/matomo": "<2.4.3", + "mediawiki/semantic-media-wiki": "<4.0.2", + "melisplatform/melis-asset-manager": "<5.0.1", + "melisplatform/melis-cms": "<5.0.1", + "melisplatform/melis-front": "<5.0.1", + "mezzio/mezzio-swoole": "<3.7|>=4,<4.3", + "mgallegos/laravel-jqgrid": "<=1.3", + "microsoft/microsoft-graph": ">=1.16,<1.109.1|>=2,<2.0.1", + "microsoft/microsoft-graph-beta": "<2.0.1", + "microsoft/microsoft-graph-core": "<2.0.2", + "microweber/microweber": "<=2.0.4", + "miniorange/miniorange-saml": "<1.4.3", + "mittwald/typo3_forum": "<1.2.1", + "mobiledetect/mobiledetectlib": "<2.8.32", + "modx/revolution": "<=2.8.3.0-patch", + "mojo42/jirafeau": "<4.4", + "mongodb/mongodb": ">=1,<1.9.2", + "monolog/monolog": ">=1.8,<1.12", + "moodle/moodle": "<4.3.3", + "mos/cimage": "<0.7.19", + "movim/moxl": ">=0.8,<=0.10", + "mpdf/mpdf": "<=7.1.7", + "munkireport/comment": "<4.1", + "munkireport/managedinstalls": "<2.6", + "munkireport/munkireport": ">=2.5.3,<5.6.3", + "mustache/mustache": ">=2,<2.14.1", + "namshi/jose": "<2.2", + "neoan3-apps/template": "<1.1.1", + "neorazorx/facturascripts": "<2022.04", + "neos/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6", + "neos/form": ">=1.2,<4.3.3|>=5,<5.0.9|>=5.1,<5.1.3", + "neos/media-browser": "<7.3.19|>=8,<8.0.16|>=8.1,<8.1.11|>=8.2,<8.2.11|>=8.3,<8.3.9", + "neos/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.9.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<5.3.10|>=7,<7.0.9|>=7.1,<7.1.7|>=7.2,<7.2.6|>=7.3,<7.3.4|>=8,<8.0.2", + "neos/swiftmailer": ">=4.1,<4.1.99|>=5.4,<5.4.5", + "netgen/tagsbundle": ">=3.4,<3.4.11|>=4,<4.0.15", + "nette/application": ">=2,<2.0.19|>=2.1,<2.1.13|>=2.2,<2.2.10|>=2.3,<2.3.14|>=2.4,<2.4.16|>=3,<3.0.6", + "nette/nette": ">=2,<2.0.19|>=2.1,<2.1.13", + "nilsteampassnet/teampass": "<3.0.10", + "nonfiction/nterchange": "<4.1.1", + "notrinos/notrinos-erp": "<=0.7", + "noumo/easyii": "<=0.9", + "nukeviet/nukeviet": "<4.5.02", + "nyholm/psr7": "<1.6.1", + "nystudio107/craft-seomatic": "<3.4.12", + "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1", + "october/backend": "<1.1.2", + "october/cms": "<1.0.469|==1.0.469|==1.0.471|==1.1.1", + "october/october": "<=3.4.4", + "october/rain": "<1.0.472|>=1.1,<1.1.2", + "october/system": "<1.0.476|>=1.1,<1.1.12|>=2,<2.2.34|>=3,<3.5.2", + "omeka/omeka-s": "<4.0.3", + "onelogin/php-saml": "<2.10.4", + "oneup/uploader-bundle": ">=1,<1.9.3|>=2,<2.1.5", + "open-web-analytics/open-web-analytics": "<1.7.4", + "opencart/opencart": "<=3.0.3.7|>=4,<4.0.2.3-dev", + "openid/php-openid": "<2.3", + "openmage/magento-lts": "<20.5", + "opensource-workshop/connect-cms": "<1.7.2|>=2,<2.3.2", + "orchid/platform": ">=9,<9.4.4|>=14.0.0.0-alpha4,<14.5", + "oro/calendar-bundle": ">=4.2,<=4.2.6|>=5,<=5.0.6|>=5.1,<5.1.1", + "oro/commerce": ">=4.1,<5.0.11|>=5.1,<5.1.1", + "oro/crm": ">=1.7,<1.7.4|>=3.1,<4.1.17|>=4.2,<4.2.7", + "oro/crm-call-bundle": ">=4.2,<=4.2.5|>=5,<5.0.4|>=5.1,<5.1.1", + "oro/customer-portal": ">=4.2,<=4.2.8|>=5,<5.0.11|>=5.1,<5.1.1", + "oro/platform": ">=1.7,<1.7.4|>=3.1,<3.1.29|>=4.1,<4.1.17|>=4.2,<=4.2.10|>=5,<5.0.8", + "oxid-esales/oxideshop-ce": "<4.5", + "packbackbooks/lti-1-3-php-library": "<5", + "padraic/humbug_get_contents": "<1.1.2", + "pagarme/pagarme-php": "<3", + "pagekit/pagekit": "<=1.0.18", + "paragonie/random_compat": "<2", + "passbolt/passbolt_api": "<2.11", + "paypal/merchant-sdk-php": "<3.12", + "pear/archive_tar": "<1.4.14", + "pear/auth": "<1.2.4", + "pear/crypt_gpg": "<1.6.7", + "pear/pear": "<=1.10.1", + "pegasus/google-for-jobs": "<1.5.1|>=2,<2.1.1", + "personnummer/personnummer": "<3.0.2", + "phanan/koel": "<5.1.4", + "phenx/php-svg-lib": "<0.5.2", + "php-mod/curl": "<2.3.2", + "phpbb/phpbb": "<3.2.10|>=3.3,<3.3.1", + "phpems/phpems": ">=6,<=6.1.3", + "phpfastcache/phpfastcache": "<6.1.5|>=7,<7.1.2|>=8,<8.0.7", + "phpmailer/phpmailer": "<6.5", + "phpmussel/phpmussel": ">=1,<1.6", + "phpmyadmin/phpmyadmin": "<5.2.1", + "phpmyfaq/phpmyfaq": "<3.2.5", + "phpoffice/phpexcel": "<1.8", + "phpoffice/phpspreadsheet": "<1.16", + "phpseclib/phpseclib": "<2.0.31|>=3,<3.0.34", + "phpservermon/phpservermon": "<3.6", + "phpsysinfo/phpsysinfo": "<3.4.3", + "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3", + "phpwhois/phpwhois": "<=4.2.5", + "phpxmlrpc/extras": "<0.6.1", + "phpxmlrpc/phpxmlrpc": "<4.9.2", + "pi/pi": "<=2.5", + "pimcore/admin-ui-classic-bundle": "<1.3.4", + "pimcore/customer-management-framework-bundle": "<4.0.6", + "pimcore/data-hub": "<1.2.4", + "pimcore/demo": "<10.3", + "pimcore/ecommerce-framework-bundle": "<1.0.10", + "pimcore/perspective-editor": "<1.5.1", + "pimcore/pimcore": "<11.1.1", + "pixelfed/pixelfed": "<0.11.11", + "plotly/plotly.js": "<2.25.2", + "pocketmine/bedrock-protocol": "<8.0.2", + "pocketmine/pocketmine-mp": "<=4.23|>=5,<5.3.1", + "pocketmine/raklib": ">=0.14,<0.14.6|>=0.15,<0.15.1", + "pressbooks/pressbooks": "<5.18", + "prestashop/autoupgrade": ">=4,<4.10.1", + "prestashop/blockreassurance": "<=5.1.3", + "prestashop/blockwishlist": ">=2,<2.1.1", + "prestashop/contactform": ">=1.0.1,<4.3", + "prestashop/gamification": "<2.3.2", + "prestashop/prestashop": "<8.1.4", + "prestashop/productcomments": "<5.0.2", + "prestashop/ps_emailsubscription": "<2.6.1", + "prestashop/ps_facetedsearch": "<3.4.1", + "prestashop/ps_linklist": "<3.1", + "privatebin/privatebin": "<1.4", + "processwire/processwire": "<=3.0.210", + "propel/propel": ">=2.0.0.0-alpha1,<=2.0.0.0-alpha7", + "propel/propel1": ">=1,<=1.7.1", + "pterodactyl/panel": "<1.7", + "ptheofan/yii2-statemachine": ">=2.0.0.0-RC1-dev,<=2", + "ptrofimov/beanstalk_console": "<1.7.14", + "pubnub/pubnub": "<6.1", + "pusher/pusher-php-server": "<2.2.1", + "pwweb/laravel-core": "<=0.3.6.0-beta", + "pyrocms/pyrocms": "<=3.9.1", + "rainlab/blog-plugin": "<1.4.1", + "rainlab/debugbar-plugin": "<3.1", + "rainlab/user-plugin": "<=1.4.5", + "rankmath/seo-by-rank-math": "<=1.0.95", + "rap2hpoutre/laravel-log-viewer": "<0.13", + "react/http": ">=0.7,<1.9", + "really-simple-plugins/complianz-gdpr": "<6.4.2", + "redaxo/source": "<=5.15.1", + "remdex/livehelperchat": "<3.99", + "reportico-web/reportico": "<=7.1.21", + "rhukster/dom-sanitizer": "<1.0.7", + "rmccue/requests": ">=1.6,<1.8", + "robrichards/xmlseclibs": ">=1,<3.0.4", + "roots/soil": "<4.1", + "rudloff/alltube": "<3.0.3", + "s-cart/core": "<6.9", + "s-cart/s-cart": "<6.9", + "sabberworm/php-css-parser": ">=1,<1.0.1|>=2,<2.0.1|>=3,<3.0.1|>=4,<4.0.1|>=5,<5.0.9|>=5.1,<5.1.3|>=5.2,<5.2.1|>=6,<6.0.2|>=7,<7.0.4|>=8,<8.0.1|>=8.1,<8.1.1|>=8.2,<8.2.1|>=8.3,<8.3.1", + "sabre/dav": ">=1.6,<1.7.11|>=1.8,<1.8.9", + "scheb/two-factor-bundle": "<3.26|>=4,<4.11", + "sensiolabs/connect": "<4.2.3", + "serluck/phpwhois": "<=4.2.6", + "sfroemken/url_redirect": "<=1.2.1", + "sheng/yiicms": "<=1.2", + "shopware/core": "<=6.5.7.3", + "shopware/platform": "<=6.5.7.3", + "shopware/production": "<=6.3.5.2", + "shopware/shopware": "<=5.7.17", + "shopware/storefront": "<=6.4.8.1", + "shopxo/shopxo": "<2.2.6", + "showdoc/showdoc": "<2.10.4", + "silverstripe-australia/advancedreports": ">=1,<=2", + "silverstripe/admin": "<1.13.19|>=2,<2.1.8", + "silverstripe/assets": ">=1,<1.11.1", + "silverstripe/cms": "<4.11.3", + "silverstripe/comments": ">=1.3,<1.9.99|>=2,<2.9.99|>=3,<3.1.1", + "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3", + "silverstripe/framework": "<4.13.39|>=5,<5.1.11", + "silverstripe/graphql": ">=2,<2.0.5|>=3,<3.8.2|>=4,<4.3.7|>=5,<5.1.3", + "silverstripe/hybridsessions": ">=1,<2.4.1|>=2.5,<2.5.1", + "silverstripe/recipe-cms": ">=4.5,<4.5.3", + "silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1", + "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4", + "silverstripe/silverstripe-omnipay": "<2.5.2|>=3,<3.0.2|>=3.1,<3.1.4|>=3.2,<3.2.1", + "silverstripe/subsites": ">=2,<2.6.1", + "silverstripe/taxonomy": ">=1.3,<1.3.1|>=2,<2.0.1", + "silverstripe/userforms": "<3", + "silverstripe/versioned-admin": ">=1,<1.11.1", + "simple-updates/phpwhois": "<=1", + "simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4|==5.0.0.0-alpha12", + "simplesamlphp/simplesamlphp": "<1.18.6", + "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1", + "simplesamlphp/simplesamlphp-module-openid": "<1", + "simplesamlphp/simplesamlphp-module-openidprovider": "<0.9", + "simplesamlphp/xml-security": "==1.6.11", + "simplito/elliptic-php": "<1.0.6", + "sitegeist/fluid-components": "<3.5", + "sjbr/sr-freecap": "<2.4.6|>=2.5,<2.5.3", + "slim/psr7": "<1.4.1|>=1.5,<1.5.1|>=1.6,<1.6.1", + "slim/slim": "<2.6", + "slub/slub-events": "<3.0.3", + "smarty/smarty": "<3.1.48|>=4,<4.3.1", + "snipe/snipe-it": "<=6.2.2", + "socalnick/scn-social-auth": "<1.15.2", + "socialiteproviders/steam": "<1.1", + "spatie/browsershot": "<3.57.4", + "spipu/html2pdf": "<5.2.8", + "spoon/library": "<1.4.1", + "spoonity/tcpdf": "<6.2.22", + "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1", + "ssddanbrown/bookstack": "<22.02.3", + "statamic/cms": "<4.46", + "stormpath/sdk": "<9.9.99", + "studio-42/elfinder": "<2.1.62", + "subhh/libconnect": "<7.0.8|>=8,<8.1", + "sukohi/surpass": "<1", + "sulu/sulu": "<1.6.44|>=2,<2.4.16|>=2.5,<2.5.12", + "sumocoders/framework-user-bundle": "<1.4", + "superbig/craft-audit": "<3.0.2", + "swag/paypal": "<5.4.4", + "swiftmailer/swiftmailer": ">=4,<5.4.5", + "swiftyedit/swiftyedit": "<1.2", + "sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2", + "sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1", + "sylius/grid-bundle": "<1.10.1", + "sylius/paypal-plugin": ">=1,<1.2.4|>=1.3,<1.3.1", + "sylius/resource-bundle": ">=1,<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4", + "sylius/sylius": "<1.9.10|>=1.10,<1.10.11|>=1.11,<1.11.2", + "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99", + "symbiote/silverstripe-queuedjobs": ">=3,<3.0.2|>=3.1,<3.1.4|>=4,<4.0.7|>=4.1,<4.1.2|>=4.2,<4.2.4|>=4.3,<4.3.3|>=4.4,<4.4.3|>=4.5,<4.5.1|>=4.6,<4.6.4", + "symbiote/silverstripe-seed": "<6.0.3", + "symbiote/silverstripe-versionedfiles": "<=2.0.3", + "symfont/process": ">=0", + "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", + "symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", + "symfony/error-handler": ">=4.4,<4.4.4|>=5,<5.0.4", + "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1", + "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7|>=5.3.14,<=5.3.14|>=5.4.3,<=5.4.3|>=6.0.3,<=6.0.3", + "symfony/http-foundation": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7", + "symfony/http-kernel": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.2.6", + "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", + "symfony/maker-bundle": ">=1.27,<1.29.2|>=1.30,<1.31.1", + "symfony/mime": ">=4.3,<4.3.8", + "symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", + "symfony/polyfill": ">=1,<1.10", + "symfony/polyfill-php55": ">=1,<1.10", + "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", + "symfony/routing": ">=2,<2.0.19", + "symfony/security": ">=2,<2.7.51|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.8", + "symfony/security-bundle": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.2.6", + "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.9", + "symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", + "symfony/security-guard": ">=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8", + "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7|>=5.1,<5.2.8|>=5.3,<5.3.2|>=5.4,<5.4.31|>=6,<6.3.8", + "symfony/serializer": ">=2,<2.0.11|>=4.1,<4.4.35|>=5,<5.3.12", + "symfony/symfony": ">=2,<4.4.51|>=5,<5.4.31|>=6,<6.3.8", + "symfony/translation": ">=2,<2.0.17", + "symfony/twig-bridge": ">=2,<4.4.51|>=5,<5.4.31|>=6,<6.3.8", + "symfony/ux-autocomplete": "<2.11.2", + "symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3", + "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8", + "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4", + "symfony/webhook": ">=6.3,<6.3.8", + "symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7|>=2.2.0.0-beta1,<2.2.0.0-beta2", + "symphonycms/symphony-2": "<2.6.4", + "t3/dce": "<0.11.5|>=2.2,<2.6.2", + "t3g/svg-sanitizer": "<1.0.3", + "t3s/content-consent": "<1.0.3|>=2,<2.0.2", + "tastyigniter/tastyigniter": "<3.3", + "tcg/voyager": "<=1.4", + "tecnickcom/tcpdf": "<6.2.22", + "terminal42/contao-tablelookupwizard": "<3.3.5", + "thelia/backoffice-default-template": ">=2.1,<2.1.2", + "thelia/thelia": ">=2.1,<2.1.3", + "theonedemon/phpwhois": "<=4.2.5", + "thinkcmf/thinkcmf": "<=5.1.7", + "thorsten/phpmyfaq": "<3.2.2", + "tikiwiki/tiki-manager": "<=17.1", + "tinymce/tinymce": "<5.10.9|>=6,<6.7.3", + "tinymighty/wiki-seo": "<1.2.2", + "titon/framework": "<9.9.99", + "tobiasbg/tablepress": "<=2.0.0.0-RC1", + "topthink/framework": "<6.0.14", + "topthink/think": "<=6.1.1", + "topthink/thinkphp": "<=3.2.3", + "torrentpier/torrentpier": "<=2.4.1", + "tpwd/ke_search": "<4.0.3|>=4.1,<4.6.6|>=5,<5.0.2", + "tribalsystems/zenario": "<=9.4.59197", + "truckersmp/phpwhois": "<=4.3.1", + "ttskch/pagination-service-provider": "<1", + "twig/twig": "<1.44.7|>=2,<2.15.3|>=3,<3.4.3", + "typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2", + "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.13|>=11,<=11.1", + "typo3/cms-core": "<=8.7.56|>=9,<=9.5.45|>=10,<=10.4.42|>=11,<=11.5.34|>=12,<=12.4.10|==13", + "typo3/cms-extbase": "<6.2.24|>=7,<7.6.8|==8.1.1", + "typo3/cms-fluid": "<4.3.4|>=4.4,<4.4.1", + "typo3/cms-form": ">=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.13|>=11,<=11.1", + "typo3/cms-frontend": "<4.3.9|>=4.4,<4.4.5", + "typo3/cms-install": "<4.1.14|>=4.2,<4.2.16|>=4.3,<4.3.9|>=4.4,<4.4.5|>=12.2,<12.4.8", + "typo3/cms-rte-ckeditor": ">=9.5,<9.5.42|>=10,<10.4.39|>=11,<11.5.30", + "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6", + "typo3/html-sanitizer": ">=1,<=1.5.2|>=2,<=2.1.3", + "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.3.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<3.3.23|>=4,<4.0.17|>=4.1,<4.1.16|>=4.2,<4.2.12|>=4.3,<4.3.3", + "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1", + "typo3/swiftmailer": ">=4.1,<4.1.99|>=5.4,<5.4.5", + "typo3fluid/fluid": ">=2,<2.0.8|>=2.1,<2.1.7|>=2.2,<2.2.4|>=2.3,<2.3.7|>=2.4,<2.4.4|>=2.5,<2.5.11|>=2.6,<2.6.10", + "ua-parser/uap-php": "<3.8", + "uasoft-indonesia/badaso": "<=2.9.7", + "unisharp/laravel-filemanager": "<2.6.4", + "userfrosting/userfrosting": ">=0.3.1,<4.6.3", + "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2", + "uvdesk/community-skeleton": "<=1.1.1", + "vanilla/safecurl": "<0.9.2", + "verot/class.upload.php": "<=2.1.6", + "vova07/yii2-fileapi-widget": "<0.1.9", + "vrana/adminer": "<4.8.1", + "waldhacker/hcaptcha": "<2.1.2", + "wallabag/tcpdf": "<6.2.22", + "wallabag/wallabag": "<2.6.7", + "wanglelecc/laracms": "<=1.0.3", + "web-auth/webauthn-framework": ">=3.3,<3.3.4", + "webbuilders-group/silverstripe-kapost-bridge": "<0.4", + "webcoast/deferred-image-processing": "<1.0.2", + "webklex/laravel-imap": "<5.3", + "webklex/php-imap": "<5.3", + "webpa/webpa": "<3.1.2", + "wikibase/wikibase": "<=1.39.3", + "wikimedia/parsoid": "<0.12.2", + "willdurand/js-translation-bundle": "<2.1.1", + "winter/wn-backend-module": "<1.2.4", + "winter/wn-system-module": "<1.2.4", + "wintercms/winter": "<1.2.3", + "woocommerce/woocommerce": "<6.6", + "wp-cli/wp-cli": ">=0.12,<2.5", + "wp-graphql/wp-graphql": "<=1.14.5", + "wpanel/wpanel4-cms": "<=4.3.1", + "wpcloud/wp-stateless": "<3.2", + "wwbn/avideo": "<=12.4", + "xataface/xataface": "<3", + "xpressengine/xpressengine": "<3.0.15", + "yeswiki/yeswiki": "<4.1", + "yetiforce/yetiforce-crm": "<=6.4", + "yidashi/yii2cmf": "<=2", + "yii2mod/yii2-cms": "<1.9.2", + "yiisoft/yii": "<1.1.29", + "yiisoft/yii2": "<2.0.38", + "yiisoft/yii2-authclient": "<2.2.15", + "yiisoft/yii2-bootstrap": "<2.0.4", + "yiisoft/yii2-dev": "<2.0.43", + "yiisoft/yii2-elasticsearch": "<2.0.5", + "yiisoft/yii2-gii": "<=2.2.4", + "yiisoft/yii2-jui": "<2.0.4", + "yiisoft/yii2-redis": "<2.0.8", + "yikesinc/yikes-inc-easy-mailchimp-extender": "<6.8.6", + "yoast-seo-for-typo3/yoast_seo": "<7.2.3", + "yourls/yourls": "<=1.8.2", + "yuan1994/tpadmin": "<=1.3.12", + "zencart/zencart": "<=1.5.7.0-beta", + "zendesk/zendesk_api_client_php": "<2.2.11", + "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3", + "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2", + "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2", + "zendframework/zend-db": "<2.2.10|>=2.3,<2.3.5", + "zendframework/zend-developer-tools": ">=1.2.2,<1.2.3", + "zendframework/zend-diactoros": "<1.8.4", + "zendframework/zend-feed": "<2.10.3", + "zendframework/zend-form": ">=2,<2.2.7|>=2.3,<2.3.1", + "zendframework/zend-http": "<2.8.1", + "zendframework/zend-json": ">=2.1,<2.1.6|>=2.2,<2.2.6", + "zendframework/zend-ldap": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.8|>=2.3,<2.3.3", + "zendframework/zend-mail": ">=2,<2.4.11|>=2.5,<2.7.2", + "zendframework/zend-navigation": ">=2,<2.2.7|>=2.3,<2.3.1", + "zendframework/zend-session": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.9|>=2.3,<2.3.4", + "zendframework/zend-validator": ">=2.3,<2.3.6", + "zendframework/zend-view": ">=2,<2.2.7|>=2.3,<2.3.1", + "zendframework/zend-xmlrpc": ">=2.1,<2.1.6|>=2.2,<2.2.6", + "zendframework/zendframework": "<=3", + "zendframework/zendframework1": "<1.12.20", + "zendframework/zendopenid": "<2.0.2", + "zendframework/zendrest": "<2.0.2", + "zendframework/zendservice-amazon": "<2.0.3", + "zendframework/zendservice-api": "<1", + "zendframework/zendservice-audioscrobbler": "<2.0.2", + "zendframework/zendservice-nirvanix": "<2.0.2", + "zendframework/zendservice-slideshare": "<2.0.2", + "zendframework/zendservice-technorati": "<2.0.2", + "zendframework/zendservice-windowsazure": "<2.0.2", + "zendframework/zendxml": ">=1,<1.0.1", + "zenstruck/collection": "<0.2.1", + "zetacomponents/mail": "<1.8.2", + "zf-commons/zfc-user": "<1.2.2", + "zfcampus/zf-apigility-doctrine": ">=1,<1.0.3", + "zfr/zfr-oauth2-server-module": "<0.1.2", + "zoujingli/thinkadmin": "<=6.1.53" + }, + "default-branch": true, + "type": "metapackage", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "role": "maintainer" + }, + { + "name": "Ilya Tribusean", + "email": "slash3b@gmail.com", + "role": "maintainer" + } + ], + "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it", + "keywords": [ + "dev" + ], + "support": { + "issues": "https://github.com/Roave/SecurityAdvisories/issues", + "source": "https://github.com/Roave/SecurityAdvisories/tree/latest" + }, + "funding": [ + { + "url": "https://github.com/Ocramius", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/roave/security-advisories", + "type": "tidelift" + } + ], + "time": "2024-02-27T22:04:41+00:00" + }, + { + "name": "slevomat/coding-standard", + "version": "8.14.1", + "source": { + "type": "git", + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/fea1fd6f137cc84f9cba0ae30d549615dbc6a926", + "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", + "php": "^7.2 || ^8.0", + "phpstan/phpdoc-parser": "^1.23.1", + "squizlabs/php_codesniffer": "^3.7.1" + }, + "require-dev": { + "phing/phing": "2.17.4", + "php-parallel-lint/php-parallel-lint": "1.3.2", + "phpstan/phpstan": "1.10.37", + "phpstan/phpstan-deprecation-rules": "1.1.4", + "phpstan/phpstan-phpunit": "1.3.14", + "phpstan/phpstan-strict-rules": "1.5.1", + "phpunit/phpunit": "8.5.21|9.6.8|10.3.5" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" + ], + "support": { + "issues": "https://github.com/slevomat/coding-standard/issues", + "source": "https://github.com/slevomat/coding-standard/tree/8.14.1" + }, + "funding": [ + { + "url": "https://github.com/kukulich", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", + "type": "tidelift" + } + ], + "time": "2023-10-08T07:28:08+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/d63cee4890a8afaf86a22e51ad4d97c91dd4579b", + "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2024-02-16T15:06:51+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "e03ad7c1535e623edbb94c22cc42353e488c6670" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/e03ad7c1535e623edbb94c22cc42353e488c6670", + "reference": "e03ad7c1535e623edbb94c22cc42353e488c6670", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", + "twig/twig": "^3.0.4" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-15T11:33:06+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "roave/security-advisories": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.2", + "ext-curl": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-pdo": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/db/migrations/20240204091722_initial_database.php b/db/migrations/20240204091722_initial_database.php new file mode 100644 index 0000000..7ee0f6e --- /dev/null +++ b/db/migrations/20240204091722_initial_database.php @@ -0,0 +1,19 @@ +isMigratingUp()) { + $initialDbSql = file_get_contents(__DIR__ . '/20240204091722_initial_database.sql'); + $this->execute($initialDbSql); + } + } +} diff --git a/db/migrations/20240204091722_initial_database.sql b/db/migrations/20240204091722_initial_database.sql new file mode 100644 index 0000000..0f71d79 --- /dev/null +++ b/db/migrations/20240204091722_initial_database.sql @@ -0,0 +1,230 @@ +SET FOREIGN_KEY_CHECKS=0; + +DROP TABLE IF EXISTS `dj_albums`; +DROP TABLE IF EXISTS `dj_songs`; +DROP TABLE IF EXISTS `internal_finance_audit`; +DROP TABLE IF EXISTS `logs`; +DROP TABLE IF EXISTS `web_announce`; +DROP TABLE IF EXISTS `web_asset_purchases`; +DROP TABLE IF EXISTS `web_assets`; + +CREATE TABLE IF NOT EXISTS `web_banlist` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `ip` varchar(50) DEFAULT NULL, + `uid` int(11) DEFAULT NULL, + `tstamp` int(11) DEFAULT NULL, + `guid` int(11) DEFAULT NULL, + `attempt` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `ip` (`ip`), + KEY `guid` (`guid`), + KEY `uid` (`uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; + +DROP TABLE IF EXISTS `web_bbcode`; + +DROP TABLE IF EXISTS `web_bookmarks`; + +DROP TABLE IF EXISTS `web_clicks`; + +CREATE TABLE IF NOT EXISTS `web_comments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `comment_group` varchar(50) DEFAULT NULL, + `comment` text DEFAULT NULL, + `location` text DEFAULT NULL, + `tstamp` int(11) DEFAULT NULL, + `like_count` int(11) DEFAULT 0, + `creator` int(11) DEFAULT NULL, + KEY `id` (`id`,`creator`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; + +CREATE TABLE IF NOT EXISTS `web_dmx_fixtures` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `fixture_name` varchar(255) NOT NULL, + `universe_number` int(11) NOT NULL, + `channel_number` int(11) NOT NULL, + `rig_name` varchar(50) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; + +CREATE TABLE IF NOT EXISTS `web_events` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `creator` int(11) NOT NULL, + `event` varchar(50) NOT NULL DEFAULT 'none', + `url` text NOT NULL, + `tstamp` int(11) NOT NULL, + `slot` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +DROP TABLE IF EXISTS `web_groups`; + +DROP TABLE IF EXISTS `web_livechat`; + +DROP TABLE IF EXISTS `web_pages`; + +DROP TABLE IF EXISTS `web_pm`; + +CREATE TABLE IF NOT EXISTS `web_posters` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `creator` int(11) DEFAULT NULL, + `last_viewed` int(11) DEFAULT NULL, + `location` varchar(50) DEFAULT NULL, + `file` varchar(50) DEFAULT NULL, + `views` int(11) DEFAULT 0, + `mode` int(11) DEFAULT 0 COMMENT '0 Default, 1 Artwork', + PRIMARY KEY (`id`), + KEY `id` (`id`), + KEY `mode` (`mode`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; + +CREATE TABLE IF NOT EXISTS `web_sessions` ( + `uid` int(11) DEFAULT NULL, + `sessionid` varchar(32) DEFAULT NULL, + `ip` varchar(39) DEFAULT NULL, + `time` int(11) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; + +DROP TABLE IF EXISTS `web_socket`; + +DROP TABLE IF EXISTS `web_status`; + +CREATE TABLE IF NOT EXISTS `web_user_skills` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `creator` int(11) DEFAULT NULL, + `skill` varchar(100) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `id` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `web_users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(24) NOT NULL DEFAULT 'DELETEME', + `password` varchar(255) NOT NULL, + `update` varchar(50) NOT NULL DEFAULT '0', + `Is_team` int(11) NOT NULL DEFAULT 0, + `is_dj` int(11) NOT NULL DEFAULT 0, + `dj_name` varchar(50) NOT NULL DEFAULT 'Skrillix', + `dj_genre` varchar(50) NOT NULL DEFAULT '0', + `is_admin` int(11) NOT NULL DEFAULT 0, + `is_blue` int(11) NOT NULL DEFAULT 0, + `team_type` varchar(50) NOT NULL DEFAULT '0', + `website` varchar(18) DEFAULT NULL, + `site` varchar(50) NOT NULL DEFAULT 'engineerisaac.com', + `wolfbuck` decimal(20,6) unsigned NOT NULL DEFAULT 0.000000, + `aboutme` text DEFAULT NULL, + `user_img` varchar(50) DEFAULT '000.png', + `dj_img` varchar(50) DEFAULT NULL, + `email` varchar(50) DEFAULT NULL, + `dealer` int(10) unsigned NOT NULL DEFAULT 0, + `permission` int(10) unsigned NOT NULL DEFAULT 10, + `role` varchar(24) DEFAULT 'user', + `ref` varchar(24) DEFAULT 'user', + `location` int(10) unsigned NOT NULL DEFAULT 0, + `webmin` int(11) NOT NULL DEFAULT 0, + `active` int(11) DEFAULT 1, + `pid` int(11) NOT NULL DEFAULT 0, + `suspended` int(11) NOT NULL DEFAULT 0, + `account` int(10) unsigned DEFAULT 0, + `score` int(10) unsigned DEFAULT 0, + `pressure` int(10) unsigned NOT NULL DEFAULT 0, + `username_16` varchar(24) NOT NULL DEFAULT 'DELETEME', + `bf2_key` varchar(24) DEFAULT NULL, + `token` varchar(50) DEFAULT NULL, + `twitch` varchar(24) DEFAULT NULL, + `steam` varchar(24) DEFAULT NULL, + `vrchat` varchar(24) DEFAULT NULL, + `discord` varchar(24) DEFAULT NULL, + `vrcdn` tinytext DEFAULT NULL, + `vrcdn_show` int(11) DEFAULT 0, + `facebook` varchar(24) DEFAULT NULL, + `twitter` varchar(24) DEFAULT NULL, + `youtube` varchar(24) DEFAULT NULL, + `youtube_id` varchar(50) DEFAULT NULL, + `game_password` varchar(50) DEFAULT NULL, + `game_country` varchar(50) DEFAULT NULL, + `first_name` varchar(50) DEFAULT NULL, + `last_name` varchar(50) DEFAULT NULL, + `profile_header` varchar(50) NOT NULL DEFAULT 'placeholder.png', + `confirmed_em` tinyint(3) unsigned NOT NULL DEFAULT 0, + `badpass` int(11) DEFAULT 0, + `goodpass` int(11) DEFAULT 0, + `allowed` int(11) DEFAULT 0, + `enabled` int(11) DEFAULT 0, + `rank` int(11) DEFAULT 0, + `uid` int(11) DEFAULT 0, + `app_load` int(11) DEFAULT 0, + `game_tstamp` int(11) DEFAULT 0, + `fesl_token` int(11) DEFAULT 0, + `posts` int(10) unsigned NOT NULL DEFAULT 0, + `is_mod` int(11) DEFAULT 0, + `is_donator` int(11) DEFAULT 0, + `donation` int(11) NOT NULL DEFAULT 0, + `lastip` varchar(39) DEFAULT NULL, + `credits` varchar(18) DEFAULT NULL, + `user_access` varchar(18) DEFAULT NULL, + `session` int(10) unsigned DEFAULT 0, + `usergroup` int(10) unsigned NOT NULL DEFAULT 0, + `regip` varchar(50) NOT NULL DEFAULT '0', + `signature` mediumtext DEFAULT NULL, + `country` varchar(4) NOT NULL DEFAULT 'US', + `reg_date` int(11) NOT NULL, + `lastactive` int(11) DEFAULT NULL, + `last_login` int(11) DEFAULT NULL, + `profile_yt` varchar(15) DEFAULT 'mEqcala-NiE', + `online` int(11) DEFAULT 0, + `banned` tinyint(1) NOT NULL DEFAULT 0, + `valid` tinyint(1) NOT NULL DEFAULT 0, + `avatar` tinytext DEFAULT NULL, + `edit_plugins` int(11) DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `username_3` (`username`), + KEY `username` (`username`), + KEY `last_active` (`lastactive`), + KEY `username_password` (`username`,`password`), + KEY `username_email` (`username`,`email`), + KEY `email` (`email`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; + +DROP TABLE IF EXISTS `web_vars`; + +DROP TABLE IF EXISTS `web_video_cata`; + +DROP TABLE IF EXISTS `web_video_views`; + +DROP TABLE IF EXISTS `web_videos`; + +CREATE TABLE IF NOT EXISTS `web_world_views` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `ip` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL, + `world` int(11) DEFAULT NULL, + `creator` int(11) DEFAULT NULL, + `tstamp` int(11) DEFAULT NULL, + `hits` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; + +CREATE TABLE IF NOT EXISTS `web_worlds` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `creator` int(11) DEFAULT 1, + `title` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '0', + `image` varchar(100) DEFAULT NULL, + `youtubeid` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '0', + `world_id` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '0', + `world_creator` varchar(50) DEFAULT '0', + `platform` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT 'PC', + `yt_creator` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL, + `vgroup` int(11) DEFAULT 1, + `featured` int(11) DEFAULT 0, + `owner` int(11) DEFAULT 0, + `description` mediumtext CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL, + `show` int(11) DEFAULT 1, + `hits` int(11) DEFAULT 0, + `type` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL, + `game` int(11) NOT NULL DEFAULT 0, + `tstamp` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL, + KEY `id` (`id`), + KEY `title` (`title`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; + +SET FOREIGN_KEY_CHECKS=1; diff --git a/db/migrations/20240204095953_set_charset.php b/db/migrations/20240204095953_set_charset.php new file mode 100644 index 0000000..17fe732 --- /dev/null +++ b/db/migrations/20240204095953_set_charset.php @@ -0,0 +1,19 @@ +isMigratingUp()) { + $initialDbSql = file_get_contents(__DIR__ . '/20240204095953_set_charset.sql'); + $this->execute($initialDbSql); + } + } +} diff --git a/db/migrations/20240204095953_set_charset.sql b/db/migrations/20240204095953_set_charset.sql new file mode 100644 index 0000000..de89d5d --- /dev/null +++ b/db/migrations/20240204095953_set_charset.sql @@ -0,0 +1,10 @@ +ALTER TABLE `web_banlist` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `web_comments` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `web_dmx_fixtures` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `web_events` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `web_posters` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `web_sessions` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `web_user_skills` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `web_users` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `web_world_views` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `web_worlds` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/db/migrations/20240204111213_clean_up_users.php b/db/migrations/20240204111213_clean_up_users.php new file mode 100644 index 0000000..3883bf8 --- /dev/null +++ b/db/migrations/20240204111213_clean_up_users.php @@ -0,0 +1,69 @@ +table('web_users') + ->renameColumn('Is_team', 'tmp_is_team') + ->update(); + + $this->table('web_users') + ->renameColumn('tmp_is_team', 'is_team') + ->removeColumn('update') + ->removeColumn('is_blue') + ->removeColumn('site') + ->removeColumn('dj_img') + ->removeColumn('dealer') + ->removeColumn('permission') + ->removeColumn('role') + ->removeColumn('location') + ->removeColumn('webmin') + ->removeColumn('pid') + ->removeColumn('suspended') + ->removeColumn('account') + ->removeColumn('score') + ->removeColumn('pressure') + ->removeColumn('steam') + ->removeColumn('facebook') + ->removeColumn('twitter') + ->removeColumn('youtube') + ->removeColumn('youtube_id') + ->removeColumn('game_password') + ->removeColumn('game_country') + ->removeColumn('first_name') + ->removeColumn('last_name') + ->removeColumn('profile_header') + ->removeColumn('confirmed_em') + ->removeColumn('allowed') + ->removeColumn('enabled') + ->removeColumn('rank') + ->removeColumn('uid') + ->removeColumn('app_load') + ->removeColumn('game_tstamp') + ->removeColumn('fesl_token') + ->removeColumn('posts') + ->removeColumn('is_mod') + ->removeColumn('is_donator') + ->removeColumn('donation') + ->removeColumn('credits') + ->removeColumn('user_access') + ->removeColumn('session') + ->removeColumn('usergroup') + ->removeColumn('regip') + ->removeColumn('signature') + ->removeColumn('profile_yt') + ->removeColumn('valid') + ->removeColumn('avatar') + ->removeColumn('edit_plugins') + ->update(); + } +} diff --git a/db/migrations/20240206083034_add_foreign_keys.php b/db/migrations/20240206083034_add_foreign_keys.php new file mode 100644 index 0000000..2e8e586 --- /dev/null +++ b/db/migrations/20240206083034_add_foreign_keys.php @@ -0,0 +1,44 @@ +addForeignKey('web_comments'); + $this->addForeignKey('web_posters'); + $this->addForeignKey('web_sessions', 'uid'); + $this->addForeignKey('web_user_skills'); + } + + private function addForeignKey(string $table, string $column = 'creator'): void + { + if ($this->isMigratingUp()) { + $this->execute( + <<table($table) + ->addForeignKey( + $column, + 'web_users', + 'id', + [ + 'delete' => 'CASCADE', + 'update' => 'CASCADE', + ] + )->update(); + } +} diff --git a/db/migrations/20240207153724_add_user_login_token.php b/db/migrations/20240207153724_add_user_login_token.php new file mode 100644 index 0000000..7dd7875 --- /dev/null +++ b/db/migrations/20240207153724_add_user_login_token.php @@ -0,0 +1,47 @@ +table('web_user_login_tokens', [ + 'id' => false, + 'primary_key' => ['id'] + ])->addColumn( + (new Column()) + ->setName('id') + ->setType(Column::STRING) + ->setLimit(16) + ->setNull(false) + )->addColumn( + (new Column()) + ->setName('verifier') + ->setType(Column::STRING) + ->setLimit(128) + ->setNull(false) + )->addColumn( + (new Column()) + ->setName('creator') + ->setType(Column::INTEGER) + ->setNull(false) + )->addTimestamps(null, false) + ->addForeignKey( + 'creator', + 'web_users', + 'id', + [ + 'delete' => 'CASCADE', + 'update' => 'CASCADE', + ] + )->create(); + } +} diff --git a/db/migrations/20240208073557_modify_user_tables.php b/db/migrations/20240208073557_modify_user_tables.php new file mode 100644 index 0000000..c9e8fec --- /dev/null +++ b/db/migrations/20240208073557_modify_user_tables.php @@ -0,0 +1,41 @@ +table('web_users') + ->changeColumn( + 'website', + (new Column()) + ->setType(Column::STRING) + ->setLimit(255) + ->setNull(true) + )->addColumn( + (new Column()) + ->setName('title') + ->setType(Column::STRING) + ->setLimit(255) + ->setNull(true) + ->setAfter('aboutme') + )->update(); + + if ($this->isMigratingUp()) { + $this->execute( + <<<'SQL' + UPDATE web_users + SET title=aboutme + WHERE is_team = 1 + SQL + ); + } + } +} diff --git a/db/migrations/20240209055718_more_user_table_changes.php b/db/migrations/20240209055718_more_user_table_changes.php new file mode 100644 index 0000000..6dfbcaa --- /dev/null +++ b/db/migrations/20240209055718_more_user_table_changes.php @@ -0,0 +1,149 @@ +table('web_users') + ->changeColumn( + 'username', + (new Column()) + ->setType(Column::STRING) + ->setLimit(24) + ->setNull(false) + ->setDefault(null) + )->changeColumn( + 'email', + (new Column()) + ->setType(Column::STRING) + ->setLimit(100) + ->setNull(false) + ->setAfter('username') + )->changeColumn( + 'is_team', + (new Column()) + ->setType(Column::BOOLEAN) + ->setNull(false) + ->setDefault(0) + )->changeColumn( + 'is_admin', + (new Column()) + ->setType(Column::BOOLEAN) + ->setAfter('is_team') + ->setNull(false) + ->setDefault(0) + )->changeColumn( + 'is_dj', + (new Column()) + ->setType(Column::BOOLEAN) + ->setNull(false) + ->setDefault(0) + )->changeColumn( + 'banned', + (new Column()) + ->setType(Column::BOOLEAN) + ->setAfter('password') + ->setNull(false) + ->setDefault(0) + )->changeColumn( + 'dj_name', + (new Column()) + ->setType(Column::STRING) + ->setLimit(150) + ->setNull(true) + ->setDefault(null) + )->changeColumn( + 'dj_genre', + (new Column()) + ->setType(Column::STRING) + ->setLimit(50) + ->setNull(true) + ->setDefault(null) + )->changeColumn( + 'team_type', + (new Column()) + ->setType(Column::STRING) + ->setLimit(50) + ->setNull(true) + ->setDefault(null) + )->addColumn( + (new Column()) + ->setName('pronouns') + ->setType(Column::STRING) + ->setLimit(50) + ->setNull(true) + ->setAfter('title') + )->changeColumn( + 'user_img', + (new Column()) + ->setType(Column::STRING) + ->setLimit(50) + ->setNull(true) + ->setDefault(null) + )->changeColumn( + 'ref', + (new Column()) + ->setType(Column::STRING) + ->setLimit(24) + ->setNull(true) + ->setDefault(null) + )->changeColumn( + 'vrcdn_show', + (new Column()) + ->setType(Column::BOOLEAN) + ->setNull(false) + ->setDefault(0) + )->changeColumn( + 'country', + (new Column()) + ->setType(Column::STRING) + ->setLimit(4) + ->setNull(true) + ->setDefault(null) + )->changeColumn( + 'online', + (new Column()) + ->setType(Column::BOOLEAN) + ->setNull(false) + ->setDefault(0) + )->removeColumn('wolfbuck') + ->removeColumn('username_16') + ->removeColumn('bf2_key') + ->removeColumn('token') + ->update(); + + if ($this->isMigratingUp()) { + $this->execute( + <<<'SQL' + UPDATE web_users + SET team_type=null + WHERE team_type = '0' + SQL + ); + + $this->execute( + <<<'SQL' + UPDATE web_users + SET dj_name=null + WHERE dj_name = 'Skrillix' + SQL + ); + + $this->execute( + <<<'SQL' + UPDATE web_users + SET dj_genre=null + WHERE dj_genre = '0' + SQL + ); + } + } +} diff --git a/db/migrations/20240209081427_actually_drop_extra_tables.php b/db/migrations/20240209081427_actually_drop_extra_tables.php new file mode 100644 index 0000000..486fef2 --- /dev/null +++ b/db/migrations/20240209081427_actually_drop_extra_tables.php @@ -0,0 +1,21 @@ +hasTable($table)) { + $this->table($table)->drop()->save(); + } + } + } +} diff --git a/db/migrations/20240209130416_fix_avatars.php b/db/migrations/20240209130416_fix_avatars.php new file mode 100644 index 0000000..50e3701 --- /dev/null +++ b/db/migrations/20240209130416_fix_avatars.php @@ -0,0 +1,22 @@ +execute( + <<<'SQL' + UPDATE web_users + SET user_img=null + WHERE user_img = '000.png' + SQL + ); + } +} diff --git a/db/migrations/20240212063433_more_new_user_fields.php b/db/migrations/20240212063433_more_new_user_fields.php new file mode 100644 index 0000000..55004f6 --- /dev/null +++ b/db/migrations/20240212063433_more_new_user_fields.php @@ -0,0 +1,39 @@ +table('web_users') + ->addColumn( + (new Column()) + ->setName('dj_img') + ->setType(Column::STRING) + ->setLimit(50) + ->setNull(true) + ->setAfter('dj_genre') + )->addColumn( + (new Column()) + ->setName('atproto_did') + ->setType(Column::STRING) + ->setLimit(128) + ->setNull(true) + ->setAfter('vrcdn_show') + )->addColumn( + (new Column()) + ->setName('is_mod') + ->setType(Column::BOOLEAN) + ->setNull(false) + ->setDefault(0) + ->setAfter('is_admin') + )->update(); + } +} diff --git a/db/migrations/20240217110838_expand_poster_network.php b/db/migrations/20240217110838_expand_poster_network.php new file mode 100644 index 0000000..c864fa6 --- /dev/null +++ b/db/migrations/20240217110838_expand_poster_network.php @@ -0,0 +1,131 @@ +table('web_groups', [ + 'id' => false, + 'primary_key' => 'id' + ]); + + $groupsTable->addColumn( + (new Column()) + ->setName('id') + ->setType(Column::STRING) + ->setLimit(50) + ->setNull(false) + )->addColumn( + (new Column()) + ->setName('name') + ->setType(Column::STRING) + ->setLimit(255) + ->setNull(false) + )->create(); + + $this->table('web_user_has_group', [ + 'id' => false, + 'primary_key' => ['user_id', 'group_id'] + ])->addColumn( + (new Column()) + ->setName('user_id') + ->setType(Column::INTEGER) + ->setNull(false) + )->addColumn( + (new Column()) + ->setName('group_id') + ->setType(Column::STRING) + ->setLimit(50) + ->setNull(false) + )->addForeignKey( + 'user_id', + 'web_users', + 'id', + [ + 'delete' => 'CASCADE', + 'update' => 'CASCADE', + ] + )->addForeignKey( + 'group_id', + 'web_groups', + 'id', + [ + 'delete' => 'CASCADE', + 'update' => 'CASCADE', + ] + )->create(); + + $posterTypesTable = $this->table('web_poster_types', [ + 'id' => false, + 'primary_key' => ['id'] + ]); + + $posterTypesTable->addColumn( + (new Column()) + ->setName('id') + ->setType(Column::STRING) + ->setLimit(50) + ->setNull(false) + )->addColumn( + (new Column()) + ->setName('description') + ->setType(Column::STRING) + ->setLimit(255) + ->setNull(true) + )->create(); + + $this->table('web_posters') + ->removeColumn('location') + ->removeColumn('mode') + ->addColumn( + (new Column()) + ->setName('nickname') + ->setType(Column::STRING) + ->setLimit(255) + ->setNull(true) + ) + ->addColumn( + (new Column()) + ->setName('expires_at') + ->setType(Column::TIMESTAMP) + ->setNull(true) + ) + ->addColumn( + (new Column()) + ->setName('type_id') + ->setType(Column::STRING) + ->setLimit(50) + ->setNull(true) + )->addColumn( + (new Column()) + ->setName('group_id') + ->setType(Column::STRING) + ->setLimit(50) + ->setNull(true) + )->addForeignKey( + 'type_id', + 'web_poster_types', + 'id', + [ + 'delete' => 'SET NULL', + 'update' => 'CASCADE', + ] + )->addForeignKey( + 'group_id', + 'web_groups', + 'id', + [ + 'delete' => 'SET NULL', + 'update' => 'CASCADE', + ] + )->update(); + } +} diff --git a/db/migrations/20240220171440_rename_poster_nickname.php b/db/migrations/20240220171440_rename_poster_nickname.php new file mode 100644 index 0000000..c3bdb1f --- /dev/null +++ b/db/migrations/20240220171440_rename_poster_nickname.php @@ -0,0 +1,18 @@ +table('web_posters') + ->renameColumn('nickname', 'collection') + ->update(); + } +} diff --git a/db/migrations/20240223182933_add_short_urls.php b/db/migrations/20240223182933_add_short_urls.php new file mode 100644 index 0000000..2147e7c --- /dev/null +++ b/db/migrations/20240223182933_add_short_urls.php @@ -0,0 +1,55 @@ +table('web_short_urls') + ->addColumn( + (new Column()) + ->setName('creator') + ->setType(Column::INTEGER) + ->setNull(true) + )->addColumn( + (new Column()) + ->setName('short_url') + ->setType(Column::STRING) + ->setLimit(128) + ->setNull(false) + )->addColumn( + (new Column()) + ->setName('long_url') + ->setType(Column::STRING) + ->setLimit(255) + ->setNull(false) + )->addColumn( + (new Column()) + ->setName('views') + ->setType(Column::INTEGER) + ->setNull(false) + ->setDefault(0) + )->addTimestamps(null, false) + ->addForeignKey( + 'creator', + 'web_users', + 'id', + [ + 'delete' => 'SET NULL', + 'update' => 'CASCADE', + ] + )->addIndex( + 'short_url', + [ + 'unique' => true + ] + )->create(); + } +} diff --git a/db/migrations/20240228225936_remove_session_tables.php b/db/migrations/20240228225936_remove_session_tables.php new file mode 100644 index 0000000..dcfc00f --- /dev/null +++ b/db/migrations/20240228225936_remove_session_tables.php @@ -0,0 +1,22 @@ +table('web_world_views') + ->drop() + ->save(); + + $this->table('web_sessions') + ->drop() + ->save(); + } +} diff --git a/db/seeds/GroupsSeeder.php b/db/seeds/GroupsSeeder.php new file mode 100644 index 0000000..ac3de59 --- /dev/null +++ b/db/seeds/GroupsSeeder.php @@ -0,0 +1,37 @@ + 'eufuria', + 'name' => 'EUFuria', + ], + [ + 'id' => 'iwait', + 'name' => 'i.W.a.I.T.', + ], + [ + 'id' => 'virtualfurmix', + 'name' => 'Virtual FurMix' + ], + [ + 'id' => 'waterwolf', + 'name' => 'WaterWolf' + ] + ]; + + $this->table('web_groups') + ->insert($rows) + ->saveData(); + } +} diff --git a/db/seeds/PosterTypesSeeder.php b/db/seeds/PosterTypesSeeder.php new file mode 100644 index 0000000..2217620 --- /dev/null +++ b/db/seeds/PosterTypesSeeder.php @@ -0,0 +1,41 @@ + 'advert', + 'description' => 'Advertisements' + ], + [ + 'id' => 'dj', + 'description' => 'DJ Poster' + ], + [ + 'id' => 'event', + 'description' => 'Events', + ], + [ + 'id' => 'meme', + 'description' => 'Memes', + ], + [ + 'id' => 'photography', + 'description' => 'Photography' + ] + ]; + + $this->table('web_poster_types') + ->insert($rows) + ->saveData(); + } +} diff --git a/db/seeds/ShortUrlsSeeder.php b/db/seeds/ShortUrlsSeeder.php new file mode 100644 index 0000000..69b4928 --- /dev/null +++ b/db/seeds/ShortUrlsSeeder.php @@ -0,0 +1,36 @@ + UserSeeder::MODERATOR_ID, + 'short_url' => 'foo', + 'long_url' => 'https://foo.example.com/' + ], + [ + 'creator' => UserSeeder::ADMIN_ID, + 'short_url' => 'bar', + 'long_url' => '/login' + ] + ]; + + $this->table('web_short_urls') + ->insert($rows) + ->saveData(); + + } +} diff --git a/db/seeds/UserHasGroupSeeder.php b/db/seeds/UserHasGroupSeeder.php new file mode 100644 index 0000000..d45883a --- /dev/null +++ b/db/seeds/UserHasGroupSeeder.php @@ -0,0 +1,30 @@ + UserSeeder::USER_ID, + 'group_id' => 'waterwolf' + ] + ]; + + $this->table('web_user_has_group') + ->insert($rows) + ->saveData(); + } +} diff --git a/db/seeds/UserSeeder.php b/db/seeds/UserSeeder.php new file mode 100644 index 0000000..f81c144 --- /dev/null +++ b/db/seeds/UserSeeder.php @@ -0,0 +1,71 @@ + self::USER_ID, + 'username' => 'User', + 'email' => 'user@waterwolf.dev', + 'password' => $password, + 'reg_date' => time(), + ], + [ + 'id' => self::TEAM_MEMBER_ID, + 'username' => 'TeamMember', + 'email' => 'teammember@waterwolf.dev', + 'password' => $password, + 'reg_date' => time(), + 'is_team' => 1, + ], + [ + 'id' => self::MODERATOR_ID, + 'username' => 'Moderator', + 'email' => 'mod@waterwolf.dev', + 'password' => $password, + 'reg_date' => time(), + 'is_mod' => 1, + ], + [ + 'id' => self::ADMIN_ID, + 'username' => 'Admin', + 'email' => 'admin@waterwolf.dev', + 'password' => $password, + 'reg_date' => time(), + 'is_admin' => 1, + ], + [ + 'id' => self::BANNED_ID, + 'username' => 'Banned', + 'email' => 'banned@waterwolf.dev', + 'password' => $password, + 'reg_date' => time(), + 'banned' => 1, + ] + ]; + + $this->table('web_users') + ->insert($rows) + ->saveData(); + } +} diff --git a/dev.dist.env b/dev.dist.env new file mode 100644 index 0000000..541303d --- /dev/null +++ b/dev.dist.env @@ -0,0 +1,24 @@ +# Customize these environment variables to your liking for your local dev install +# The passwords are randomly generated, but things like MariaDB are never exposed +# to the web in local + +# Database username used by the app +MARIADB_USER="web" + +# Database password used by the app +MARIADB_PASSWORD="ymiS3QDYELqAwcWXJBVkenuDkt" + +# Database name used by the app +MARIADB_DATABASE="waterwolf" + +# Root DB password for admins +MARIADB_ROOT_PASSWORD="CD2132428BD13725AA3EFCCF92DB" + +# AWS Simple E-mail Service (SES) DSN +MAILER_DSN="ses+smtp://ACCESS_KEY:SECRET_KEY@default?region=us-west-2" + +# API Key for VRChat API proxy +VRCHAT_API_KEY="" + +# Webhook URL to dispatch +DISCORD_WEBHOOK_URL="" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..17bc4b8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +services: + web: + image: waterwolfdev/waterwolf-site:development + build: + context: . + target: development + depends_on: + - db + volumes: + - .:/var/app/www + env_file: + - dev.env + environment: + MARIADB_SERVER: db + restart: always + ports: + - "127.0.0.1:8080:8080" + logging: &default-logging + options: + max-size: "1m" + max-file: "5" + + db: + image: docker.io/library/mariadb:lts + volumes: + - db_data:/var/lib/mysql + env_file: + - dev.env + ports: + - "127.0.0.1:13306:3306" + logging: *default-logging + +volumes: + db_data: { } diff --git a/frontend/js/dateTimeUtils.js b/frontend/js/dateTimeUtils.js new file mode 100644 index 0000000..a3dbdca --- /dev/null +++ b/frontend/js/dateTimeUtils.js @@ -0,0 +1,25 @@ +import {DateTime} from 'luxon'; + +const defaultTz = 'America/New_York'; + +// Date/Time Utilities + +export function nowToDateTime(tz = defaultTz) { + return DateTime.local({zone: tz}); +} + +export function timestampToDateTime(value, tz = defaultTz) { + return DateTime.fromSeconds(Number(value), {zone: tz}); +} + +export function dateTimeToDateTimeString(value) { + return value.toLocaleString(DateTime.DATETIME_MED); +} + +export function dateTimeToTimeString(value) { + return value.toLocaleString(DateTime.TIME_SIMPLE); +} + +export function dateTimeToRelative(value) { + return value.toRelative(); +} diff --git a/frontend/js/passwordStrength.js b/frontend/js/passwordStrength.js new file mode 100644 index 0000000..6e1a3f3 --- /dev/null +++ b/frontend/js/passwordStrength.js @@ -0,0 +1,46 @@ +export default function passwordStrength( + passwordFieldId = 'passwordField', + passwordStrengthBarId = 'passwordStrengthBar', + passwordStrengthTextId = 'passwordStrengthText' +) { + document.getElementById(passwordFieldId).addEventListener("input", function () { + let val = this.value; + let strength = getStrength(val); + let progressBar = document.getElementById(passwordStrengthBarId); + let strengthText = document.getElementById(passwordStrengthTextId); + + progressBar.style.width = strength.percent + "%"; + + switch (strength.level) { + case 0: + progressBar.className = "progress-bar bg-danger"; + strengthText.innerText = "Weak"; + break; + case 1: + progressBar.className = "progress-bar bg-warning"; + strengthText.innerText = "Moderate"; + break; + case 2: + progressBar.className = "progress-bar bg-success"; + strengthText.innerText = "Strong"; + break; + } + }); + + function getStrength(password) { + let strength = { + level: 0, + percent: 0 + }; + + if (password.length >= 8) strength.percent += 33; + if (/[A-Z]/.test(password)) strength.percent += 33; + if (/[0-9]/.test(password)) strength.percent += 34; + + if (strength.percent <= 33) strength.level = 0; + else if (strength.percent <= 66) strength.level = 1; + else strength.level = 2; + + return strength; + } +} diff --git a/frontend/js/sweetalert.js b/frontend/js/sweetalert.js new file mode 100644 index 0000000..a311602 --- /dev/null +++ b/frontend/js/sweetalert.js @@ -0,0 +1,66 @@ +import $ from "jquery"; +import Swal from 'sweetalert2/dist/sweetalert2'; + +const swalCustom = Swal.mixin({ + confirmButtonText: "Confirm", + cancelButtonText: "Cancel", + showCancelButton: true, +}); + +const swalConfirmDelete = swalCustom.mixin({ + title: "Are you sure?", + confirmButtonText: "Delete", + confirmButtonColor: "#e64942", + focusCancel: true, +}); + +export function confirmDanger ($el) { + let swalConfig = {}; + + if ($el.data('confirm-danger')) { + swalConfig.title = $el.data('confirm-danger'); + } + + let buttonText = $el.text(); + if (buttonText) { + swalConfig.confirmButtonText = buttonText; + } + + return swalConfirmDelete.fire(swalConfig); +} + +ready(() => { + $('a.btn-danger,a.btn[data-confirm-danger]').on('click', function (e) { + e.preventDefault(); + + let $el = $(e.target); + if (!$el.is('a')) { + $el = $el.closest('a'); + } + + const linkUrl = $(this).attr('href'); + confirmDanger($el).then((result) => { + if (result.value) { + window.location.href = linkUrl; + } + }); + return false; + }); + + $('button[data-confirm-danger]').on('click', function(e) { + e.preventDefault(); + + let $el = $(e.target); + if (!$el.is('button')) { + $el = $el.closest('button'); + } + + confirmDanger($el).then((result) => { + if (result.value) { + $el.closest('form').submit(); + } + }); + + return false; + }); +}); diff --git a/frontend/layout.js b/frontend/layout.js new file mode 100644 index 0000000..edfffd3 --- /dev/null +++ b/frontend/layout.js @@ -0,0 +1,66 @@ +// Frontend styling +import "~/scss/style.scss"; + +// Frontend JS +import * as bootstrap from "bootstrap"; +import $ from "jquery"; + +// Custom functions +import passwordStrength from './js/passwordStrength.js'; +import {confirmDanger} from './js/sweetalert.js'; +import { + dateTimeToDateTimeString, + dateTimeToRelative, + dateTimeToTimeString, + nowToDateTime, + timestampToDateTime, +} from './js/dateTimeUtils.js'; + +window.bootstrap = bootstrap; + +window.jQuery = window.$ = $; + +window.passwordStrength = passwordStrength; + +window.confirmDanger = confirmDanger; + +window.nowToDateTime = nowToDateTime; +window.timestampToDateTime = timestampToDateTime; +window.dateTimeToDateTimeString = dateTimeToDateTimeString; +window.dateTimeToTimeString = dateTimeToTimeString; +window.dateTimeToRelative = dateTimeToRelative; + +const ready = (callback) => { + if (document.readyState !== "loading") callback(); + else document.addEventListener("DOMContentLoaded", callback); +}; + +ready(() => { + // Toasts + document.querySelectorAll('.toast-notification').forEach((el) => { + const toast = new bootstrap.Toast(el); + toast.show(); + }); + + // Trigger the radio popup. + let radioPopup = null; + document.querySelectorAll('[data-radio-popup]').forEach((el) => { + el.addEventListener("click", (e) => { + e.preventDefault(); + + if (radioPopup == null || radioPopup.closed) { + radioPopup = window.open( + el.href, + "WaterWolfRadio", + "resizable,scrollbars,status,width=550,height=800" + ); + } else { + radioPopup.focus(); + } + + return false; + }); + }); +}); + +export default bootstrap; diff --git a/frontend/scss/_legacy.scss b/frontend/scss/_legacy.scss new file mode 100644 index 0000000..cad13bc --- /dev/null +++ b/frontend/scss/_legacy.scss @@ -0,0 +1,66 @@ +$flare: rgb(0, 175, 255); +$content-bg: rgb(52, 58, 64); + +.ts-team { + .team-member-container { + position: relative; + z-index: 0; + text-decoration: none !important; + + &:hover { + z-index: 50; + } + } + + .team-member { + z-index: 0; + transition: all 0.4s; + + .card { + transition: all 0.4s; + border-radius: 10px; + + img { + background-color: #000; + border-radius: 10px; + } + + h4 { + transition: all 0.4s; + font-size: 2.4em; + } + + &:hover { + h4 { + transition: all 0.4s; + font-size: 2.8em; + } + + background-color: lighten($content-bg, 20%); + } + } + + &:hover { + transform: rotate(2deg) scale(1.3); + box-shadow: 0px 0px 50px rgba(0, 0, 0, 0.7); + } + } +} + +.ts-profile-picture { + background-position: center center; + background-repeat: no-repeat; + background-size: 100%; + background-color: #000; + box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.4); + outline: 3px solid #FFF; + height: 200px; + width: 200px; + border-radius: 1000px; + transition: all 0.3s ease; + + &:hover { + background-size: 120%; + transition: all 0.3s ease; + } +} diff --git a/frontend/scss/_variables.scss b/frontend/scss/_variables.scss new file mode 100644 index 0000000..251cd4b --- /dev/null +++ b/frontend/scss/_variables.scss @@ -0,0 +1,27 @@ +// Bootstrap customization variables + +$body-color-dark: #ECECEC; + +$card-bg: #222; + +$headings-font-family: "Boogaloo", cursive; +$font-family-base: "Noto Sans", sans-serif; + +$header-nav-height: 5rem; +$navbar-dark-color: #DDD; +$navbar-dark-active-color: #FFF; +$navbar-dark-hover-color: #FFF; +$navbar-nav-link-padding-x: .5rem; + +$offcanvas-border-width: 0; +$offcanvas-bg-color: var(--bs-tertiary-bg); +$offcanvas-padding-x: 0; +$offcanvas-padding-y: 0; + +$navdrawer-width: 17.5rem; +$navdrawer-gutter-width: 3.5rem; +$navdrawer-inner-spacer-x: 1rem; +$navdrawer-inner-spacer-y: 0.5rem; +$navdrawer-icon-width: 2rem; +$navdrawer-link-height: 3rem; +$navdrawer-subheader-height: 3rem; diff --git a/frontend/scss/components/_badges.scss b/frontend/scss/components/_badges.scss new file mode 100644 index 0000000..5e68d70 --- /dev/null +++ b/frontend/scss/components/_badges.scss @@ -0,0 +1,9 @@ +.badges { + & > * { + margin-right: .25rem; + } + + & > *:last-child { + margin-right: 0; + } +} diff --git a/frontend/scss/components/_body.scss b/frontend/scss/components/_body.scss new file mode 100644 index 0000000..e7ac680 --- /dev/null +++ b/frontend/scss/components/_body.scss @@ -0,0 +1,26 @@ +body { + background-image: url('/static/img/awoo.webp'); + background-attachment: fixed; + background-size: contain; + background-position: top right; + background-repeat: no-repeat; +} + +main#main { + padding-top: $header-nav-height; + + @include media-breakpoint-down(sm) { + & { + padding-top: ($container-padding-x * .5); + } + } + + div.content, div.page { + // Fix for BG video. + position: relative; + } + + div.page { + margin-top: 1rem; + } +} diff --git a/frontend/scss/components/_buttons.scss b/frontend/scss/components/_buttons.scss new file mode 100644 index 0000000..9d229e9 --- /dev/null +++ b/frontend/scss/components/_buttons.scss @@ -0,0 +1,9 @@ +.buttons { + & > * { + margin-right: .75rem; + } + + & > *:last-child { + margin-right: 0; + } +} diff --git a/frontend/scss/components/_footer.scss b/frontend/scss/components/_footer.scss new file mode 100644 index 0000000..0831461 --- /dev/null +++ b/frontend/scss/components/_footer.scss @@ -0,0 +1,21 @@ +footer#footer { + position: relative; + z-index: 10; + background-color: #006CA2 !important; + + color: #FFF !important; + text-shadow: 1px 1px #000; + + font-size: $font-size-sm; + + a, a:focus, a:active { + color: #ffffff; + } + + .footer-icons { + a { + padding: 0 1rem; + font-size: $font-size-lg * 2; + } + } +} diff --git a/frontend/scss/components/_header.scss b/frontend/scss/components/_header.scss new file mode 100644 index 0000000..cd4ad60 --- /dev/null +++ b/frontend/scss/components/_header.scss @@ -0,0 +1,54 @@ +header .navbar { + min-height: $header-nav-height; + + background-color: #006CA2 !important; + + font-weight: bold; + text-shadow: 1px 1px #000; + box-shadow: 2px 2px rgba(0, 0, 0, 0.1); + + .nav-link, + .btn { + white-space: nowrap; + } + + i { + padding-right: 0.25rem; + } + + @include media-breakpoint-up(lg) { + background-image: url('/static/img/header_left.png'), url('/static/img/header_right.png'); + background-position: left bottom, right bottom; + background-repeat: no-repeat, no-repeat; + + .navbar-brand { + padding-right: 3rem; + } + } + + @include media-breakpoint-only(xl) { + & { + --bs-nav-link-font-size: .9rem; + + .btn { + --bs-btn-font-size: .9rem; + } + } + } + + @include media-breakpoint-only(lg) { + & { + --bs-nav-link-font-size: .8rem; + + .btn { + --bs-btn-font-size: .8rem; + } + } + } + + @include media-breakpoint-down(sm) { + & { + position: relative; + } + } +} diff --git a/frontend/scss/components/_navdrawer.scss b/frontend/scss/components/_navdrawer.scss new file mode 100644 index 0000000..08572cd --- /dev/null +++ b/frontend/scss/components/_navdrawer.scss @@ -0,0 +1,99 @@ +.offcanvas.navdrawer { + width: $navdrawer-width; + box-shadow: 0 0 4px 0 rgba(0, 0, 0, .14), 0 3px 4px 0 rgba(0, 0, 0, .12), 0 1px 5px 0 rgba(0, 0, 0, .2); +} + +@include media-breakpoint-up(lg) { + #main.has-sidebar, + #footer.has-sidebar { + margin-left: $navdrawer-width; + } + + .offcanvas-backdrop { + display: none; + } + + .offcanvas.navdrawer { + transform: none; + visibility: visible; + top: $header-nav-height; + z-index: auto; + } +} + +// Content +.navdrawer-content { + width: 100%; +} + +// Misc +.navdrawer-body { + margin-bottom: $navdrawer-inner-spacer-y; + padding-right: $navdrawer-inner-spacer-x; + padding-left: $navdrawer-inner-spacer-x; +} + +.navdrawer-header { + display: block; + border-bottom: 1px solid rgba(0, 0, 0, .2); + padding: $navdrawer-inner-spacer-y $navdrawer-inner-spacer-x; + + background-color: var(--bs-secondary-bg); + + .navbar-brand { + font-size: 1.25rem; + } +} + +// Nav +.navdrawer-nav { + display: flex; + flex-direction: column; + list-style: none; + margin-top: $navdrawer-inner-spacer-y; + margin-bottom: $navdrawer-inner-spacer-y; + padding-left: 0; + + .navdrawer-header + & { + margin-top: 0; + } + + .nav-link { + @include text-truncate; + + display: inline-flex; + align-items: center; + width: 100%; + + font-size: 1rem; + font-weight: 500; + + line-height: 1; + padding: .75rem 1rem; + + color: var(--bs-body-color); + + & > i { + width: 1rem; + margin-right: .75rem; + } + + &:focus { + outline: 0; + } + + &.active, + &:active { + color: var(--bs-primary); + } + + &:hover { + background-color: var(--bs-secondary-bg); + } + + &.disabled { + background-color: transparent; + color: var(--bs-nav-link-disabled-color); + } + } +} diff --git a/frontend/scss/components/_scrollbar.scss b/frontend/scss/components/_scrollbar.scss new file mode 100644 index 0000000..ef54cf6 --- /dev/null +++ b/frontend/scss/components/_scrollbar.scss @@ -0,0 +1,15 @@ +::-webkit-scrollbar { + width: 5px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; +} + +::-webkit-scrollbar-thumb { + background: #888; +} + +::-webkit-scrollbar-thumb:hover { + background: #555; +} diff --git a/frontend/scss/components/_toasts.scss b/frontend/scss/components/_toasts.scss new file mode 100644 index 0000000..d6eae3d --- /dev/null +++ b/frontend/scss/components/_toasts.scss @@ -0,0 +1,3 @@ +div.toast-container.top-0 { + top: $header-nav-height !important; +} diff --git a/frontend/scss/components/_video.scss b/frontend/scss/components/_video.scss new file mode 100644 index 0000000..a469399 --- /dev/null +++ b/frontend/scss/components/_video.scss @@ -0,0 +1,16 @@ +video.bg-video { + display: none; + + @include media-breakpoint-up(md) { + & { + display: block; + position: fixed; + object-fit: cover; + right: 0; + bottom: 0; + width: 100vw; + height: 100vh; + z-index: 0; + } + } +} diff --git a/frontend/scss/pages/_foxxcon.scss b/frontend/scss/pages/_foxxcon.scss new file mode 100644 index 0000000..50a500e --- /dev/null +++ b/frontend/scss/pages/_foxxcon.scss @@ -0,0 +1,126 @@ +.fxcn { + #header-video { + position: fixed; + z-index: -10; + object-fit: cover; + width: 100%; + height: 100vh; + } + + .cuntdown { + font-size: 1.3em; + width: 100vw; + margin-left: 0px; + color: white; + font-family: 'Noto Sans', sans-serif; + text-align: center; + animation-name: fadeup; + animation-duration: 2s; + animation-delay: 3s; + animation-fill-mode: backwards; + + .numbers { + font-weight: 700; + font-size: 3.2em; + + } + } + + @keyframes fadeup { + from { + opacity: 0 + } + to { + opacity: 1 + } + } + + .fxcn-logo { + width: 50vw; + margin-left: 25vw; + animation-name: fadeup; + animation-duration: 1s; + animation-delay: 0s; + animation-fill-mode: backwards; + + .f { + animation-name: fadeup; + animation-duration: 1s; + animation-delay: 0.4s; + animation-fill-mode: backwards; + } + + .o { + animation-name: fadeup; + animation-duration: 1s; + animation-delay: 0.7s; + animation-fill-mode: backwards; + } + + .x { + animation-name: fadeup; + animation-duration: 1s; + animation-delay: 1s; + animation-fill-mode: backwards; + } + + .x2 { + animation-name: fadeup; + animation-duration: 1s; + animation-delay: 1.3s; + animation-fill-mode: backwards; + } + + .c { + animation-name: fadeup; + animation-duration: 1s; + animation-delay: 1.6s; + animation-fill-mode: backwards; + } + + .o2 { + animation-name: fadeup; + animation-duration: 1s; + animation-delay: 1.9s; + animation-fill-mode: backwards; + } + + .n { + animation-name: fadeup; + animation-duration: 1s; + animation-delay: 2.1s; + animation-fill-mode: backwards; + } + } + + .fxcn-cyberpunk { + width: 70vw; + margin-left: 15vw; + animation-name: fadeup; + animation-duration: 2s; + animation-delay: 2.8s; + animation-fill-mode: backwards; + + } + + .fxcn-buttons { + animation-name: fadeup; + animation-duration: 2s; + animation-delay: 3.5s; + animation-fill-mode: backwards; + margin-bottom: 20vh; + + a { + background-color: rgba(0, 127, 255, 0.5); + border: solid 2px rgb(0, 200, 255); + box-shadow: inset 0px 0px 10px rgba(0, 200, 255, 0.6); + } + + text-align: center; + + } + + .content { + z-index: 69420; + } +} diff --git a/frontend/scss/style.scss b/frontend/scss/style.scss new file mode 100644 index 0000000..3387bf4 --- /dev/null +++ b/frontend/scss/style.scss @@ -0,0 +1,26 @@ +@import 'https://fonts.googleapis.com/css2?family=Boogaloo&family=Noto+Sans:wght@300;700&display=swap'; + +// Bootstrap +@import '_variables'; +@import 'bootstrap/scss/bootstrap'; + +// Bootstrap Icons +@import 'bootstrap-icons/font/bootstrap-icons.css'; + +// Vendor libraries +@import 'vendors/_sweetalert2'; + +// Custom imports +@import 'components/_badges'; +@import 'components/_body'; +@import 'components/_buttons'; +@import 'components/_header'; +@import 'components/_footer'; +@import 'components/_navdrawer'; +@import 'components/_toasts'; +@import 'components/_video'; + +// Pages +@import 'pages/_foxxcon'; + +@import '_legacy'; diff --git a/frontend/scss/vendors/_sweetalert2.scss b/frontend/scss/vendors/_sweetalert2.scss new file mode 100644 index 0000000..0ad8130 --- /dev/null +++ b/frontend/scss/vendors/_sweetalert2.scss @@ -0,0 +1,40 @@ +@import 'sweetalert2/src/variables'; + +$swal2-dark-theme-black: #19191a; +$swal2-dark-theme-white: #e1e1e1; + +$swal2-background: var(--bs-body-bg); +$swal2-border: 1px solid var(--bs-tertiary-color); +$swal2-title-color: var(--bs-body-color); +$swal2-backdrop: rgba(var(--bs-tertiary-bg-rgb), .75); + +// FOOTER +$swal2-footer-border-color: var(--bs-table-border-color); +$swal2-footer-color: darken($swal2-dark-theme-black, 15%); + +// TIMER POGRESS BAR +$swal2-timer-progress-bar-background: rgba(var(--bs-body-color-rgb), .6); + +// INPUT +$swal2-input-color: var(--bs-input-color); +$swal2-input-background: transparent; +$swal2-input-border: 1px solid var(--bs-border-color); + +// VALIDATION MESSAGE +$swal2-validation-message-background: lighten($swal2-dark-theme-black, 10%); +$swal2-validation-message-color: $swal2-dark-theme-white; + +// QUEUE +$swal2-progress-step-background: lighten($swal2-dark-theme-black, 25%); + +// COMMON VARIABLES FOR CONFIRM AND CANCEL BUTTONS +$swal2-button-focus-box-shadow: 0 0 0 1px $swal2-background, 0 0 0 3px $swal2-outline-color; + +// TOAST +$swal2-toast-background: $swal2-dark-theme-black; +$swal2-toast-button-focus-box-shadow: 0 0 0 1px $swal2-background, 0 0 0 3px $swal2-outline-color; + +// CANCEL BUTTON +$swal2-cancel-button-background-color: $gray-600; + +@import 'sweetalert2/src/sweetalert2'; diff --git a/frontend/scss/wwradio.scss b/frontend/scss/wwradio.scss new file mode 100644 index 0000000..06a864c --- /dev/null +++ b/frontend/scss/wwradio.scss @@ -0,0 +1,396 @@ +@import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;700&display=swap'); + +//---------------------------------- +$theme-a: rgb(131, 131, 131); +$theme-b: rgb(68, 68, 68); +$theme-c: #fff; + +$black: #000000; +$text-grey: #888; +$white: #ffffff; +$becki: rgba(93, 0, 180, 1); +//---------------------------------- + +* { + margin: 0; + padding: 0; +} + +body { + background-color: $black; + overflow: auto; + font-family: 'Rajdhani', sans-serif !important; + font-size: 1.4em; +} + +a { + color: $white; +} + +//scrollbar gen'd bc im lazy... +body { + --sb-track-color: #000000; + --sb-thumb-color: #ffffff; + --sb-size: 10px; + + scrollbar-color: var(--sb-thumb-color) var(--sb-track-color); +} + +body::-webkit-scrollbar { + width: var(--sb-size); +} + +body::-webkit-scrollbar-track { + background: var(--sb-track-color); + border-radius: 2px; +} + +body::-webkit-scrollbar-thumb { + background: var(--sb-thumb-color); + border-radius: 2px; +} + +//end of scrollbar + +#player-itself { + width: 100%; + text-align: center; + height: 100px; + margin-bottom: 100px; +} + +.dr-normal { + background-color: $black; + max-width: 500px; + width: 500px; + margin: 0 auto; + + .logo { + img { + width: 400px; + } + + width: 500px; + margin-left: 50px; + padding-top: 10px; + } + + .now-playing-box { + width: 100%; + color: $text-grey; + font-weight: 700; + font-size: 1.4em; + margin-top: 15px; + margin-bottom: 15px; + text-align: center; + background-color: $black; + z-index: 100; + position: relative; + + border-bottom: solid 1px rgb(40, 40, 40); + + background-image: url("/static/wwradio/player-track-bg.png"); + background-size: 100%, 100%; + background-repeat: no-repeat; + background-position: center bottom; + + #np-artist { + color: $white; + font-weight: 700; + } + + #np-title { + color: $white; + font-weight: 400; + } + + p { + display: inline-block; + margin-right: 5px; + } + } + + .background-box { + background-image: url('/static/wwradio/no-art.jpg'); + position: absolute; + width: 550px; + height: 450px; + margin-top: -75px; + margin-left: -25px; + z-index: 99; + background-size: cover; + background-position: center center; + opacity: 0.5; + filter: blur(10px) saturate(2); + transition: all 5s; + } + + .art-shadow { + width: 300px; + height: 0; + margin-left: 100px; + margin-top: -50px; + opacity: 1; + box-shadow: 0 0 12px 2px rgba(180, 180, 180, 0.4); + + z-index: 98; + background-color: rgba(0, 0, 0, 0); + } + + .album-art { + margin-left: 100px; + margin-top: 50px; + height: 300px; + width: 300px; + background-position: center center; + background-size: cover; + background-color: $theme-c; + background-image: url('/static/wwradio/no-art.jpg'); + transition: all 3s; + z-index: 100; + position: relative; + } + + .cc_recenttracks_list { + position: relative; + margin-top: 115px; + z-index: 100; + width: 500px; + + .cctrack { + width: 500px; + padding-top: 10px; + padding-bottom: 10px; + color: $theme-c; + background-image: url("/static/wwradio/player-track-bg.png"); + background-size: 100%, 100%; + background-repeat: no-repeat; + background-position: center center; + } + } + + .cctitle { + color: $theme-c !important; + text-shadow: 0 2px 6px rgba(0, 0, 0, 0.4); + } + + .cccover { + padding: 0 !important; + background-color: #000 !important; + border: none !important; + float: left; + margin-right: 15px !important; + clear: left; + } + + .ccbuy, .ccalbum { + display: none; + + } + + .ccartist { + color: $text-grey !important; + } + + .sent { + height: 50px !important; + } + + .trb-button { + background-color: rgba(0, 0, 0, 0.5); + position: relative; + display: block; + top: 40px; + left: 5px; + z-index: 105; + width: 50px; + height: 50px; + line-height: 50px; + border-radius: 50px; + color: rgba(255, 255, 255, 0.5); + border: solid 1px rgba(255, 255, 255, 0.2); + font-size: 2em; + transform: rotate(-90deg); + text-align: center; + float: left; + text-decoration: none; + transition: all 1s; + } + + .trb-hidden { + transform: rotate(90deg); + transition: all 1s; + } + + .hidden { + height: 0 !important; + transition: all 1s; + margin-bottom: -1px !important; + } + + .request-box { + background-color: $black; + border-top: solid 1px rgb(40, 40, 40); + margin-top: 110px; + margin-bottom: 10px; + position: relative; + height: 240px; + z-index: 100; + width: 500px; + transition: all 1s; + + .cc_req_result_mvinaround { + display: none !important; + } + + #song-sent-text { + height: 0; + border-bottom: solid 0 rgb(0, 70, 0); + color: rgb(80, 200, 80); + font-size: 20px; + line-height: 40px; + text-align: center; + overflow: hidden; + transition: all 1s; + } + + .show-text { + height: 40px !important; + border-bottom: solid 1px rgb(0, 70, 0) !important; + transition: all 1s; + } + + form { + font-family: 'Rajdhani', sans-serif !important; + + label { + color: $theme-c; + display: inline-block; + width: 30%; + } + + input[type="text"], input[type="email"] { + border: none; + border-bottom: solid 1px $text-grey; + background-color: $black; + padding-top: 2px; + padding-bottom: 2px; + margin-top: 18px; + width: 60%; + color: $theme-c; + transition: all 0.5s; + + &:hover, &:focus { + background-color: rgb(40, 40, 40); + outline: none; + box-shadow: 0px -4px 10px 5px rgb(40, 40, 40); + transition: all 0.5s; + } + } + + .req-button { + text-align: right; + } + + input[type="button"] { + margin-top: 18px; + height: 40px; + width: 50%; + margin-left: 50%; + text-align: center; + position: relative; + display: block; + font-size: 20px; + font-family: 'Rajdhani', sans-serif !important; + border: none; + border-bottom: solid 4px rgb(80, 80, 80); + background-color: $text-grey; + color: $theme-c; + + &:active { + margin-top: 23px; + border-bottom: 0; + margin-bottom: -5px; + outline: none; + } + + } + } + } +} + +.server-details { + width: 60%; + margin: 20%; + border: solid 3px $theme-a; + background-color: $becki; + color: $white; + border-radius: 5px; + font-size: 1sem; + + h2 { + width: 100%; + background-color: $theme-a; + color: $theme-c; + + } + + #logo { + width: 80%; + margin-left: 10%; + margin-bottom: 20px; + } + + p { + font-weight: 700; + font-style: italic; + padding-top: 20px; + } + + section { + padding: 5%; + + input[type="password"] { + font-size: 20px; + padding: 20px; + border: solid 2px $theme-a; + } + + input[type="submit"] { + font-size: 20px; + background-color: rgb(100, 255, 100); + color: rgb(0, 130, 0); + border: none; + border-bottom: solid 5px rgb(0, 200, 0); + padding: 20px; + + transition: all 0.3s; + } + + table { + width: 100%; + border: none; + border-radius: 3px; + font-size: 1em; + + th { + background-color: $theme-b; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 5px; + text-align: left; + color: $white; + } + + td { + padding-top: 5px; + padding-left: 5px; + padding-bottom: 5px; + border: none; + margin: 0; + border-bottom: solid 1px rgba(0, 0, 0, 0.2); + color: $white; + } + } + } +} diff --git a/frontend/vue/comments.js b/frontend/vue/comments.js new file mode 100644 index 0000000..4361012 --- /dev/null +++ b/frontend/vue/comments.js @@ -0,0 +1,7 @@ +import initApp from "~/vue/initApp"; +import Comments from "~/vue/components/Comments.vue"; + +initApp( + 'Comments', + Comments +); diff --git a/frontend/vue/components/Comments.vue b/frontend/vue/components/Comments.vue new file mode 100644 index 0000000..2567a4d --- /dev/null +++ b/frontend/vue/components/Comments.vue @@ -0,0 +1,132 @@ + + + diff --git a/frontend/vue/components/Common/Toast.vue b/frontend/vue/components/Common/Toast.vue new file mode 100644 index 0000000..d1c740f --- /dev/null +++ b/frontend/vue/components/Common/Toast.vue @@ -0,0 +1,63 @@ + + + diff --git a/frontend/vue/functions/useNotify.js b/frontend/vue/functions/useNotify.js new file mode 100644 index 0000000..93359e1 --- /dev/null +++ b/frontend/vue/functions/useNotify.js @@ -0,0 +1,77 @@ +import {h, render} from "vue"; +import {default as BSToast} from 'bootstrap/js/src/toast'; + +import Toast from '~/vue/components/Common/Toast.vue'; +import {currentVueInstance} from "~/vue/vendor/vueInstance"; + +export function createToast(props) { + let slot; + if (Array.isArray(props.message)) { + slot = props.message + delete props.message + } + + const defaultSlot = () => { + return slot + }; + + const vNode = h(Toast, props, defaultSlot); + vNode.appContext = currentVueInstance._context; + + const newDiv = document.createElement('div'); + newDiv.style.display = "contents"; + document.querySelector('.toast-container').appendChild(newDiv); + + render(vNode, newDiv); + + return new BSToast(vNode.el); +} + +/* Composition API BootstrapVue utilities */ +export function useNotify() { + const notify = (message = null, options = {}) => { + if (document.hidden) { + return; + } + + const toast = createToast({ + ...options, + message + }); + toast.show(); + }; + + const notifyError = (message = null, options = {}) => { + if (message === null) { + message = 'An error occurred and your request could not be completed.'; + } + + const defaults = { + variant: 'danger' + }; + + notify(message, {...defaults, ...options}); + + return message; + }; + + const notifySuccess = (message = null, options = {}) => { + if (message === null) { + message = 'Changes saved.'; + } + + const defaults = { + variant: 'success' + }; + + notify(message, {...defaults, ...options}); + + return message; + }; + + return { + notify, + notifyError, + notifySuccess + }; +} diff --git a/frontend/vue/initApp.js b/frontend/vue/initApp.js new file mode 100644 index 0000000..0e8332a --- /dev/null +++ b/frontend/vue/initApp.js @@ -0,0 +1,30 @@ +import {createApp, h} from "vue"; +import installAxios from "~/vue/vendor/axios"; +import {installCurrentVueInstance} from "~/vue/vendor/vueInstance.js"; + +export default function initApp( + appName = 'Generic', + appComponent +) { + let componentProps = {}; + + const vueApp = createApp({ + render() { + return h(appComponent, componentProps); + } + }); + + installCurrentVueInstance(vueApp); + + window[`vueComponent${appName}`] = async (el, props = {}) => { + componentProps = props; + + installAxios(vueApp); + + vueApp.mount(el); + }; + + return { + vueApp + }; +} diff --git a/frontend/vue/vendor/axios.js b/frontend/vue/vendor/axios.js new file mode 100644 index 0000000..9ad53fd --- /dev/null +++ b/frontend/vue/vendor/axios.js @@ -0,0 +1,48 @@ +import axios from "axios"; +import VueAxios from "vue-axios"; +import {inject} from "vue"; +import {useNotify} from "~/vue/functions/useNotify.js"; + +export const useAxios = () => inject('axios'); + +export default function installAxios(vueApp) { + + const axiosInstance = axios.create(); + + // Configure some Axios settings that depend on the BootstrapVue $bvToast superglobal. + const handleAxiosError = (error) => { + let notifyMessage = 'An error occurred and your request could not be completed.'; + if (error.response) { + // Request made and server responded + const responseJson = error.response.data ?? {}; + notifyMessage = responseJson.message ?? notifyMessage; + console.error(responseJson); + } else if (error.request) { + // The request was made but no response was received + console.error(error.request); + } else { + // Something happened in setting up the request that triggered an Error + console.error('Error', error.message); + } + + const {notifyError} = useNotify(); + notifyError(notifyMessage); + }; + + axiosInstance.interceptors.request.use((config) => { + return config; + }, (error) => { + handleAxiosError(error); + return Promise.reject(error); + }); + + axiosInstance.interceptors.response.use((response) => { + return response; + }, (error) => { + handleAxiosError(error); + return Promise.reject(error); + }); + + vueApp.use(VueAxios, axiosInstance); + vueApp.provide('axios', axiosInstance); +} diff --git a/frontend/vue/vendor/sweetalert.js b/frontend/vue/vendor/sweetalert.js new file mode 100644 index 0000000..70699f4 --- /dev/null +++ b/frontend/vue/vendor/sweetalert.js @@ -0,0 +1,54 @@ +import Swal from 'sweetalert2/dist/sweetalert2'; + +export function useSweetAlert() { + const swalCustom = Swal.mixin({ + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + showCancelButton: true, + }); + + const swalConfirmDelete = swalCustom.mixin({ + title: 'Delete Record?', + confirmButtonText: 'Delete', + confirmButtonColor: '#e64942', + focusCancel: true + }); + + const showAlert = (options = {}) => { + return swalCustom.fire(options); + } + + const confirmDelete = (options = {}) => { + return swalConfirmDelete.fire(options); + } + + const vConfirmLink = (el, binding) => { + el.addEventListener('click', (e) => { + e.preventDefault(); + + const options = { + title: null + }; + + if (el.hasAttribute('data-confirm-title')) { + options.title = el.getAttribute('data-confirm-title'); + } else if (binding.value) { + options.title = binding.value; + } + + confirmDelete(options).then((resp) => { + if (!resp.value) { + return; + } + + window.location.href = el.href; + }); + }); + }; + + return { + showAlert, + confirmDelete, + vConfirmLink + }; +} diff --git a/frontend/vue/vendor/vueInstance.js b/frontend/vue/vendor/vueInstance.js new file mode 100644 index 0000000..bc7239f --- /dev/null +++ b/frontend/vue/vendor/vueInstance.js @@ -0,0 +1,4 @@ +export let currentVueInstance; +export const installCurrentVueInstance = (vueApp) => { + currentVueInstance = vueApp; +}; diff --git a/frontend/wwradio.js b/frontend/wwradio.js new file mode 100644 index 0000000..b56fef5 --- /dev/null +++ b/frontend/wwradio.js @@ -0,0 +1,138 @@ +// Frontend styling +import "~/scss/wwradio.scss"; + +// Frontend JS +import $ from "jquery"; +window.jQuery = window.$ = $; + +const ready = (callback) => { + if (document.readyState !== "loading") callback(); + else document.addEventListener("DOMContentLoaded", callback); +}; + +const infoUrl = "https://quasar.shoutca.st:2199/external/rpc.php?m=streaminfo.get&username=techsane"; + +function updateStreamInfo(){ + $.getJSON(infoUrl).done(function(data){ + const radioData = data.data[0] ?? {}; + if (!radioData.offline) { + $("#d_online").text("Online") + $("#d_adjonline").text(radioData.autodj) + $("#d_source").text(radioData.source) + $("#d_listeners").text(radioData.listeners + " Out Of " + radioData.maxlisteners) + $("#d_date").text(radioData.date) + $("#d_time").text(radioData.time) + $("#d_song").text(radioData.song) + $("#d_song_raw").text(radioData.rawmeta) + $("#d_stype").text(radioData.servertype) + $("#d_listenurl").text(radioData.tuneinurl).attr("href", radioData.tuneinurl) + } + }); +} + +window.startInfoLoop = function() { + let countdown = 10; + updateStreamInfo(); + setInterval(function(){ + if(countdown === 0) { + updateStreamInfo(); + countdown = 10; + + $("#rf-countdown").text(countdown+"...Refreshed!") + } else { + countdown = countdown - 1 + $("#rf-countdown").text(countdown) + } + },1000) +}; + +// Delta Radio Player 2. Developed for Delta Radio By Jamie Overington (Jamie Overington.co.uk) +// Canvas Visualizations + +let nowPlayingImage = ""; +let nowPlayingArtist = ""; +let nowPlayingTitle = ""; + +function findUrl(image, size) { + let n, entry; + try{ + for (n = 0; n < image.length; ++n) { + entry = image[n]; + if (entry.size === size) { + return entry["#text"]; + } + } + return ""; + } + catch(e){ + return ""; + } +} + +const streamInfoUrl = "https://quasar.shoutca.st:2199/external/rpc.php?m=streaminfo.get&username=techsane"; +const lastFmApiKey = "791f0afe347adc2ba93a3c7317a2810a"; + +function GetCurrentTrack(){ + $.getJSON(streamInfoUrl).done(function(data) { + const radioData = data.data[0] ?? {}; + + if (nowPlayingArtist === radioData.track.artist || nowPlayingTitle === radioData.track.title) { + return; + } + + nowPlayingArtist = radioData.track.artist; + nowPlayingTitle = radioData.track.title; + + $('#np-artist').text(nowPlayingArtist); + $('#np-title').text(nowPlayingTitle); + + if (nowPlayingArtist.length > 0 && nowPlayingTitle.length > 0) { + let lastFmUrl = "https://ws.audioscrobbler.com/2.0/?method=track.getInfo&api_key=" + lastFmApiKey + + "&autocorrect=1&artist=" + encodeURIComponent(nowPlayingArtist) + "&autocorrect=1&track=" + + encodeURIComponent(nowPlayingTitle) + "&format=json"; + + $.getJSON(lastFmUrl).done(function (data) { + console.log("Fetching Track...") + try { + nowPlayingImage = findUrl(data.track.album.image, "large") + if (nowPlayingImage.length > 0) { + $('#np-album-art-box').css("background-image", "url('" + nowPlayingImage + "')"); + $('.background-box').css("background-image", "url('" + nowPlayingImage + "')"); + console.log("Found Track!") + } + } catch (e) { + lastFmUrl = "https://ws.audioscrobbler.com/2.0/?method=artist.getInfo&api_key=" + lastFmApiKey + + "&autocorrect=1&artist=" + encodeURIComponent(nowPlayingArtist) + "&format=json"; + + $.getJSON(lastFmUrl).done(function (data) { + nowPlayingImage = findUrl(data.artist.image, "large") + if (nowPlayingImage.length > 0) { + $('#np-album-art-box').css("background-image", "url('" + nowPlayingImage + "')"); + $('.background-box').css("background-image", "url('" + nowPlayingImage + "')"); + console.log("Couldn't Find track, But found artist.", nowPlayingArtist, nowPlayingTitle) + } else { + console.log("Couldnt Find artist", nowPlayingArtist) + $('#np-album-art-box').css("background-image", " url('files/img/no-art.png')"); + $('.background-box').css("background-image", " url('files/img/no-art.png')"); + console.log("---[ Album Art Grabber ]---") + console.log("Unable to find album art on last.fm") + console.log("--- ------------------- ---") + } + }); + } + }); + } + }); +} + +window.startPlayerLoop = function() { + setTimeout(function(){ + $("video").prop("muted",false).prop("volume",0.5); + }, 1000); + + GetCurrentTrack(); + setTimeout(function(){ + $('#np-artist').text(nowPlayingArtist); + $('#np-title').text(nowPlayingTitle); + },3000) +}; diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..c580b88 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "!/*": [ + "*" + ], + "~/*": [ + "frontend/*" + ] + } + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f60d899 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1290 @@ +{ + "name": "www", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "axios": "^1.6.7", + "bootstrap": "^5.3.2", + "bootstrap-icons": "^1.11.3", + "jquery": "^3.7.1", + "luxon": "^3.4.4", + "sweetalert2": "11.4.8", + "vue": "^3.4.20", + "vue-axios": "^3.5.2" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "sass": "^1.70.0", + "vite": "^5.0.12" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.10.0.tgz", + "integrity": "sha512-/MeDQmcD96nVoRumKUljsYOLqfv1YFJps+0pTrb2Z9Nl/w5qNUysMaWQsrd1mvAlNT4yza1iVyIu4Q4AgF6V3A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.10.0.tgz", + "integrity": "sha512-lvu0jK97mZDJdpZKDnZI93I0Om8lSDaiPx3OiCk0RXn3E8CMPJNS/wxjAvSJJzhhZpfjXsjLWL8LnS6qET4VNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.10.0.tgz", + "integrity": "sha512-uFpayx8I8tyOvDkD7X6n0PriDRWxcqEjqgtlxnUA/G9oS93ur9aZ8c8BEpzFmsed1TH5WZNG5IONB8IiW90TQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.10.0.tgz", + "integrity": "sha512-nIdCX03qFKoR/MwQegQBK+qZoSpO3LESurVAC6s6jazLA1Mpmgzo3Nj3H1vydXp/JM29bkCiuF7tDuToj4+U9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.10.0.tgz", + "integrity": "sha512-Fz7a+y5sYhYZMQFRkOyCs4PLhICAnxRX/GnWYReaAoruUzuRtcf+Qnw+T0CoAWbHCuz2gBUwmWnUgQ67fb3FYw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.10.0.tgz", + "integrity": "sha512-yPtF9jIix88orwfTi0lJiqINnlWo6p93MtZEoaehZnmCzEmLL0eqjA3eGVeyQhMtxdV+Mlsgfwhh0+M/k1/V7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.10.0.tgz", + "integrity": "sha512-9GW9yA30ib+vfFiwjX+N7PnjTnCMiUffhWj4vkG4ukYv1kJ4T9gHNg8zw+ChsOccM27G9yXrEtMScf1LaCuoWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.10.0.tgz", + "integrity": "sha512-X1ES+V4bMq2ws5fF4zHornxebNxMXye0ZZjUrzOrf7UMx1d6wMQtfcchZ8SqUnQPPHdOyOLW6fTcUiFgHFadRA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.10.0.tgz", + "integrity": "sha512-w/5OpT2EnI/Xvypw4FIhV34jmNqU5PZjZue2l2Y3ty1Ootm3SqhI+AmfhlUYGBTd9JnpneZCDnt3uNOiOBkMyw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.10.0.tgz", + "integrity": "sha512-q/meftEe3QlwQiGYxD9rWwB21DoKQ9Q8wA40of/of6yGHhZuGfZO0c3WYkN9dNlopHlNT3mf5BPsUSxoPuVQaw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.10.0.tgz", + "integrity": "sha512-NrR6667wlUfP0BHaEIKgYM/2va+Oj+RjZSASbBMnszM9k+1AmliRjHc3lJIiOehtSSjqYiO7R6KLNrWOX+YNSQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.10.0.tgz", + "integrity": "sha512-FV0Tpt84LPYDduIDcXvEC7HKtyXxdvhdAOvOeWMWbQNulxViH2O07QXkT/FffX4FqEI02jEbCJbr+YcuKdyyMg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.10.0.tgz", + "integrity": "sha512-OZoJd+o5TaTSQeFFQ6WjFCiltiYVjIdsXxwu/XZ8qRpsvMQr4UsVrE5UyT9RIvsnuF47DqkJKhhVZ2Q9YW9IpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz", + "integrity": "sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.4.20", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.20.tgz", + "integrity": "sha512-l7M+xUuL8hrGtRLkrf+62d9zucAdgqNBTbJ/NufCOIuJQhauhfyAKH9ra/qUctCXcULwmclGAVpvmxjbBO30qg==", + "dependencies": { + "@babel/parser": "^7.23.9", + "@vue/shared": "3.4.20", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.4.20", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.20.tgz", + "integrity": "sha512-/cSBGL79HFBYgDnqCNKErOav3bPde3n0sJwJM2Z09rXlkiowV/2SG1tgDAiWS1CatS4Cvo0o74e1vNeCK1R3RA==", + "dependencies": { + "@vue/compiler-core": "3.4.20", + "@vue/shared": "3.4.20" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.4.20", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.20.tgz", + "integrity": "sha512-nPuTZz0yxTPzjyYe+9nQQsFYImcz/57UX8N3jyhl5oIUUs2jqqAMaULsAlJwve3qNYfjQzq0bwy3pqJrN9ecZw==", + "dependencies": { + "@babel/parser": "^7.23.9", + "@vue/compiler-core": "3.4.20", + "@vue/compiler-dom": "3.4.20", + "@vue/compiler-ssr": "3.4.20", + "@vue/shared": "3.4.20", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.7", + "postcss": "^8.4.35", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.4.20", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.20.tgz", + "integrity": "sha512-b3gFQPiHLvI12C56otzBPpQhZ5kgkJ5RMv/zpLjLC2BIFwX5GktDqYQ7xg0Q2grP6uFI8al3beVKvAVxFtXmIg==", + "dependencies": { + "@vue/compiler-dom": "3.4.20", + "@vue/shared": "3.4.20" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.4.20", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.20.tgz", + "integrity": "sha512-P5LJcxUkG6inlHr6MHVA4AVFAmRYJQ7ONGWJILNjMjoYuEXFhYviSCb9BEMyszSG/1kWCZbtWQlKSLasFRpThw==", + "dependencies": { + "@vue/shared": "3.4.20" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.4.20", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.20.tgz", + "integrity": "sha512-MPvsQpGAxoBqLHjqopt4YPtUYBpq0K6oAWDTwIR1CTNZ3y9O/J2ZVh+i2JpxKNYwANJBiZ20O99NE20uisB7xw==", + "dependencies": { + "@vue/reactivity": "3.4.20", + "@vue/shared": "3.4.20" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.4.20", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.20.tgz", + "integrity": "sha512-OkbPVP69H+8m74543zMAAx/LIkajxufYyow41gc0s5iF0uplT5uTQ4llDYu1GeJZEI8wjL5ueiPQruk4qwOMmA==", + "dependencies": { + "@vue/runtime-core": "3.4.20", + "@vue/shared": "3.4.20", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.4.20", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.20.tgz", + "integrity": "sha512-w3VH2GuwxQHA6pJo/HCV22OfVC8Mw4oeHQM+vKeqtRK0OPE1Wilnh+P/SDVGGxPjJsGmyfphi0dbw8UKZQJH9w==", + "dependencies": { + "@vue/compiler-ssr": "3.4.20", + "@vue/shared": "3.4.20" + }, + "peerDependencies": { + "vue": "3.4.20" + } + }, + "node_modules/@vue/shared": { + "version": "3.4.20", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.20.tgz", + "integrity": "sha512-KTEngal0aiUvNJ6I1Chk5Ew5XqChsFsxP4GKAYXWb99zKJWjNU72p2FWEOmZWHxHcqtniOJsgnpd3zizdpfEag==" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bootstrap": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.2.tgz", + "integrity": "sha512-D32nmNWiQHo94BKHLmOrdjlL05q1c8oxbtBphQFb9Z5to6eGRDCm0QgeaZ4zFBHzfg2++rqa2JkqCcxDy0sH0g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/bootstrap-icons": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz", + "integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ] + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/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, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/immutable": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" + }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string": { + "version": "0.30.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", + "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rollup": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.10.0.tgz", + "integrity": "sha512-t2v9G2AKxcQ8yrG+WGxctBes1AomT0M4ND7jTFBCVPXQ/WFTvNSefIrNSmLKhIKBrvN8SG+CZslimJcT3W2u2g==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.10.0", + "@rollup/rollup-android-arm64": "4.10.0", + "@rollup/rollup-darwin-arm64": "4.10.0", + "@rollup/rollup-darwin-x64": "4.10.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.10.0", + "@rollup/rollup-linux-arm64-gnu": "4.10.0", + "@rollup/rollup-linux-arm64-musl": "4.10.0", + "@rollup/rollup-linux-riscv64-gnu": "4.10.0", + "@rollup/rollup-linux-x64-gnu": "4.10.0", + "@rollup/rollup-linux-x64-musl": "4.10.0", + "@rollup/rollup-win32-arm64-msvc": "4.10.0", + "@rollup/rollup-win32-ia32-msvc": "4.10.0", + "@rollup/rollup-win32-x64-msvc": "4.10.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/sass": { + "version": "1.70.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz", + "integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sweetalert2": { + "version": "11.4.8", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.4.8.tgz", + "integrity": "sha512-BDS/+E8RwaekGSxCPUbPnsRAyQ439gtXkTF/s98vY2l9DaVEOMjGj1FaQSorfGREKsbbxGSP7UXboibL5vgTMA==", + "funding": { + "type": "individual", + "url": "https://sweetalert2.github.io/#donations" + } + }, + "node_modules/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, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/vite": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.1.tgz", + "integrity": "sha512-wclpAgY3F1tR7t9LL5CcHC41YPkQIpKUGeIuT8MdNwNZr6OqOTLs7JX5vIHAtzqLWXts0T+GDrh9pN2arneKqg==", + "dev": true, + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.35", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.4.20", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.20.tgz", + "integrity": "sha512-xF4zDKXp67NjgORFX/HOuaiaKYjgxkaToK0KWglFQEYlCw9AqgBlj1yu5xa6YaRek47w2IGiuvpvrGg/XuQFCw==", + "dependencies": { + "@vue/compiler-dom": "3.4.20", + "@vue/compiler-sfc": "3.4.20", + "@vue/runtime-dom": "3.4.20", + "@vue/server-renderer": "3.4.20", + "@vue/shared": "3.4.20" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-axios": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/vue-axios/-/vue-axios-3.5.2.tgz", + "integrity": "sha512-GP+dct7UlAWkl1qoP3ppw0z6jcSua5/IrMpjB5O8bh089iIiJ+hdxPYH2NPEpajlYgkW5EVMP95ttXWdas1O0g==", + "peerDependencies": { + "axios": "*", + "vue": "^3.0.0 || ^2.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..668fbde --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "type": "module", + "dependencies": { + "axios": "^1.6.7", + "bootstrap": "^5.3.2", + "bootstrap-icons": "^1.11.3", + "jquery": "^3.7.1", + "luxon": "^3.4.4", + "sweetalert2": "11.4.8", + "vue": "^3.4.20", + "vue-axios": "^3.5.2" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "sass": "^1.70.0", + "vite": "^5.0.12" + }, + "scripts": { + "build": "vite build", + "serve": "vite" + } +} diff --git a/phinx.php b/phinx.php new file mode 100644 index 0000000..93b7c96 --- /dev/null +++ b/phinx.php @@ -0,0 +1,29 @@ +getDatabaseInfo(); + +$di = App\AppFactory::buildContainer($env); +$db = $di->get(Doctrine\DBAL\Connection::class); + +return + [ + 'paths' => [ + 'migrations' => '%%PHINX_CONFIG_DIR%%/db/migrations', + 'seeds' => '%%PHINX_CONFIG_DIR%%/db/seeds', + ], + 'environments' => [ + 'default_migration_table' => 'migrations', + 'default_environment' => 'db', + 'db' => [ + 'name' => $dbInfo['dbname'], + 'connection' => $db->getNativeConnection(), + ], + ], + 'version_order' => 'creation', + ]; diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..40e02eb --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,32 @@ + + + The WaterWolf PHP coding standard. + + backend + web + + + + + + + + + + + + + + + + + + 0 + + + + + + + + diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..0fb3dbd --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,17 @@ +parameters: + level: 6 + + paths: + - backend + - web + + fileExtensions: + - php + - phtml + + dynamicConstantNames: + - GLOBAL_URL + - GLOBAL_SITECODE + - GLOBAL_URI_WITHOUT_QUERY + + checkMissingIterableValueType: false diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..9a8687a --- /dev/null +++ b/vite.config.js @@ -0,0 +1,41 @@ +import {defineConfig} from "vite"; +import {resolve} from "path"; +import vue from '@vitejs/plugin-vue'; + +const inputs = { + "layout": resolve(__dirname, "./frontend/layout.js"), + "wwradio": resolve(__dirname, "./frontend/wwradio.js"), + "vue_comments": resolve(__dirname, "./frontend/vue/comments.js") +}; + +console.log(inputs); + +// https://vitejs.dev/config/ +export default defineConfig({ + base: "/static/dist", + build: { + rollupOptions: { + input: inputs + }, + manifest: true, + emptyOutDir: true, + chunkSizeWarningLimit: "1m", + outDir: resolve(__dirname, "./web/static/dist"), + }, + server: { + strictPort: true, + fs: { + allow: ["."], + }, + }, + plugins: [ + vue(), + ], + resolve: { + alias: { + "!": resolve(__dirname), + "~": resolve(__dirname, "./frontend"), + }, + extensions: [".mjs", ".js", ".mts", ".ts", ".jsx", ".tsx", ".json", ".vue"], + }, +}); diff --git a/web/ads.txt b/web/ads.txt new file mode 100644 index 0000000..7f0f80b --- /dev/null +++ b/web/ads.txt @@ -0,0 +1 @@ +google.com, pub-3077649647714722, DIRECT, f08c47fec0942fa0 \ No newline at end of file diff --git a/web/browserconfig.xml b/web/browserconfig.xml new file mode 100644 index 0000000..c51ba72 --- /dev/null +++ b/web/browserconfig.xml @@ -0,0 +1,11 @@ + + + + + + + + #ffffff + + + diff --git a/web/favicon.ico b/web/favicon.ico new file mode 100644 index 0000000..b0c7c97 Binary files /dev/null and b/web/favicon.ico differ diff --git a/web/index.php b/web/index.php new file mode 100644 index 0000000..f553356 --- /dev/null +++ b/web/index.php @@ -0,0 +1,6 @@ +run(); diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..62f4308 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,47 @@ +{ + "short_name": "waterwolf.club", + "name": "EN", + "start_url": "/", + "gcm_sender_id": "1073325491958", + "icons": [ + { + "src": "/static/img/favicon/android-icon-36x36.png", + "sizes": "36x36", + "type": "image/png", + "density": "0.75" + }, + { + "src": "/static/img/favicon/android-icon-48x48.png", + "sizes": "48x48", + "type": "image/png", + "density": "1.0" + }, + { + "src": "/static/img/favicon/android-icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "density": "1.5" + }, + { + "src": "/static/img/favicon/android-icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "density": "2.0" + }, + { + "src": "/static/img/favicon/android-icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "density": "3.0" + }, + { + "src": "/static/img/favicon/android-icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "density": "4.0" + } + ], + "background_color": "#F6F7F8", + "theme_color": "#A80E13", + "display": "standalone" +} diff --git a/web/robots.txt b/web/robots.txt new file mode 100644 index 0000000..3dbf8c5 --- /dev/null +++ b/web/robots.txt @@ -0,0 +1,8 @@ +User-agent: * +Crawl-delay: 5 +Disallow: /dashboard +Disallow: /login +Disallow: /register +Disallow: /forgot +Disallow: /recover + diff --git a/web/static/css/settings.css b/web/static/css/settings.css new file mode 100644 index 0000000..6b274f7 --- /dev/null +++ b/web/static/css/settings.css @@ -0,0 +1,164 @@ +/* + DEMO STYLE +*/ + + +p { + font-family: 'Poppins', sans-serif; + font-size: 1.1em; + font-weight: 300; + line-height: 1.7em; + color: #999; +} + +a, +a:hover, +a:focus { + color: inherit; + text-decoration: none; + transition: all 0.3s; +} + +.navbar { + padding: 15px 10px; + background: #fff; + border: none; + border-radius: 0; + margin-bottom: 40px; + box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1); +} + +.navbar-btn { + box-shadow: none; + outline: none !important; + border: none; +} + +.line { + width: 100%; + height: 1px; + border-bottom: 1px dashed #ddd; + margin: 40px 0; +} + +/* --------------------------------------------------- + SIDEBAR STYLE +----------------------------------------------------- */ + +.wrapper { + display: flex; + width: 100%; + align-items: stretch; +} + +#sidebar { + min-width: 250px; + max-width: 250px; + background: #4A4A4A; + color: #fff; + transition: all 0.3s; +} + +#sidebar.active { + margin-left: -250px; +} + +#sidebar .sidebar-header { + padding: 20px; + background: #4F4F4F; +} + +#sidebar ul.components { + padding: 20px 0; + border-bottom: 1px solid #545454; +} + +#sidebar ul p { + color: #fff; + padding: 10px; +} + +#sidebar ul li a { + padding: 10px; + font-size: 1.1em; + display: block; +} + +#sidebar ul li a:hover { + color: #7386D5; + background: #fff; +} + +#sidebar ul li.active>a, +a[aria-expanded="true"] { + color: #fff; + background: #6d7fcc; +} + +a[data-toggle="collapse"] { + position: relative; +} + +.dropdown-toggle::after { + display: block; + position: absolute; + top: 50%; + right: 20px; + transform: translateY(-50%); +} + +ul ul a { + font-size: 0.9em !important; + padding-left: 30px !important; + background: #6d7fcc; +} + +ul.CTAs { + padding: 20px; +} + +ul.CTAs a { + text-align: center; + font-size: 0.9em !important; + display: block; + border-radius: 5px; + margin-bottom: 5px; +} + +a.download { + background: #fff; + color: #7386D5; +} + +a.article, +a.article:hover { + background: #6d7fcc !important; + color: #fff !important; +} + +/* --------------------------------------------------- + CONTENT STYLE +----------------------------------------------------- */ + +#content { + width: 100%; + padding: 20px; + min-height: 100vh; + transition: all 0.3s; +} + +/* --------------------------------------------------- + MEDIAQUERIES +----------------------------------------------------- */ + +@media (max-width: 768px) { + #sidebar { + margin-left: -250px; + } + #sidebar.active { + margin-left: 0; + } + #sidebarCollapse span { + display: none; + } +} diff --git a/web/static/defective/bg_video.mp4 b/web/static/defective/bg_video.mp4 new file mode 100644 index 0000000..1c1b3a9 Binary files /dev/null and b/web/static/defective/bg_video.mp4 differ diff --git a/web/static/defective/bg_video.webm b/web/static/defective/bg_video.webm new file mode 100644 index 0000000..ece05d6 Binary files /dev/null and b/web/static/defective/bg_video.webm differ diff --git a/web/static/defective/fullvideo.mp4 b/web/static/defective/fullvideo.mp4 new file mode 100644 index 0000000..8fdcb8c Binary files /dev/null and b/web/static/defective/fullvideo.mp4 differ diff --git a/web/static/defective/logo.png b/web/static/defective/logo.png new file mode 100644 index 0000000..a473dd2 Binary files /dev/null and b/web/static/defective/logo.png differ diff --git a/web/static/img/TopHeader.png b/web/static/img/TopHeader.png new file mode 100644 index 0000000..784de61 Binary files /dev/null and b/web/static/img/TopHeader.png differ diff --git a/web/static/img/WaterWolfLogo.svg b/web/static/img/WaterWolfLogo.svg new file mode 100644 index 0000000..dc87059 --- /dev/null +++ b/web/static/img/WaterWolfLogo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/static/img/amy_isaac.png b/web/static/img/amy_isaac.png new file mode 100644 index 0000000..8c228c5 Binary files /dev/null and b/web/static/img/amy_isaac.png differ diff --git a/web/static/img/avatar.webp b/web/static/img/avatar.webp new file mode 100644 index 0000000..423aba5 Binary files /dev/null and b/web/static/img/avatar.webp differ diff --git a/web/static/img/awoo.webp b/web/static/img/awoo.webp new file mode 100644 index 0000000..199f358 Binary files /dev/null and b/web/static/img/awoo.webp differ diff --git a/web/static/img/body_bg.webp b/web/static/img/body_bg.webp new file mode 100644 index 0000000..b302ba6 Binary files /dev/null and b/web/static/img/body_bg.webp differ diff --git a/web/static/img/con1.webp b/web/static/img/con1.webp new file mode 100644 index 0000000..341a0f3 Binary files /dev/null and b/web/static/img/con1.webp differ diff --git a/web/static/img/favicon/android-icon-144x144.png b/web/static/img/favicon/android-icon-144x144.png new file mode 100644 index 0000000..15a661f Binary files /dev/null and b/web/static/img/favicon/android-icon-144x144.png differ diff --git a/web/static/img/favicon/android-icon-192x192.png b/web/static/img/favicon/android-icon-192x192.png new file mode 100644 index 0000000..1b82c2b Binary files /dev/null and b/web/static/img/favicon/android-icon-192x192.png differ diff --git a/web/static/img/favicon/android-icon-36x36.png b/web/static/img/favicon/android-icon-36x36.png new file mode 100644 index 0000000..d659c5e Binary files /dev/null and b/web/static/img/favicon/android-icon-36x36.png differ diff --git a/web/static/img/favicon/android-icon-48x48.png b/web/static/img/favicon/android-icon-48x48.png new file mode 100644 index 0000000..484dfb6 Binary files /dev/null and b/web/static/img/favicon/android-icon-48x48.png differ diff --git a/web/static/img/favicon/android-icon-72x72.png b/web/static/img/favicon/android-icon-72x72.png new file mode 100644 index 0000000..348c0ed Binary files /dev/null and b/web/static/img/favicon/android-icon-72x72.png differ diff --git a/web/static/img/favicon/android-icon-96x96.png b/web/static/img/favicon/android-icon-96x96.png new file mode 100644 index 0000000..1f51d93 Binary files /dev/null and b/web/static/img/favicon/android-icon-96x96.png differ diff --git a/web/static/img/favicon/apple-icon-114x114.png b/web/static/img/favicon/apple-icon-114x114.png new file mode 100644 index 0000000..84385e6 Binary files /dev/null and b/web/static/img/favicon/apple-icon-114x114.png differ diff --git a/web/static/img/favicon/apple-icon-120x120.png b/web/static/img/favicon/apple-icon-120x120.png new file mode 100644 index 0000000..0690d29 Binary files /dev/null and b/web/static/img/favicon/apple-icon-120x120.png differ diff --git a/web/static/img/favicon/apple-icon-144x144.png b/web/static/img/favicon/apple-icon-144x144.png new file mode 100644 index 0000000..15a661f Binary files /dev/null and b/web/static/img/favicon/apple-icon-144x144.png differ diff --git a/web/static/img/favicon/apple-icon-152x152.png b/web/static/img/favicon/apple-icon-152x152.png new file mode 100644 index 0000000..a8f6796 Binary files /dev/null and b/web/static/img/favicon/apple-icon-152x152.png differ diff --git a/web/static/img/favicon/apple-icon-180x180.png b/web/static/img/favicon/apple-icon-180x180.png new file mode 100644 index 0000000..e7b9b60 Binary files /dev/null and b/web/static/img/favicon/apple-icon-180x180.png differ diff --git a/web/static/img/favicon/apple-icon-57x57.png b/web/static/img/favicon/apple-icon-57x57.png new file mode 100644 index 0000000..5950d32 Binary files /dev/null and b/web/static/img/favicon/apple-icon-57x57.png differ diff --git a/web/static/img/favicon/apple-icon-60x60.png b/web/static/img/favicon/apple-icon-60x60.png new file mode 100644 index 0000000..cda8d71 Binary files /dev/null and b/web/static/img/favicon/apple-icon-60x60.png differ diff --git a/web/static/img/favicon/apple-icon-72x72.png b/web/static/img/favicon/apple-icon-72x72.png new file mode 100644 index 0000000..348c0ed Binary files /dev/null and b/web/static/img/favicon/apple-icon-72x72.png differ diff --git a/web/static/img/favicon/apple-icon-76x76.png b/web/static/img/favicon/apple-icon-76x76.png new file mode 100644 index 0000000..5127925 Binary files /dev/null and b/web/static/img/favicon/apple-icon-76x76.png differ diff --git a/web/static/img/favicon/apple-icon-precomposed.png b/web/static/img/favicon/apple-icon-precomposed.png new file mode 100644 index 0000000..cba77f3 Binary files /dev/null and b/web/static/img/favicon/apple-icon-precomposed.png differ diff --git a/web/static/img/favicon/apple-icon.png b/web/static/img/favicon/apple-icon.png new file mode 100644 index 0000000..cba77f3 Binary files /dev/null and b/web/static/img/favicon/apple-icon.png differ diff --git a/web/static/img/favicon/dev-144.png b/web/static/img/favicon/dev-144.png new file mode 100644 index 0000000..720db68 Binary files /dev/null and b/web/static/img/favicon/dev-144.png differ diff --git a/web/static/img/favicon/favicon-16x16.png b/web/static/img/favicon/favicon-16x16.png new file mode 100644 index 0000000..090a7fe Binary files /dev/null and b/web/static/img/favicon/favicon-16x16.png differ diff --git a/web/static/img/favicon/favicon-32x32.png b/web/static/img/favicon/favicon-32x32.png new file mode 100644 index 0000000..097b6f3 Binary files /dev/null and b/web/static/img/favicon/favicon-32x32.png differ diff --git a/web/static/img/favicon/favicon-96x96.png b/web/static/img/favicon/favicon-96x96.png new file mode 100644 index 0000000..1f51d93 Binary files /dev/null and b/web/static/img/favicon/favicon-96x96.png differ diff --git a/web/static/img/favicon/ms-icon-144x144.png b/web/static/img/favicon/ms-icon-144x144.png new file mode 100644 index 0000000..15a661f Binary files /dev/null and b/web/static/img/favicon/ms-icon-144x144.png differ diff --git a/web/static/img/favicon/ms-icon-150x150.png b/web/static/img/favicon/ms-icon-150x150.png new file mode 100644 index 0000000..f2521bf Binary files /dev/null and b/web/static/img/favicon/ms-icon-150x150.png differ diff --git a/web/static/img/favicon/ms-icon-310x310.png b/web/static/img/favicon/ms-icon-310x310.png new file mode 100644 index 0000000..d970cb3 Binary files /dev/null and b/web/static/img/favicon/ms-icon-310x310.png differ diff --git a/web/static/img/favicon/ms-icon-70x70.png b/web/static/img/favicon/ms-icon-70x70.png new file mode 100644 index 0000000..f63337a Binary files /dev/null and b/web/static/img/favicon/ms-icon-70x70.png differ diff --git a/web/static/img/foxxcon_cp_badge.png b/web/static/img/foxxcon_cp_badge.png new file mode 100644 index 0000000..f2477ee Binary files /dev/null and b/web/static/img/foxxcon_cp_badge.png differ diff --git a/web/static/img/header_left.png b/web/static/img/header_left.png new file mode 100644 index 0000000..f6f4607 Binary files /dev/null and b/web/static/img/header_left.png differ diff --git a/web/static/img/header_right.png b/web/static/img/header_right.png new file mode 100644 index 0000000..326469c Binary files /dev/null and b/web/static/img/header_right.png differ diff --git a/web/static/img/isaac_cat.png b/web/static/img/isaac_cat.png new file mode 100644 index 0000000..13d86fd Binary files /dev/null and b/web/static/img/isaac_cat.png differ diff --git a/web/static/img/logo-with-logotype.svg b/web/static/img/logo-with-logotype.svg new file mode 100644 index 0000000..c6dd64d --- /dev/null +++ b/web/static/img/logo-with-logotype.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/static/img/no_poster.jpg b/web/static/img/no_poster.jpg new file mode 100644 index 0000000..ae6e5cc Binary files /dev/null and b/web/static/img/no_poster.jpg differ diff --git a/web/static/img/no_poster.png b/web/static/img/no_poster.png new file mode 100644 index 0000000..23cd303 Binary files /dev/null and b/web/static/img/no_poster.png differ diff --git a/web/static/img/no_poster_thumb.jpg b/web/static/img/no_poster_thumb.jpg new file mode 100644 index 0000000..09fd055 Binary files /dev/null and b/web/static/img/no_poster_thumb.jpg differ diff --git a/web/static/img/no_poster_thumb.png b/web/static/img/no_poster_thumb.png new file mode 100644 index 0000000..b45b740 Binary files /dev/null and b/web/static/img/no_poster_thumb.png differ diff --git a/web/static/img/portal/portal1.jpg b/web/static/img/portal/portal1.jpg new file mode 100644 index 0000000..d820938 Binary files /dev/null and b/web/static/img/portal/portal1.jpg differ diff --git a/web/static/img/portal/portal2.jpg b/web/static/img/portal/portal2.jpg new file mode 100644 index 0000000..8adad96 Binary files /dev/null and b/web/static/img/portal/portal2.jpg differ diff --git a/web/static/img/portal/portal3.jpg b/web/static/img/portal/portal3.jpg new file mode 100644 index 0000000..e538f3a Binary files /dev/null and b/web/static/img/portal/portal3.jpg differ diff --git a/web/static/img/portal/portal4.jpg b/web/static/img/portal/portal4.jpg new file mode 100644 index 0000000..2b20198 Binary files /dev/null and b/web/static/img/portal/portal4.jpg differ diff --git a/web/static/img/portal/portal5.jpg b/web/static/img/portal/portal5.jpg new file mode 100644 index 0000000..77e4f42 Binary files /dev/null and b/web/static/img/portal/portal5.jpg differ diff --git a/web/static/img/portal/portal6.jpg b/web/static/img/portal/portal6.jpg new file mode 100644 index 0000000..c984753 Binary files /dev/null and b/web/static/img/portal/portal6.jpg differ diff --git a/web/static/img/portal/portal7.jpg b/web/static/img/portal/portal7.jpg new file mode 100644 index 0000000..7491b6d Binary files /dev/null and b/web/static/img/portal/portal7.jpg differ diff --git a/web/static/img/tech_ada.png b/web/static/img/tech_ada.png new file mode 100644 index 0000000..d68f8e2 Binary files /dev/null and b/web/static/img/tech_ada.png differ diff --git a/web/static/img/tons_of_dj.png b/web/static/img/tons_of_dj.png new file mode 100644 index 0000000..136be4c Binary files /dev/null and b/web/static/img/tons_of_dj.png differ diff --git a/web/static/img/workflow.png b/web/static/img/workflow.png new file mode 100644 index 0000000..0c1cdb6 Binary files /dev/null and b/web/static/img/workflow.png differ diff --git a/web/static/video/ANW_Dance3.mp4 b/web/static/video/ANW_Dance3.mp4 new file mode 100644 index 0000000..d33d413 Binary files /dev/null and b/web/static/video/ANW_Dance3.mp4 differ diff --git a/web/static/video/login.mp4 b/web/static/video/login.mp4 new file mode 100644 index 0000000..84adfa4 Binary files /dev/null and b/web/static/video/login.mp4 differ diff --git a/web/static/video/login.webm b/web/static/video/login.webm new file mode 100644 index 0000000..c6761ba Binary files /dev/null and b/web/static/video/login.webm differ diff --git a/web/static/video/register.mp4 b/web/static/video/register.mp4 new file mode 100644 index 0000000..9fbf16b Binary files /dev/null and b/web/static/video/register.mp4 differ diff --git a/web/static/video/register.webm b/web/static/video/register.webm new file mode 100644 index 0000000..ccfd85e Binary files /dev/null and b/web/static/video/register.webm differ diff --git a/web/static/video/shy.jpg b/web/static/video/shy.jpg new file mode 100644 index 0000000..088d0ad Binary files /dev/null and b/web/static/video/shy.jpg differ diff --git a/web/static/video/shy.mp4 b/web/static/video/shy.mp4 new file mode 100644 index 0000000..d34ad44 Binary files /dev/null and b/web/static/video/shy.mp4 differ diff --git a/web/static/video/shy.webm b/web/static/video/shy.webm new file mode 100644 index 0000000..438b07f Binary files /dev/null and b/web/static/video/shy.webm differ diff --git a/web/static/wwradio/no-art.jpg b/web/static/wwradio/no-art.jpg new file mode 100644 index 0000000..d32ba67 Binary files /dev/null and b/web/static/wwradio/no-art.jpg differ diff --git a/web/static/wwradio/player-track-bg.png b/web/static/wwradio/player-track-bg.png new file mode 100644 index 0000000..9f58ff9 Binary files /dev/null and b/web/static/wwradio/player-track-bg.png differ diff --git a/web/static/wwradio/ww-radio.png b/web/static/wwradio/ww-radio.png new file mode 100644 index 0000000..978ff05 Binary files /dev/null and b/web/static/wwradio/ww-radio.png differ diff --git a/web/tvtools/events.html b/web/tvtools/events.html new file mode 100644 index 0000000..dd5199c --- /dev/null +++ b/web/tvtools/events.html @@ -0,0 +1,46 @@ + + + + + + + + +Events + + + + + + + + + + + + + + + + Welcome + + + + diff --git a/web/tvtools/scroller.html b/web/tvtools/scroller.html new file mode 100644 index 0000000..03d8b58 --- /dev/null +++ b/web/tvtools/scroller.html @@ -0,0 +1,207 @@ + + + + + + + + Scroller + + + + + + + + + + + + + +
+
EDT
+
+ +
The WaterWolf Grand Stock Exchange (WGSE) -
+
Pets (PETS)
+
Boops (BOOP)
+
Proto Memory (PMEM)
+
Tails (TAIL)
+
Scales (SCALE)
+
Beans (BNZ)
+
Hooves (HOOF)
+
Fur (FUZ)
+
Sharks (SHRK)
+
ReX Traders (REX)
+
WaterWolf Media Group (WTWF)
+
WaterWolf Music BMG (WTMS)
+
Lume Juice (LUME)
+
Johari Juice (ORNG)
+
Yellow Dog Soda (YELDG)
+
Ponies (MLPX)
+
Wicker Stock (WIX)
+
The Larscis Foundation (LAR)
+
Taidum Chaos Energy (BNABX)
+
Bespoke Streaming Backend Services (ROXY)
+
Far Oval Trading Co. (FARO)
+
Chimera Network (CHNW)
+
Fast Forward Media Production and Entertainment Group, + Inc. (FFMPEG) +
+
Pammematth Anxiety (PMMA)
+
+
+
+ + + + + diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..11148cc --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,40 @@ +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +module.exports = { + entry: [ + './frontend/less/style.less' + ], + plugins: [ + new MiniCssExtractPlugin() + ], + output: { + path: path.resolve(__dirname, './web/static/dist') + }, + devServer: { + static: path.resolve(__dirname, './web/static/dist'), + port: 8080, + hot: true + }, + + module: { + rules: [ + { + test: /\.less$/, + use: [ + MiniCssExtractPlugin.loader, + 'css-loader', + 'less-loader' + ], + }, + { + test: /\.css$/, + exclude: /node_modules/, + use: [ + MiniCssExtractPlugin.loader, + 'css-loader', + ], + }, + ] + }, +}