diff --git a/package.json b/package.json index 6f06d62..570a62b 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "", "main": "index.js", "scripts": { + "build:submissions-uploader": "vite build --config vite-configs/vite.submissions-uploader.config.js", "build:rhpz-url": "vite build --config vite-configs/vite.rhpz-url.config.js", "build:send-notification": "vite build --config vite-configs/vite.send-notification.config.js", "build:manage-notifications": "vite build --config vite-configs/vite.manage-notifications.config.js", diff --git a/src/Extenders/Admin/Pages/Settings.php b/src/Extenders/Admin/Pages/Settings.php index 29fe0b6..4267b99 100644 --- a/src/Extenders/Admin/Pages/Settings.php +++ b/src/Extenders/Admin/Pages/Settings.php @@ -181,6 +181,26 @@ class Settings extends Abstract_Page { 'discord', ); + add_settings_section( + 'xf', + __( 'XenForo', 'romhackplaza' ), + function(){}, + 'romhackplaza-config' + ); + + add_settings_field( + 'xf_auto_create_user', + __( 'Automatic XenForo account creation if user don\'t have XF ID', 'romhackplaza' ), + function(){ + global $_romhackplaza; + $value = esc_attr( $_romhackplaza->settings->get( 'xf_auto_create_user' ) ?? '0' ); + + echo sprintf( '', 'romhackplaza_plugin_options', 'xf_auto_create_user', checked( $value, '1', false ) ); + }, + 'romhackplaza-config', + 'xf', + ); + } /* --- diff --git a/src/Extenders/Post/Properties.php b/src/Extenders/Post/Properties.php index 1a11ac1..5b342b2 100644 --- a/src/Extenders/Post/Properties.php +++ b/src/Extenders/Post/Properties.php @@ -7,7 +7,7 @@ defined( '\ABSPATH' ) || exit; class Properties extends Abstract_Extender { - public static \WP_Post $current_post; + public static \WP_Post|null $current_post; protected function can_extend(): bool { @@ -23,7 +23,7 @@ class Properties extends Abstract_Extender { } - public static function change_current_post( \WP_Post $post ): void { + public static function change_current_post( \WP_Post|null $post ): void { static::$current_post = $post; diff --git a/src/Extenders/Submissions.php b/src/Extenders/Submissions.php index c4a6eef..8b27eae 100644 --- a/src/Extenders/Submissions.php +++ b/src/Extenders/Submissions.php @@ -111,12 +111,12 @@ class Submissions extends Abstract_Extender { 'submissions-uploader', ROMHACKPLAZA_PLUGIN_URI . '/assets/js/submissions/uploader.js', [], - '20251106', + '20251106-68', args: [ 'defer' => true, 'in_footer' => true ] )->enqueue()->add_localize( '_romhackplaza_script_uploader', [ 'submit_url' => admin_url( 'admin-ajax.php' ) ], - ); + )->enable_i18n(); } diff --git a/src/Extenders/XenForo/API.php b/src/Extenders/XenForo/API.php new file mode 100644 index 0000000..33bf267 --- /dev/null +++ b/src/Extenders/XenForo/API.php @@ -0,0 +1,233 @@ + [ + 'XF-Api-Key' => $_ENV['ROMHACKPLAZA_XF_SUPER_USER_API_KEY'], + 'Content-Type' => 'application/json', + ], + ]; + + if( $action_type !== 'GET' ) + $wp_args['body'] = json_encode( $body ); + + if( $need_xf_api_user ) + $wp_args['headers']['XF-Api-User'] = 1; + + $wp_args['headers'] = wp_parse_args( $headers, $wp_args['headers'] ); + + $response = null; + + switch ( $action_type ) { + case 'GET': + $response = wp_remote_get( $url, $wp_args ); + break; + case 'POST': + $response = wp_remote_post( $url, $wp_args ); + break; + default: + $wp_args['method'] = $action_type; + $response = wp_remote_request( $url, $wp_args ); + break; + } + + if( !empty( $response->errors ) ){ + echo ""; + $response = $response->errors; + if( is_array( $response ) ) + $response['api_error'] = true; + } else { + $response = json_decode( wp_remote_retrieve_body( $response ), ARRAY_A ); + if( is_array( $response ) ) + $response['api_error'] = false; + } + return $response; + } + + private static function _error_occured( mixed $response ): bool { + + if( is_array( $response ) && isset( $response['api_error'] ) && $response['api_error'] === true ) + return true; + return false; + + } + + /* + * API Methods + */ + + public static function post__alerts( + int $to_xf_user_id, + string $message, + string $url = "", + string $url_title = "", + int $from_xf_user_id = 0, + ): array|null { + + $url = self::_build_url( 'alerts' ); + $body = [ + 'to_user_id' => $to_xf_user_id, + 'alert' => $message, + 'from_user_id' => $from_xf_user_id, + 'link_url' => $url, + 'link_title' => $url_title, + ]; + + $response = self::_do_request( 'POST', $url, $body, need_xf_api_user: true ); + + if( self::_error_occured( $response ) ) + return null; + + return $response; + } + + public static function post__auth_login_token( + int $xf_user_id, + array $body = [], + ): array|null { + + if( !isset( $body['user_id'] ) ) + $body['user_id'] = $xf_user_id; + + $url = self::_build_url( 'auth/login-token' ); + $response = self::_do_request( 'POST', $url, $body ); + + if( self::_error_occured( $response ) ) + return null; + + return $response; + + } + + public static function get__index( + + ): array|null { + $url = self::_build_url( 'index' ); + $response = self::_do_request( 'GET', $url ); + + if( self::_error_occured( $response ) ) + return null; + + return $response; + } + + public static function get__me( + ): array|null { + $url = self::_build_url( "me" ); + $response = self::_do_request( 'GET', $url ); + + if( self::_error_occured( $response ) ) + return null; + + return $response['me']; + } + + public static function get__users_id( + int $xf_user_id, + array $body = [] + ){ + $url = self::_build_url( "users/{$xf_user_id}", $body ); + $response = self::_do_request( 'GET', $url, need_xf_api_user: true ); + + if( self::_error_occured( $response ) ) + return null; + + return $response; + } + + public static function post__users( + array $body + ){ + $url = self::_build_url( "users" ); + $response = self::_do_request( 'POST', $url, $body, need_xf_api_user: true ); + + if( self::_error_occured( $response ) ) + return null; + + return $response; + } + + public static function post__users_id( + int $xf_user_id, + array $body + ){ + $url = self::_build_url( "users/{$xf_user_id}" ); + $response = self::_do_request( 'POST', $url, $body, need_xf_api_user: true ); + + if( self::_error_occured( $response ) ) + return null; + + return $response; + } + + /* + * Custom API methods + */ + + public static function get__plaza_alerts_id( + int $xf_user_id = 0, + ): array|null { + + $url = self::_build_url( "plaza-alerts/{$xf_user_id}" ); + $response = self::_do_request( 'GET', $url, need_xf_api_user: true ); + + if( self::_error_occured( $response ) ) + return null; + + return $response; + + } + + public static function post__plaza_reports( + int $entry_id, + string $entry_title, + string $entry_url, + int $xf_user_id = 0, + string $username = "Guest", + string $email = "", + string $message = "", + ){ + + $url = self::_build_url( "plaza-reports" ); + + $body = [ + 'entry_id' => $entry_id, + 'entry_title' => $entry_title, + 'entry_url' => $entry_url, + 'xf_user_id' => $xf_user_id, + 'username' => $username, + 'email' => $email, + 'message' => $message, + ]; + + $response = self::_do_request( 'POST', $url, $body, need_xf_api_user: true ); + if( self::_error_occured( $response ) ) + return null; + + return $response; + } + +} \ No newline at end of file diff --git a/src/Extenders/XenForo/Abstract_XenForo_Children.php b/src/Extenders/XenForo/Abstract_XenForo_Children.php new file mode 100644 index 0000000..9bf10db --- /dev/null +++ b/src/Extenders/XenForo/Abstract_XenForo_Children.php @@ -0,0 +1,13 @@ +add_action('wp_login', [ $this, 'auth_on_xenforo' ], 50, 2 ); + } + + private function verify_xenforo_auth( \WP_User $user, int $xf_user_id ){ + + $response = API::get__users_id( $xf_user_id, [ 'with_posts' => false ] ); + if( $response != null && isset( $response['user']['email'] ) ){ + return $response['user']['email'] === $user->user_email; + } + + return false; + } + + public function auth_on_xenforo( string $username, \WP_User $wp_user ) { + + $xf_user_id = absint( get_the_author_meta( XenForo::XF_USER_ID, $wp_user->ID ) ?? 0 ); + if( $xf_user_id == 0 ) + return; // Disable login... + + if( !$this->verify_xenforo_auth( $wp_user, $xf_user_id ) ) + return; + + $response = API::post__auth_login_token( $xf_user_id, [ + 'user_id' => $xf_user_id, + 'return_url' => home_url(), + 'force' => true, + ] ); + + if( $response != null && isset( $response['login_url'] ) ) { + if( wp_redirect( $response['login_url'] ) ) { + exit; + } + } + + } + +} \ No newline at end of file diff --git a/src/Extenders/XenForo/User_Settings.php b/src/Extenders/XenForo/User_Settings.php new file mode 100644 index 0000000..b7d26ce --- /dev/null +++ b/src/Extenders/XenForo/User_Settings.php @@ -0,0 +1,174 @@ +value => 5, + Roles::Member->value => 2, + Roles::Verified->value => 6, + Roles::Moderator->value => 4, + Roles::Administrator->value => 3, + Roles::Bot->value => 7 + ]; + + protected function extend(): void + { + $this->add_action( 'show_user_profile', [ $this, 'xf_user_id_field'] ); + $this->add_action( 'edit_user_profile', [ $this, 'xf_user_id_field'] ); + + $this->add_action('personal_options_update', [ $this, 'update_xf_user_id_from_profile'] ); + $this->add_action('edit_user_profile_update', [ $this, 'update_xf_user_id_from_profile'] ); + + $this->add_action( 'profile_update', [ $this, 'sync_user_profile' ], 20, 2 ); + } + + public function xf_user_id_field( \WP_User $user ): void { + + global $_romhackplaza; + if( !$_romhackplaza->user_roles->current_is_staff() ) + return; + + $field_value = absint( get_the_author_meta( XenForo::XF_USER_ID, $user->ID ) ?? 0 ); + + ?> +

+ + + + + +
+ +
+ user_roles->current_is_staff() ) // Can't update this field if not staff member. + return; + + if( isset( $_POST[ XenForo::XF_USER_ID ] ) ){ + + $field_value = absint( $_POST[ XenForo::XF_USER_ID ] ) ?? 0; + if( $field_value != 0 ) + update_user_meta( $user_id, XenForo::XF_USER_ID, $field_value ); + } + + } + + private function prepare_sync_user_role( string $role_name, int $user_id ): array{ + + global $_romhackplaza; + return [ + 'user_group_id' => self::USER_ROLE_SYNC[$role_name], + 'is_staff' => $_romhackplaza->user_roles->user_is_staff( $user_id ) + ]; + + } + + public function sync_user_role( int $user_id ){ + + global $_romhackplaza; + $role = $_romhackplaza->user_roles->user_upper( $user_id ); + + if( $role == null ) + return; + + $xf_user_id = absint( get_the_author_meta( XenForo::XF_USER_ID, $user_id ) ); + if( $xf_user_id == 0 ) + return; + + API::post__users_id( $xf_user_id, self::prepare_sync_user_role( $role->value, $user_id ) ); + } + + public function create_xenforo_account_from_wp_user( int $user_id ){ + + global $_romhackplaza; + if( $_romhackplaza->settings->get( 'xf_auto_create_user' ) != '1' ) + return; + + $wp_user = get_user_by( 'id', $user_id ); + if( !$wp_user ) + return; + + $create_user_array = []; + $create_user_array['username'] = $wp_user->display_name; + + $role = $_romhackplaza->user_roles->user_upper( $user_id ); + if( $role != null ) + $create_user_array = array_merge( $create_user_array, self::prepare_sync_user_role( $role->value, $user_id ) ); + + $create_user_array['email'] = sanitize_email( $wp_user->user_email ); + $create_user_array['password'] = wp_generate_password(); + $create_user_array['custom_fields']['wp_user_id'] = $user_id; + $create_user_array['custom_fields']['wp_entries_number'] = 0; + + $response = API::post__users( $create_user_array ); + + if( $response != null && $response['success'] === true && isset( $response['user']['user_id'] ) ){ + update_user_meta( $user_id, XenForo::XF_USER_ID, $response['user']['user_id'] ); + } + + } + + public function sync_user_profile( int $user_id, \WP_User $old_values ): void { + + $xf_user_id = absint( get_the_author_meta( XenForo::XF_USER_ID, $user_id ) ); + if( $xf_user_id == 0 ) { + $this->create_xenforo_account_from_wp_user($user_id); + return; + } + + $update_user_array = []; + + // Username change + if( !empty( $_POST['display_name'] ) && $_POST['display_name'] !== $old_values->display_name ){ + $update_user_array['username'] = sanitize_text_field( $_POST['display_name'] ); + $update_user_array['username_change_visible'] = true; + } + + // Role change + if( !empty( $_POST['role'] ) && !in_array( $_POST['role'], $old_values->roles ) ) + $update_user_array = array_merge( $update_user_array, self::prepare_sync_user_role( sanitize_text_field( $_POST['role'] ), $user_id ) ); + + // E-Mail change + if( !empty( $_POST['email'] ) && $_POST['email'] !== $old_values->user_email ) + $update_user_array['email'] = sanitize_email( $_POST['email'] ); + + // Password change + if( !empty( $_POST['pass1'] ) && !empty( $_POST['pass2'] ) && $_POST['pass1'] === $_POST['pass2'] ) + $update_user_array['password'] = esc_attr( $_POST['pass1'] ); + + $update_user_array['custom_fields']['wp_user_id'] = $user_id; + + // ... + + $response = null; + if( $update_user_array != [] ) + $response = API::post__users_id( $xf_user_id, $update_user_array ); + + } + + public function get_number_of_unviewed_alerts( int $user_id ): int { + + $xf_user_id = absint( get_user_meta( $user_id, XenForo::XF_USER_ID, true ) ?? 0 ); + if( $xf_user_id == 0 ) + return 0; + + $response = API::get__plaza_alerts_id( $xf_user_id ); + if( $response != null ) + return count( $response['alerts'] ?? [] ); + + return 0; + + } + +} \ No newline at end of file diff --git a/src/Extenders/XenForo/XenForo.php b/src/Extenders/XenForo/XenForo.php new file mode 100644 index 0000000..c470e08 --- /dev/null +++ b/src/Extenders/XenForo/XenForo.php @@ -0,0 +1,48 @@ +users = new User_Settings(); + $this->auth = new Auth(); + + do_action( "qm/debug", $this->users->get_number_of_unviewed_alerts( 1 ) ); + + } + +} \ No newline at end of file diff --git a/src/Identifier.php b/src/Identifier.php new file mode 100644 index 0000000..1f085f4 --- /dev/null +++ b/src/Identifier.php @@ -0,0 +1,46 @@ + array_key_first(...) + return $user_roles |> array_first(...) |> Roles::tryFrom(...); } diff --git a/src/Plugin.php b/src/Plugin.php index 7e074b8..c1666b3 100755 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -83,6 +83,8 @@ class Plugin { $this->children['extend_save_post'] = new Extenders\Post\Save_Post(); $this->children['extend_post_properties'] = new Extenders\Post\Properties(); + $this->children['extend_xenforo'] = new Extenders\XenForo\XenForo(); + $this->children['extend_acf_pro'] = new Extenders\Advanced_Custom_Fields_Pro(); $this->children['override_post_announcements'] = new Overrides\Post_Announcements(); diff --git a/src/Settings.php b/src/Settings.php index ef9b970..682941e 100644 --- a/src/Settings.php +++ b/src/Settings.php @@ -30,6 +30,7 @@ class Settings { 'public_edit_disabled_tag_id' => '', 'discord_webhook_romhacks_translations' => '', 'discord_webhook_global' => '', + 'xf_auto_create_user' => '0', ]; $this->refresh(); diff --git a/ts/globals.d.ts b/ts/globals.d.ts index a46dfe0..e79cf2c 100644 --- a/ts/globals.d.ts +++ b/ts/globals.d.ts @@ -2,5 +2,8 @@ declare const $: any; declare const jQuery: any; +/// +declare const wp: any; + /// declare function romhackplaza_manage_modal( a: object, b: string|undefined, c: string|undefined, d: string|undefined, e: string|undefined ): void; \ No newline at end of file diff --git a/ts/submissions/class-drop-container.ts b/ts/submissions/class-drop-container.ts index cf60111..9c584f8 100644 --- a/ts/submissions/class-drop-container.ts +++ b/ts/submissions/class-drop-container.ts @@ -28,11 +28,11 @@ export class DropContainer { private addEvents(): void { - this.element.addEventListener( 'click', this.submitFile, false ); + this.element.addEventListener( 'click', (e: Event) => { this.submitFile(e) }, false ); this.element.addEventListener( 'dragover', (e: Event) => e.preventDefault(), false ); this.element.addEventListener( 'dragenter', (e: Event) => this.element.classList.add( 'drag-active'), false ); this.element.addEventListener( 'dragleave', (e: Event) => this.element.classList.remove( 'drag-active'), false ); - this.element.addEventListener( 'drop', this.dropElement ); + this.element.addEventListener( 'drop', (e: Event) => { this.dropElement( e ); } ); } submitFile( e: Event ): void { diff --git a/ts/submissions/class-reserve-post-id.ts b/ts/submissions/class-reserve-post-id.ts new file mode 100644 index 0000000..bb2e4ed --- /dev/null +++ b/ts/submissions/class-reserve-post-id.ts @@ -0,0 +1,18 @@ +import {__, I } from "./globals"; +declare const _romhackplaza_script_uploader: any; +declare const romhackplaza_modal_submissions: any; + +export class Reserve_Post_ID { + + /** + * + * @param callback - MUST BE FROM A Upload child. + */ + constructor( callback: () => any ){ + + if( I.is_edit ) { + callback(); + return; + } + } +} \ No newline at end of file diff --git a/ts/submissions/class-upload.ts b/ts/submissions/class-upload.ts index 0bdef96..dffbd78 100644 --- a/ts/submissions/class-upload.ts +++ b/ts/submissions/class-upload.ts @@ -1,9 +1,12 @@ -import {__, FORBIDDEN_CARS, romhackplaza_modal_submissions, during_upload, I } from "./globals"; +import {__, FORBIDDEN_CARS, I } from "./globals"; import {DropContainer} from "./class-drop-container"; +declare const _romhackplaza_script_uploader: any; +declare const romhackplaza_modal_submissions: any; export class Upload { file: File; + progress_bar: HTMLElement|undefined; constructor( file: File ){ @@ -22,10 +25,20 @@ export class Upload { console.error("WTF at beginUpload method."); return; } + this.switchDuringUpload(); if( typeof I.drop_container !== 'undefined' && I.drop_container instanceof DropContainer ) I.drop_container.switch(); + let progress = document.getElementById( 'progress' ); + if( progress !== null ) { + progress.style.display = 'block'; + this.progress_bar = progress.querySelector('.bar') as HTMLElement; + } + + if( I.reserved_post_id === undefined || I.reserved_post_id === null ) + console.log( "ok" ); + } checkForbiddenCars() :boolean { @@ -42,11 +55,20 @@ export class Upload { private switchDuringUpload(): void { // @ts-ignore - during_upload = !during_upload; + I.during_upload = !I.during_upload; + this.changeStatus( __( "Preparing upload...", 'romhackplaza' ) ); this.switchSubmissionButton(); } + private changeStatus( str: string ){ + + let sts: HTMLElement|null = document.getElementById( 'status' ); + if( sts !== null ) + sts.textContent = str; + + } + private switchSubmissionButton(): void { let btn: HTMLElement|null = document.getElementById( 'submitTranslationButton' ); diff --git a/ts/submissions/class-uploader-data.ts b/ts/submissions/class-uploader-data.ts new file mode 100644 index 0000000..bff8f57 --- /dev/null +++ b/ts/submissions/class-uploader-data.ts @@ -0,0 +1,52 @@ +export class Uploader_Data { + + private data: object; + + constructor() { + this.data = {}; + + return new Proxy( this, { + get: ( target, p ) => { + return target.get( p as string ); + }, + set: ( target, p, v ) => { + target.set( p as string, v ); + return true + } + }); + } + + add_field( field_name: string, field_value: any, readonly: boolean ){ + + // @ts-ignore + if( !this.data[field_name] ) + // @ts-ignore + this.data[field_name] = { "data": field_value, can_write: ( readonly ? 1 : -1 ) }; + + } + + get( field_name : string ): any { + + // @ts-ignore + if( this.data[field_name] ) + // @ts-ignore + return this.data[field_name]['data']; + + return undefined; + + } + + set( field_name: string, field_value: any ){ + + // @ts-ignore + if( this.data[field_name] && this.data[field_name]['can_write'] != 0 ){ + // @ts-ignore + this.data[field_name]['data'] = field_value; + // @ts-ignore + if( this.data[field_name]['can_write'] == 1 ) + // @ts-ignore + this.data[field_name]['can_write'] = 0; + } + + } +} \ No newline at end of file diff --git a/ts/submissions/globals.ts b/ts/submissions/globals.ts index e348c80..0bba185 100644 --- a/ts/submissions/globals.ts +++ b/ts/submissions/globals.ts @@ -1,18 +1,17 @@ -export declare const _romhackplaza_script_uploader: any; -export declare const romhackplaza_modal_submissions: any; -declare const wp: any; -export const { __, _x, _n, _nx } = wp.i18n; +import {Uploader_Data} from "./class-uploader-data"; +export const { __, _x, _n, _nx } = wp.i18n; export const FORBIDDEN_CARS: Array = [ "&", "+" ]; -export let during_upload = false; -export const I: any = { - upload: undefined, - drop_container: undefined, -} +export const I : Uploader_Data = new Uploader_Data(); +I.add_field( 'is_edit', false, true ); +I.add_field( 'reserved_post_id', undefined, false ); +I.add_field( 'during_upload', false, false ); +I.add_field( 'upload', undefined, false ); +I.add_field( 'drop_container', undefined, true ); export interface Can_Upload_Detail { file: File; diff --git a/ts/submissions/index.ts b/ts/submissions/index.ts index 1b466d8..8fa9d43 100644 --- a/ts/submissions/index.ts +++ b/ts/submissions/index.ts @@ -1,10 +1,16 @@ -import {type Can_Upload_Detail, during_upload, I} from "./globals"; +import {type Can_Upload_Detail, I} from "./globals"; import {Upload} from "./class-upload"; import {DropContainer} from "./class-drop-container"; document.addEventListener('DOMContentLoaded', () => { - if( !document.getElementById( 'fileInput' ) ) // Check if exists. + if( !document.getElementById( 'file-container' ) ) // Check if exists. + return; + + // Add PostID if edit. + let url_params = new URLSearchParams(window.location.search); + I.is_edit = url_params.has( 'edit_entry' ); + I.reserved_post_id = url_params.get('edit_entry' ) || null; // @ts-ignore I.drop_container = new DropContainer( document.getElementById( 'file-container' ) as HTMLElement, document.getElementById( 'file-container-text' ) as HTMLElement ); @@ -13,7 +19,7 @@ document.addEventListener('DOMContentLoaded', () => { document.addEventListener( 'can_upload', (e: CustomEvent ) => { const { file } = e.detail; - if( !during_upload ) // @ts-ignore + if( !I.during_upload ) // @ts-ignore I.upload = new Upload( file ); }); diff --git a/vite-configs/vite.submissions-uploader.config.js b/vite-configs/vite.submissions-uploader.config.js new file mode 100644 index 0000000..b112862 --- /dev/null +++ b/vite-configs/vite.submissions-uploader.config.js @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite'; +import path from 'path'; + +export default defineConfig({ + root: path.resolve(__dirname, '..'), + build: { + outDir: 'assets/js/submissions', + emptyOutDir: false, + + rollupOptions: { + input: 'ts/submissions/index.ts', + output: { + entryFileNames: `uploader.js`, + }, + }, + }, +}); \ No newline at end of file