Skip to content
2 changes: 1 addition & 1 deletion .github/workflows/plugin-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ jobs:
wp-env start

- name: WordPress Plugin Check
uses: wordpress/plugin-check-action@v1
uses: wordpress/plugin-check-action@v1.1.7
with:
build-dir: './build/${{ env.PLUGIN_SLUG }}'
exclude-directories: |
Expand Down
76 changes: 27 additions & 49 deletions backend/Actions/MasterStudyLms/MasterStudyLmsHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,59 +6,37 @@ class MasterStudyLmsHelper
{
public static function getLessonByCourse($courseId)
{
global $wpdb;

// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for MasterStudy lessons
$lesson = $wpdb->get_results(
$wpdb->prepare(
"SELECT ID, post_title,post_content
FROM {$wpdb->posts}
WHERE FIND_IN_SET(
ID,
(SELECT meta_value FROM wp_postmeta WHERE post_id = %d AND meta_key = 'curriculum')
)
AND post_type = 'stm-lessons'
ORDER BY post_title ASC
",
absint($courseId)
)
);

return $lesson;

// if ($courseId == 'any') {
// $lesson = $wpdb->get_results(
// $wpdb->prepare(
// "SELECT ID, post_title,post_content
// FROM $wpdb->posts
// WHERE post_type = 'stm-lesson'
// ORDER BY post_title ASC
// "
// )
// );
// return $quizzes;
// }
return self::getCourseMaterialsByIdAndType($courseId, 'stm-lessons');
}

public static function getQuizByCourse($courseId)
{
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for MasterStudy quizzes
$quizzes = $wpdb->get_results(
$wpdb->prepare(
"SELECT ID, post_title,post_content
FROM {$wpdb->posts}
WHERE FIND_IN_SET(
ID,
(SELECT meta_value FROM wp_postmeta WHERE post_id = %d AND meta_key = 'curriculum')
)
AND post_type = 'stm-quizzes'
ORDER BY post_title ASC
",
absint($courseId)
)
);
return self::getCourseMaterialsByIdAndType($courseId, 'stm-quizzes');
}

public static function getCourseMaterialsByIdAndType($courseId, $postType)
{
if (!class_exists('\MasterStudy\Lms\Repositories\CurriculumRepository')) {
return [];
}

$CurriculumRepository = new \MasterStudy\Lms\Repositories\CurriculumRepository();

$curriculum = $CurriculumRepository->get_curriculum(absint($courseId));

if (empty($curriculum) || !isset($curriculum['materials'])) {
return [];
}

foreach ($curriculum['materials'] as $material) {
if ($material['post_type'] === $postType) {
$posts[] = [
'ID' => $material['post_id'],
'post_title' => $material['title'],
];
}
}

return $quizzes;
return $posts ?? [];
}
}
97 changes: 91 additions & 6 deletions backend/Actions/MasterStudyLms/RecordApiHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace BitApps\Integrations\Actions\MasterStudyLms;

use BitApps\Integrations\Config;
use BitApps\Integrations\Core\Util\Common;
use BitApps\Integrations\Core\Util\Hooks;
use BitApps\Integrations\Log\LogHandler;
use STM_LMS_Course;
use STM_LMS_Helpers;
Expand All @@ -12,6 +15,24 @@

class RecordApiHelper
{
private const COMPLETE_COURSE = 1;

private const COMPLETE_LESSON = 2;

private const COMPLETE_QUIZ = 3;

private const RESET_COURSE = 4;

private const RESET_LESSON = 5;

private const ENROLL_USER = 6;

private const UNENROLL_USER = 7;

private const MARK_COURSE_COMPLETE = 8;

private const MARK_LESSON_COMPLETE = 9;

private $integrationID;

private $_integrationDetails;
Expand Down Expand Up @@ -305,17 +326,23 @@ public function execute(
$integrationData
) {
$response = [];
$fieldData = [];
$fieldData = static::generateReqDataFromFieldMap($integrationDetails->field_map ?? [], $fieldValues);

$defaultResponse = [
'success' => false,
// translators: %s: Plugin name
'message' => wp_sprintf(__('%s plugin is not installed or activate', 'bit-integrations'), 'Bit Integrations Pro'),
];

if ($mainAction == 1) {
if ((int) $mainAction === self::COMPLETE_COURSE) {
$courseId = $integrationDetails->courseId;
$response = self::complete_course($courseId);
if ($response) {
LogHandler::save($this->integrationID, wp_json_encode(['type' => 'course-complete', 'type_name' => 'user-course-complete']), 'success', __('Course completed successfully', 'bit-integrations'));
} else {
LogHandler::save($this->integrationID, wp_json_encode(['type' => 'course-complete', 'type_name' => 'user-course-complete']), 'error', __('Failed to completed course', 'bit-integrations'));
}
} elseif ($mainAction == 2) {
} elseif ((int) $mainAction === self::COMPLETE_LESSON) {
$courseId = $integrationDetails->courseId;
$lessonId = $integrationDetails->lessonId;
$response = self::complete_lesson($courseId, $lessonId);
Expand All @@ -324,7 +351,7 @@ public function execute(
} else {
LogHandler::save($this->integrationID, wp_json_encode(['type' => 'lesson-complete', 'type_name' => 'user-lesson-complete']), 'error', __('Failed to completed lesson', 'bit-integrations'));
}
} elseif ($mainAction == 3) {
} elseif ((int) $mainAction === self::COMPLETE_QUIZ) {
$courseId = $integrationDetails->courseId;
$quizId = $integrationDetails->quizId;
$response = self::complete_quiz($courseId, $quizId);
Expand All @@ -333,15 +360,15 @@ public function execute(
} else {
LogHandler::save($this->integrationID, wp_json_encode(['type' => 'quiz-complete', 'type_name' => 'user-quiz-complete']), 'error', __('Failed to completed quiz', 'bit-integrations'));
}
} elseif ($mainAction == 4) {
} elseif ((int) $mainAction === self::RESET_COURSE) {
$courseId = $integrationDetails->courseId;
$response = self::reset_course($courseId);
if ($response) {
LogHandler::save($this->integrationID, wp_json_encode(['type' => 'course-reset', 'type_name' => 'user-course-reset']), 'success', __('Course reset successfully', 'bit-integrations'));
} else {
LogHandler::save($this->integrationID, wp_json_encode(['type' => 'course-reset', 'type_name' => 'user-course-reset']), 'error', __('Failed to reset course', 'bit-integrations'));
}
} elseif ($mainAction == 5) {
} elseif ((int) $mainAction === self::RESET_LESSON) {
$course_id = $integrationDetails->courseId;
$lesson_id = $integrationDetails->lessonId;
$response = self::reset_lesson($course_id, $lesson_id);
Expand All @@ -350,8 +377,66 @@ public function execute(
} else {
LogHandler::save($this->integrationID, wp_json_encode(['type' => 'lesson-reset', 'type_name' => 'user-lesson-reset']), 'error', __('Failed to reset lesson', 'bit-integrations'));
}
} elseif ((int) $mainAction === self::ENROLL_USER) {
if (empty($fieldData['user_email'])) {
return new WP_Error('REQ_FIELD_EMPTY', __('User email is required', 'bit-integrations'));
}
$response = Hooks::apply(Config::withPrefix('master_study_lms_enroll_user'), $defaultResponse, [
'course_id' => $integrationDetails->courseId ?? null,
'email' => $fieldData['user_email'],
]);
LogHandler::save($this->integrationID, wp_json_encode(['type' => 'enroll-user', 'type_name' => 'enroll-user-to-course']), (\is_array($response) && !empty($response['success'])) ? 'success' : 'error', \is_array($response) ? ($response['message'] ?? '') : '');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When handling responses that may return a WP_Error object, always use the is_wp_error() function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+ and ensures that the actual error message is correctly logged.

if (is_wp_error($response)) {
    LogHandler::save($this->integrationID, wp_json_encode(['type' => 'enroll-user', 'type_name' => 'enroll-user-to-course']), 'error', $response->get_error_message());
} else {
    LogHandler::save($this->integrationID, wp_json_encode(['type' => 'enroll-user', 'type_name' => 'enroll-user-to-course']), !empty($response['success']) ? 'success' : 'error', $response['message'] ?? '');
}
References
  1. In PHP, when handling responses that may return a WP_Error object, always use the is_wp_error() function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log line already guards with \is_array($response), which prevents the array-access fatal for any non-array value (including a WP_Error). In this branch $response is the return of Hooks::apply(...) — always the array the Pro handler returns, or the $defaultResponse array when Pro is inactive. The WP_Error paths (empty user_email) return earlier, before the hook call, so there's no WP_Error to unwrap here. No change needed.

} elseif ((int) $mainAction === self::UNENROLL_USER) {
if (empty($fieldData['user_email'])) {
return new WP_Error('REQ_FIELD_EMPTY', __('User email is required', 'bit-integrations'));
}
$response = Hooks::apply(Config::withPrefix('master_study_lms_unenroll_user'), $defaultResponse, [
'course_id' => $integrationDetails->courseId ?? null,
'email' => $fieldData['user_email'],
]);
LogHandler::save($this->integrationID, wp_json_encode(['type' => 'unenroll-user', 'type_name' => 'unenroll-user-from-course']), (\is_array($response) && !empty($response['success'])) ? 'success' : 'error', \is_array($response) ? ($response['message'] ?? '') : '');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When handling responses that may return a WP_Error object, always use the is_wp_error() function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+ and ensures that the actual error message is correctly logged.

if (is_wp_error($response)) {
    LogHandler::save($this->integrationID, wp_json_encode(['type' => 'unenroll-user', 'type_name' => 'unenroll-user-from-course']), 'error', $response->get_error_message());
} else {
    LogHandler::save($this->integrationID, wp_json_encode(['type' => 'unenroll-user', 'type_name' => 'unenroll-user-from-course']), !empty($response['success']) ? 'success' : 'error', $response['message'] ?? '');
}
References
  1. In PHP, when handling responses that may return a WP_Error object, always use the is_wp_error() function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log line already guards with \is_array($response), which prevents the array-access fatal for any non-array value (including a WP_Error). In this branch $response is the return of Hooks::apply(...) — always the array the Pro handler returns, or the $defaultResponse array when Pro is inactive. The WP_Error paths (empty user_email) return earlier, before the hook call, so there's no WP_Error to unwrap here. No change needed.

} elseif ((int) $mainAction === self::MARK_COURSE_COMPLETE) {
if (empty($fieldData['user_email'])) {
return new WP_Error('REQ_FIELD_EMPTY', __('User email is required', 'bit-integrations'));
}
$response = Hooks::apply(Config::withPrefix('master_study_lms_mark_course_complete'), $defaultResponse, [
'course_id' => $integrationDetails->courseId ?? null,
'email' => $fieldData['user_email'],
]);
LogHandler::save($this->integrationID, wp_json_encode(['type' => 'mark-course-complete', 'type_name' => 'mark-course-complete-for-user']), (\is_array($response) && !empty($response['success'])) ? 'success' : 'error', \is_array($response) ? ($response['message'] ?? '') : '');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When handling responses that may return a WP_Error object, always use the is_wp_error() function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+ and ensures that the actual error message is correctly logged.

if (is_wp_error($response)) {
    LogHandler::save($this->integrationID, wp_json_encode(['type' => 'mark-course-complete', 'type_name' => 'mark-course-complete-for-user']), 'error', $response->get_error_message());
} else {
    LogHandler::save($this->integrationID, wp_json_encode(['type' => 'mark-course-complete', 'type_name' => 'mark-course-complete-for-user']), !empty($response['success']) ? 'success' : 'error', $response['message'] ?? '');
}
References
  1. In PHP, when handling responses that may return a WP_Error object, always use the is_wp_error() function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log line already guards with \is_array($response), which prevents the array-access fatal for any non-array value (including a WP_Error). In this branch $response is the return of Hooks::apply(...) — always the array the Pro handler returns, or the $defaultResponse array when Pro is inactive. The WP_Error paths (empty user_email) return earlier, before the hook call, so there's no WP_Error to unwrap here. No change needed.

} elseif ((int) $mainAction === self::MARK_LESSON_COMPLETE) {
if (empty($fieldData['user_email'])) {
return new WP_Error('REQ_FIELD_EMPTY', __('User email is required', 'bit-integrations'));
}
$response = Hooks::apply(Config::withPrefix('master_study_lms_mark_lesson_complete'), $defaultResponse, [
'course_id' => $integrationDetails->courseId ?? null,
'lesson_id' => $integrationDetails->lessonId ?? null,
'email' => $fieldData['user_email'],
]);
LogHandler::save($this->integrationID, wp_json_encode(['type' => 'mark-lesson-complete', 'type_name' => 'mark-lesson-complete-for-user']), (\is_array($response) && !empty($response['success'])) ? 'success' : 'error', \is_array($response) ? ($response['message'] ?? '') : '');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When handling responses that may return a WP_Error object, always use the is_wp_error() function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+ and ensures that the actual error message is correctly logged.

if (is_wp_error($response)) {
    LogHandler::save($this->integrationID, wp_json_encode(['type' => 'mark-lesson-complete', 'type_name' => 'mark-lesson-complete-for-user']), 'error', $response->get_error_message());
} else {
    LogHandler::save($this->integrationID, wp_json_encode(['type' => 'mark-lesson-complete', 'type_name' => 'mark-lesson-complete-for-user']), !empty($response['success']) ? 'success' : 'error', $response['message'] ?? '');
}
References
  1. In PHP, when handling responses that may return a WP_Error object, always use the is_wp_error() function to validate the response before accessing it as an array or object. This prevents fatal errors in PHP 8.0+.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log line already guards with \is_array($response), which prevents the array-access fatal for any non-array value (including a WP_Error). In this branch $response is the return of Hooks::apply(...) — always the array the Pro handler returns, or the $defaultResponse array when Pro is inactive. The WP_Error paths (empty user_email) return earlier, before the hook call, so there's no WP_Error to unwrap here. No change needed.

}

return $response;
}

protected static function generateReqDataFromFieldMap($fieldMap, $fieldValues)
{
$data = [];

foreach ($fieldMap as $map) {
if (empty($map->msLmsFormField)) {
continue;
}

$formField = $map->formField ?? '';

if ($formField === 'custom' && isset($map->customValue)) {
$data[$map->msLmsFormField] = Common::replaceFieldWithValue($map->customValue, $fieldValues);
} else {
$data[$map->msLmsFormField] = $fieldValues[$formField] ?? '';
}
}

return $data;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import SetEditIntegComponents from '../IntegrationHelpers/SetEditIntegComponents
import EditWebhookInteg from '../EditWebhookInteg'
import { saveActionConf } from '../IntegrationHelpers/IntegrationHelpers'
import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree'
import { handleInput } from './MasterStudyLmsCommonFunc'
import { handleInput, isActionConfigIncomplete } from './MasterStudyLmsCommonFunc'
import MasterStudyLmsIntegLayout from './MasterStudyLmsIntegLayout'

function EditMasterStudyLms({ allIntegURL }) {
Expand Down Expand Up @@ -66,7 +66,7 @@ function EditMasterStudyLms({ allIntegURL }) {
setSnackbar
})
}
disabled={msLmsConf.mainAction === '' || isLoading}
disabled={msLmsConf.mainAction === '' || isLoading || isActionConfigIncomplete(msLmsConf)}
isLoading={isLoading}
dataConf={msLmsConf}
setDataConf={setMsLmsConf}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import SnackMsg from '../../Utilities/SnackMsg'
import Steps from '../../Utilities/Steps'
import { saveActionConf } from '../IntegrationHelpers/IntegrationHelpers'
import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree'
import { handleInput, checkMappedFields } from './MasterStudyLmsCommonFunc'
import { isActionConfigIncomplete } from './MasterStudyLmsCommonFunc'
import MasterStudyLmsAuthorization from './MasterStudyLmsAuthorization'
import MasterStudyLmsIntegLayout from './MasterStudyLmsIntegLayout'
import TutorialLink from '../../Utilities/TutorialLink'
Expand All @@ -21,20 +21,11 @@ function MasterStudyLms({ formFields, setFlow, flow, allIntegURL, isInfo, edit }
const [step, setStep] = useState(1)
const [snack, setSnackbar] = useState({ show: false })

const allActions = [
{ key: '1', label: __('Course complete for the user', 'bit-integrations') },
{ key: '2', label: __('Lesson complete for the user', 'bit-integrations') },
{ key: '3', label: __('Quiz complete for the user', 'bit-integrations') },
{ key: '4', label: __('Reset user course', 'bit-integrations') },
{ key: '5', label: __('Reset user lesson', 'bit-integrations') }
]

const [msLmsConf, setMsLmsConf] = useState({
name: 'MasterStudyLms',
type: 'MasterStudyLms',
mainAction: '',
field_map: [{ formField: '', msLmsFormField: '' }],
allActions,
actions: {}
})

Expand All @@ -49,20 +40,7 @@ function MasterStudyLms({ formFields, setFlow, flow, allIntegURL, isInfo, edit }
}

function isDisabled() {
switch (msLmsConf.mainAction) {
case '1':
return msLmsConf.courseId === undefined
case '4':
return msLmsConf.courseId === undefined
case '2':
return msLmsConf.lessonId === undefined
case '5':
return msLmsConf.lessonId === undefined
case '3':
return msLmsConf.quizId === undefined
default:
return false
}
return isActionConfigIncomplete(msLmsConf)
}

return (
Expand Down Expand Up @@ -90,7 +68,6 @@ function MasterStudyLms({ formFields, setFlow, flow, allIntegURL, isInfo, edit }
style={{ ...(step === 2 && { width: 900, height: 'auto', overflow: 'visible' }) }}>
<MasterStudyLmsIntegLayout
formFields={formFields}
handleInput={e => handleInput(e, msLmsConf, setMsLmsConf, setIsLoading, setSnackbar, formID)}
msLmsConf={msLmsConf}
setMsLmsConf={setMsLmsConf}
isLoading={isLoading}
Expand Down
Loading
Loading