Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions dt-apps/dt-home/magic-link-home-app.php
Original file line number Diff line number Diff line change
Expand Up @@ -491,12 +491,7 @@ public function add_endpoints() {
public function endpoint_get( WP_REST_Request $request ) {
$params = $request->get_params();

// Debug logging
error_log( 'DT Home Screen REST endpoint called' );
error_log( 'Request params: ' . print_r( $params, true ) );

if ( ! isset( $params['parts'], $params['action'] ) ) {
error_log( 'Missing parameters - parts: ' . ( isset( $params['parts'] ) ? 'yes' : 'no' ) . ', action: ' . ( isset( $params['action'] ) ? 'yes' : 'no' ) );
return new WP_Error( __METHOD__, 'Missing parameters', [ 'status' => 400 ] );
}

Expand All @@ -511,8 +506,6 @@ public function endpoint_get( WP_REST_Request $request ) {
$apps_manager = DT_Home_Apps::instance();
$apps = $apps_manager->get_apps_for_user( $user_id );

error_log( 'Apps found: ' . count( $apps ) );

return [
'success' => true,
'apps' => $apps,
Expand All @@ -524,8 +517,6 @@ public function endpoint_get( WP_REST_Request $request ) {
$training_manager = DT_Home_Training::instance();
$training_videos = $training_manager->get_videos_for_frontend();

error_log( 'Training videos found: ' . count( $training_videos ) );

return [
'success' => true,
'training_videos' => $training_videos,
Expand All @@ -541,9 +532,6 @@ public function endpoint_get( WP_REST_Request $request ) {
$apps = $apps_manager->get_apps_for_user( $user_id );
$training_videos = $training_manager->get_videos_for_frontend();

error_log( 'Apps found: ' . count( $apps ) );
error_log( 'Training videos found: ' . count( $training_videos ) );

return [
'success' => true,
'user_id' => $user_id,
Expand Down
6 changes: 3 additions & 3 deletions dt-assets/js/comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -731,9 +731,9 @@ jQuery(document).ready(function ($) {
);
}

$('.revert-field').html(field || a.meta_key);
$('.revert-current-value').html(a.meta_value);
$('.revert-old-value').html(a.old_value || 0);
$('.revert-field').text(field || a.meta_key);
$('.revert-current-value').text(a.meta_value);
$('.revert-old-value').text(a.old_value || 0);
})
.catch((err) => {
console.error(err);
Expand Down
2 changes: 1 addition & 1 deletion dt-assets/js/details.js
Original file line number Diff line number Diff line change
Expand Up @@ -1103,7 +1103,7 @@ jQuery(document).ready(function ($) {
onClick: function (node, a, item) {
$('.confirm-merge-with-post').show();
$('#confirm-merge-with-post-id').val(item.ID);
$('#name-of-post-to-merge').html(item.name);
$('#name-of-post-to-merge').text(item.name);
},
onResult: function (node, query, result, resultCount) {
let text = window.TYPEAHEADS.typeaheadHelpText(
Expand Down
2 changes: 1 addition & 1 deletion dt-assets/js/modular-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@
<img style="padding: 0 4px" src="${window.wpApiShare.template_dir}/dt-assets/images/trash.svg">
</span>`);
delete_filter.on('click', function () {
$(`.delete-filter-name`).html(filter.name);
$(`.delete-filter-name`).text(filter.name);
$('#delete-filter-modal').foundation('open');
filter_to_delete = filter.ID;
});
Expand Down
8 changes: 6 additions & 2 deletions dt-contacts/contacts-transfer.php
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,11 @@ public static function receive_transferred_contact( $params ) {
$lagging_meta_input = [];
$lagging_location_meta_input = [];
$errors = new WP_Error();
$site_link_post_id = Site_Link_System::get_post_id_by_site_key( Site_Link_System::decrypt_transfer_token( $params['transfer_token'] ) );
$site_link_post_id = empty( $params['transfer_token'] ) ? false : Site_Link_System::get_post_id_by_site_key( Site_Link_System::decrypt_transfer_token( $params['transfer_token'] ) );
if ( empty( $site_link_post_id ) ) {
$errors->add( 'transfer_token_invalid', 'Invalid or missing transfer token' );
return $errors;
}
$field_settings = DT_Posts::get_post_field_settings( $post_args['post_type'] );

/**
Expand All @@ -554,7 +558,7 @@ public static function receive_transferred_contact( $params ) {
if ( $key === 'type' && $value[0] === 'media' ) {
$value[0] = 'access';
}
$meta_input[ $key ] = maybe_unserialize( $value[0] );
$meta_input[ $key ] = is_serialized( $value[0] ) ? unserialize( $value[0], [ 'allowed_classes' => false ] ) : $value[0];
}
}
$post_args['meta_input'] = $meta_input;
Expand Down
5 changes: 5 additions & 0 deletions dt-contacts/duplicates-merging.php
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,11 @@ public static function merge_posts( $post_type, $primary_post_id, $archiving_pos
return $archiving_post;
}

// A merge mutates both records, so the caller must be able to update both.
if ( !DT_Posts::can_update( $post_type, $primary_post_id ) || !DT_Posts::can_update( $post_type, $archiving_post_id ) ) {
return new WP_Error( __METHOD__, 'No permissions to merge these records', [ 'status' => 403 ] );
}

// Ignore specified fields
$ignored_fields = [
'post_date'
Expand Down
43 changes: 30 additions & 13 deletions dt-core/admin/admin-settings-endpoints.php
Original file line number Diff line number Diff line change
Expand Up @@ -787,20 +787,37 @@ public static function edit_translations( WP_REST_Request $request ) {
public function plugin_install( WP_REST_Request $request ) {
require_once( ABSPATH . 'wp-admin/includes/file.php' );
$params = $request->get_params();
$download_url = sanitize_text_field( wp_unslash( $params['download_url'] ) );
$download_url = sanitize_text_field( wp_unslash( $params['download_url'] ?? '' ) );

// Only fetch validated, externally-routable http(s) URLs. wp_http_validate_url() rejects
// loopback, private and link-local addresses, closing server-side request forgery.
if ( empty( $download_url )
|| ! wp_http_validate_url( $download_url )
|| ! in_array( wp_parse_url( $download_url, PHP_URL_SCHEME ), [ 'http', 'https' ], true ) ) {
return new WP_Error( 'invalid_download_url', 'Invalid download URL.', [ 'status' => 400 ] );
}

// wp_http_validate_url() does not cover link-local (e.g. 169.254.169.254 cloud metadata)
// or all reserved ranges; reject any host that resolves into a private or reserved network.
$resolved_ip = gethostbyname( (string) wp_parse_url( $download_url, PHP_URL_HOST ) );
if ( filter_var( $resolved_ip, FILTER_VALIDATE_IP )
&& ! filter_var( $resolved_ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
return new WP_Error( 'invalid_download_url', 'Invalid download URL.', [ 'status' => 400 ] );
}

set_time_limit( 0 );
$folder_name = explode( '/', $download_url );
$folder_name = get_home_path() . 'wp-content/plugins/' . $folder_name[4] . '.zip';
if ( $folder_name != '' ) {
//download the zip file to plugins
file_put_contents( $folder_name, file_get_contents( $download_url ) );
// get the absolute path to $file
$folder_name = realpath( $folder_name );
//unzip
WP_Filesystem();
$unzip = unzip_file( $folder_name, realpath( get_home_path() . 'wp-content/plugins/' ) );
//remove the file
unlink( $folder_name );
WP_Filesystem();

// Download to a temp file through the HTTP API rather than reading the URL directly.
$tmp_file = download_url( $download_url );
if ( is_wp_error( $tmp_file ) ) {
return $tmp_file;
}

$unzip = unzip_file( $tmp_file, realpath( get_home_path() . 'wp-content/plugins/' ) );
unlink( $tmp_file );
if ( is_wp_error( $unzip ) ) {
return $unzip;
}
return true;
}
Expand Down
8 changes: 4 additions & 4 deletions dt-core/admin/site-link-post-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -1419,7 +1419,7 @@ public static function decrypt_transfer_token( $transfer_token ) {
*/

if ( isset( $array['token'], $array['token_as_transfer_key'] ) && $array['token_as_transfer_key'] ){
if ( $array['token'] == $transfer_token ){
if ( hash_equals( (string) $array['token'], (string) $transfer_token ) ){
return $key;
}
} else {
Expand All @@ -1429,9 +1429,9 @@ public static function decrypt_transfer_token( $transfer_token ) {
$next = gmdate( 'Y-m-dH', strtotime( current_time( 'Y-m-d H:i:s', 1 ) . '+1 hour' ) );
$next_hour = md5( $key . $next );

if ( $current_hour == $transfer_token
|| $past_hour == $transfer_token
|| $next_hour == $transfer_token ){
if ( hash_equals( $current_hour, (string) $transfer_token )
|| hash_equals( $past_hour, (string) $transfer_token )
|| hash_equals( $next_hour, (string) $transfer_token ) ){

return $key;
}
Expand Down
4 changes: 2 additions & 2 deletions dt-core/core-endpoints.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ public function add_api_routes() {
*/
public static function get_settings() {
$user = wp_get_current_user();
if ( !$user ){
return new WP_Error( 'get_settings', 'Something went wrong. Are you a user?', [ 'status' => 400 ] );
if ( !$user->exists() ){
return new WP_Error( 'get_settings', 'Something went wrong. Are you a user?', [ 'status' => 401 ] );
}
$available_translations = dt_get_available_languages();
$post_types = DT_Posts::get_post_types();
Expand Down
18 changes: 17 additions & 1 deletion dt-core/dt-storage.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,28 @@ public static function upload_file( string $key_prefix = '', array $upload = [],
$tmp = $upload['tmp_name'] ?? '';
$type = $upload['type'] ?? '';

// Derive the real content type from the file's bytes rather than trusting the
// client-supplied value, and never store uploads as inline-executable content:
// anything that is not a known raster image is served as a download.
if ( $tmp && function_exists( 'finfo_open' ) ) {
$finfo = finfo_open( FILEINFO_MIME_TYPE );
if ( $finfo ) {
$detected = finfo_file( $finfo, $tmp );
finfo_close( $finfo );
if ( !empty( $detected ) ) {
$type = $detected;
}
}
}
$safe_inline_types = [ 'image/gif', 'image/jpeg', 'image/png', 'image/webp' ];
$content_type = in_array( strtolower( trim( $type ) ), $safe_inline_types, true ) ? $type : 'application/octet-stream';

try {
$client->putObject([
'Bucket' => $bucket,
'Key' => $key,
'Body' => fopen( $tmp, 'r' ),
'ContentType' => $type
'ContentType' => $content_type
]);

$uploaded_thumbnail_key = null;
Expand Down
95 changes: 95 additions & 0 deletions dt-core/jwt-rate-limit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php
/**
* Brute-force throttle for the JWT authentication token endpoint.
*
* The /jwt-auth/v1/token endpoint verifies credentials with no built-in rate
* limiting, so it can be used for unthrottled online password guessing. This
* caps repeated failures from a single client IP within a time window. It is
* scoped to the JWT token endpoint only — wp-login and XML-RPC are untouched —
* so it does not overlap with login-form limiters. Sites running a dedicated
* security plugin (e.g. Wordfence) that already covers REST authentication can
* disable it with: add_filter( 'dt_enable_jwt_login_throttle', '__return_false' ).
*/

if ( !defined( 'ABSPATH' ) ) {
exit;
}

/**
* The client IP used to bucket throttle counters. Defaults to the TCP peer
* (REMOTE_ADDR), which a request cannot spoof via headers. Sites behind a
* trusted reverse proxy can supply the forwarded client IP via the filter.
*
* @return string
*/
function dt_jwt_throttle_client_ip() {
$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
return (string) apply_filters( 'dt_jwt_throttle_client_ip', $ip );
}

/**
* Transient key holding the recent-failure count for the current client IP.
*
* @return string
*/
function dt_jwt_throttle_key() {
return 'dt_jwt_throttle_' . md5( dt_jwt_throttle_client_ip() );
}

/**
* Whether the current request targets the JWT token-mint endpoint (not /validate).
*
* @param WP_REST_Request|null $request
* @return bool
*/
function dt_is_jwt_token_request( $request = null ) {
if ( $request instanceof WP_REST_Request ) {
return $request->get_route() === '/jwt-auth/v1/token';
}
$path = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
return strpos( $path, 'jwt-auth/v1/token' ) !== false && strpos( $path, 'token/validate' ) === false;
}

/**
* Reject token requests from a client IP that has exceeded the failure threshold.
*/
function dt_jwt_throttle_block( $result, $server, $request ) {
if ( $result !== null ) {
return $result;
}
if ( !apply_filters( 'dt_enable_jwt_login_throttle', true ) || !dt_is_jwt_token_request( $request ) ) {
return $result;
}
$max = (int) apply_filters( 'dt_jwt_login_throttle_max_attempts', 10 );
if ( (int) get_transient( dt_jwt_throttle_key() ) >= $max ) {
return new WP_Error(
'jwt_auth_too_many_attempts',
__( 'Too many failed login attempts. Please try again later.', 'disciple_tools' ),
[ 'status' => 429 ]
);
}
return $result;
}
add_filter( 'rest_pre_dispatch', 'dt_jwt_throttle_block', 10, 3 );

/**
* Count a failed credential attempt against the client IP, for token requests only.
*/
function dt_jwt_throttle_record_failure() {
if ( !apply_filters( 'dt_enable_jwt_login_throttle', true ) || !dt_is_jwt_token_request() ) {
return;
}
$window = (int) apply_filters( 'dt_jwt_login_throttle_window', 15 * MINUTE_IN_SECONDS );
$key = dt_jwt_throttle_key();
set_transient( $key, (int) get_transient( $key ) + 1, $window );
}
add_action( 'wp_login_failed', 'dt_jwt_throttle_record_failure' );

/**
* Clear the failure counter once a token is successfully issued.
*/
function dt_jwt_throttle_clear_on_success( $data, $user ) {
delete_transient( dt_jwt_throttle_key() );
return $data;
}
add_filter( 'jwt_auth_token_before_dispatch', 'dt_jwt_throttle_clear_on_success', 10, 2 );
2 changes: 1 addition & 1 deletion dt-metrics/records/date-range-activity.php
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ public function date_range_activity( WP_REST_Request $request ){

} elseif ( $field_type == 'user_select' ){
$value = $params['value'];
$meta_value_sql = ( !empty( $value ) ? "AND meta_value LIKE 'user-" . $value['ID'] . "'" : "AND meta_value LIKE '%'" );
$meta_value_sql = ( !empty( $value['ID'] ) ? "AND meta_value LIKE 'user-" . intval( $value['ID'] ) . "'" : "AND meta_value LIKE '%'" );

} else {
$meta_value_sql = "AND meta_value LIKE '" . ( empty( $params['value'] ) ? '%' : esc_sql( $params['value'] ) ) . "'";
Expand Down
Loading
Loading