diff --git a/.env.example b/.env.example index e6902671c..90e75aebd 100644 --- a/.env.example +++ b/.env.example @@ -42,6 +42,19 @@ ADMIN_LTI_NAME="UDOIT 3 Admin" USE_DEVELOPMENT_AUTH="no" VERSION_NUMBER="3.3.1" +# Define which accessibility checker to use +# Available options: "phpally", "equalaccess_local", "equalaccess_lambda" +ACCESSIBILITY_CHECKER="phpally" + +# NOTE: When using a lambda function with equal access, +# you need to define the following in a separate .env.local: +# AWS_ACCESS_KEY_ID= +# AWS_SECRET_ACCESS_KEY= +# AWS_REGION= +# AWS_SERVICE= +# AWS_HOST=abcdefghi.execute-api.us-east-1.amazonaws.com +# AWS_ENDPOINT=https://abcdefghi.execute-api.us-east-1.amazonaws.com/endpoint/generate-accessibility-report + ###> symfony/messenger ### MESSENGER_TRANSPORT_DSN=doctrine://default @@ -68,6 +81,7 @@ PHPALLY_SUGGESTION_RULES=" RedirectedLink, EmbedTagDetected, IframeNotHandled, + TableNotEmpty " # Rules that are easiest to tackle when using UDOIT. Comma-separated list of rule IDs. EASY_FIX_RULES=" diff --git a/assets/js/Components/Admin/UsersPage.js b/assets/js/Components/Admin/UsersPage.js index 3df5dd450..8648f3f67 100644 --- a/assets/js/Components/Admin/UsersPage.js +++ b/assets/js/Components/Admin/UsersPage.js @@ -162,7 +162,6 @@ class UsersPage extends React.Component { .then((responseStr) => responseStr.json()) .then((response) => { let users = this.state.users - console.log('response', response); if (response && response.id) { const ind = users.findIndex((el) => { el.id === response.id }) users[ind] = response @@ -183,4 +182,4 @@ class UsersPage extends React.Component { } } -export default UsersPage; \ No newline at end of file +export default UsersPage; diff --git a/assets/js/Components/App.js b/assets/js/Components/App.js index 6985d55b8..b73b593bd 100644 --- a/assets/js/Components/App.js +++ b/assets/js/Components/App.js @@ -39,6 +39,7 @@ class App extends React.Component { this.handleIssueSave = this.handleIssueSave.bind(this) this.handleFileSave = this.handleFileSave.bind(this) this.handleCourseRescan = this.handleCourseRescan.bind(this) + this.handleFullCourseRescan = this.handleFullCourseRescan.bind(this) this.handleNewReport = this.handleNewReport.bind(this) this.resizeFrame = this.resizeFrame.bind(this) } @@ -53,6 +54,7 @@ class App extends React.Component { navigation={this.state.navigation} handleNavigation={this.handleNavigation} handleCourseRescan={this.handleCourseRescan} + handleFullCourseRescan={this.handleFullCourseRescan} handleModal={this.handleModal} /> {(('welcome' !== this.state.navigation) && ('summary' !== this.state.navigation)) && @@ -148,6 +150,11 @@ class App extends React.Component { return api.scanCourse(this.settings.course.id) } + fullRescan() { + let api = new Api(this.settings) + return api.fullRescan(this.settings.course.id) + } + disableReview = () => { return this.state.syncComplete && !this.state.disableReview } @@ -162,6 +169,16 @@ class App extends React.Component { this.forceUpdate() } + handleFullCourseRescan() { + if (this.state.hasNewReport) { + this.setState({ hasNewReport: false, syncComplete: false }) + this.fullRescan() + .then((response) => response.json()) + .then(this.handleNewReport) + } + this.forceUpdate() + } + handleNewReport(data) { let report = this.state.report let hasNewReport = this.state.hasNewReport @@ -184,7 +201,6 @@ class App extends React.Component { }); } if (data.data && data.data.id) { - console.log('new data', data.data) report = data.data hasNewReport = true } @@ -217,14 +233,20 @@ class App extends React.Component { } handleIssueSave(newIssue, newReport) { - let { report } = this.state - report = {...report, ...newReport} + const oldReport = this.state.report; + + const report = { ...oldReport, ...newReport }; if (report && Array.isArray(report.issues)) { - report.issues = report.issues.map(issue => (issue.id == newIssue.id) ? newIssue : issue) + // Combine backend issues with frontend issue state + report.issues = report.issues.map((issue) => { + if (issue.id === newIssue.id) return newIssue; + const oldIssue = oldReport.issues.find((oldReportIssue) => oldReportIssue.id === issue.id); + return oldIssue !== undefined ? { ...oldIssue, ...issue } : issue; + }); } - this.setState({ report }) + this.setState({ report }); } handleFileSave(newFile, newReport) { diff --git a/assets/js/Components/Constants.js b/assets/js/Components/Constants.js index b89d355b3..06c880ccc 100644 --- a/assets/js/Components/Constants.js +++ b/assets/js/Components/Constants.js @@ -31,9 +31,13 @@ export const issueRuleIds = [ "RedirectedLink", "TableDataShouldHaveTableHeader", "TableHeaderShouldHaveScope", + "TableNotEmpty", "VideoCaptionsMatchCourseLanguage", "VideoEmbedCheck", "VideoProvidesCaptions", "VideosEmbeddedOrLinkedNeedCaptions", - "VideosHaveAutoGeneratedCaptions" + "VideosHaveAutoGeneratedCaptions", + + "img_alt_misuse", + "text_contrast_sufficient", ] diff --git a/assets/js/Components/ContentPage.js b/assets/js/Components/ContentPage.js index 38b4d5cfc..54124c104 100644 --- a/assets/js/Components/ContentPage.js +++ b/assets/js/Components/ContentPage.js @@ -77,17 +77,6 @@ class ContentPage extends React.Component { } } - static getDerivedStateFromProps(props, state) { - const stateActiveIssue = state.activeIssue - const propsActiveIssue = stateActiveIssue && props.report.issues[stateActiveIssue.id] - if(propsActiveIssue && propsActiveIssue.status !== stateActiveIssue.status) { - return { - activeIssue: propsActiveIssue - } - } - return null - } - handleSearchTerm = (e, val) => { this.setState({searchTerm: val, filteredIssues: [], tableSettings: Object.assign({}, this.state.tableSettings, {pageNum: 0})}); } @@ -102,9 +91,17 @@ class ContentPage extends React.Component { } handleCloseButton = () => { + const newReport = { ...this.props.report }; + newReport.issues = newReport.issues.map((issue) => { + issue.recentlyResolved = false; + issue.recentlyUpdated = false; + return issue; + }); + this.setState({ + report: newReport, modalOpen: false - }) + }); } handleTrayToggle = (e, val) => { diff --git a/assets/js/Components/FilesModal.js b/assets/js/Components/FilesModal.js index 993e24afb..f34e5888b 100644 --- a/assets/js/Components/FilesModal.js +++ b/assets/js/Components/FilesModal.js @@ -36,6 +36,7 @@ class FilesModal extends React.Component { this.handleDropAccept = this.handleDropAccept.bind(this) this.handleDropReject = this.handleDropReject.bind(this) this.handleFilePost = this.handleFilePost.bind(this) + this.setAcceptType = this.setAcceptType.bind(this) } componentDidUpdate(prevProps, prevState) { @@ -59,6 +60,46 @@ class FilesModal extends React.Component { return -1; } + setAcceptType(file) { + let accept = [] + + switch(file.fileType) { + case "doc": + accept = ["doc", "docx"] + break + + case "ppt": + accept = ["ppt", "pptx"] + break + + case "xls": + accept = ["xls", "xlsx"] + break + + default: + accept = file.fileType + break + } + + let extension = file.fileName.slice(-4) + + switch(extension) { + case "xlsx": + accept = "xlsx" + break + + case "pptx": + accept = "pptx" + break + + case "docx": + accept = "docx" + break + } + + return accept + } + // Handler for the previous and next buttons on the modal // Will wrap around if the index goes out of bounds handleFileChange(newIndex) { @@ -80,7 +121,8 @@ class FilesModal extends React.Component { } render() { - const { activeFile } = this.props + let { activeFile } = this.props + activeFile.acceptType = this.setAcceptType(activeFile) let activeIndex = this.findActiveIndex() return ( @@ -121,7 +163,7 @@ class FilesModal extends React.Component { {this.props.t('label.replace')} {this.props.t('label.replace.desc')} 1024 * 1024 * 10) { + this.addMessage({severity: 'error', message: this.props.t('msg.file.replace.file_size'), timeout: 5000}) + this.setState({ replaceFileObj: null }) + this.forceUpdate() + return + } + this.setState({ replaceFileObj: file }) } diff --git a/assets/js/Components/FilesPage.js b/assets/js/Components/FilesPage.js index 5c2217d3a..70807e2ad 100644 --- a/assets/js/Components/FilesPage.js +++ b/assets/js/Components/FilesPage.js @@ -328,4 +328,4 @@ class FilesPage extends React.Component { } } -export default FilesPage; \ No newline at end of file +export default FilesPage; diff --git a/assets/js/Components/Forms/HeadingEmptyForm.js b/assets/js/Components/Forms/HeadingEmptyForm.js index 13cc13420..0df724720 100644 --- a/assets/js/Components/Forms/HeadingEmptyForm.js +++ b/assets/js/Components/Forms/HeadingEmptyForm.js @@ -78,17 +78,15 @@ export default class HeadingEmptyForm extends React.Component { if(!this.state.deleteHeader) { this.checkTextNotEmpty() } - if (this.formErrors.length > 0) { this.setState({ textInputErrors: this.formErrors }) - } - + } + else { this.setState({ textInputErrors: []}) let issue = this.props.activeIssue issue.newHtml = this.processHtml() - console.log(issue.newHtml) this.props.handleIssueSave(issue) } } diff --git a/assets/js/Components/Forms/TableHeaders.js b/assets/js/Components/Forms/TableHeaders.js index c18bcc3d9..bcc47eeec 100644 --- a/assets/js/Components/Forms/TableHeaders.js +++ b/assets/js/Components/Forms/TableHeaders.js @@ -46,7 +46,6 @@ export default class TableHeaders extends React.Component { } handleSubmit() { - console.log('activeIssue', this.props.activeIssue) let issue = this.props.activeIssue issue.newHtml = this.fixHeaders() this.props.handleIssueSave(issue) diff --git a/assets/js/Components/Header.js b/assets/js/Components/Header.js index 5e860b805..ff5f492c6 100644 --- a/assets/js/Components/Header.js +++ b/assets/js/Components/Header.js @@ -44,6 +44,7 @@ class Header extends React.Component { {/* this.handleMoreNav('settings')}>{this.props.t('menu.settings')} */} {this.props.t('menu.scan_course')} + {this.props.t('menu.full_rescan')} {this.props.t('menu.download_pdf')} diff --git a/assets/js/Services/Api.js b/assets/js/Services/Api.js index 3ace069d9..330989c9f 100644 --- a/assets/js/Services/Api.js +++ b/assets/js/Services/Api.js @@ -13,6 +13,7 @@ export default class Api { adminCourses: '/api/admin/courses/account/{account}/term/{term}', scanContent: '/api/sync/content/{contentItem}', scanCourse: '/api/sync/{course}', + fullRescan: '/api/sync/rescan/{course}', scanIssue: '/api/issues/{issue}/scan', adminReport: '/api/admin/courses/{course}/reports/latest', adminReportHistory: '/api/admin/reports/account/{account}/term/{term}', @@ -233,6 +234,21 @@ export default class Api { }) } + fullRescan(courseId) + { + const authToken = this.getAuthToken() + let url = `${this.apiUrl}${this.endpoints.fullRescan}` + url = url.replace('{course}', courseId) + + return fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-AUTH-TOKEN': authToken, + }, + }) + } + scanContent(contentId) { const authToken = this.getAuthToken() diff --git a/assets/js/Services/Html.js b/assets/js/Services/Html.js index 436d64511..e8c59ffdd 100644 --- a/assets/js/Services/Html.js +++ b/assets/js/Services/Html.js @@ -37,8 +37,6 @@ export function setInnerText(element, newText) { const children = element.childNodes let textNodeFound = false - console.log(children) - children.forEach(node => { if(node.nodeType === Node.TEXT_NODE) { if(textNodeFound != true) { diff --git a/assets/js/Services/Ufixit.js b/assets/js/Services/Ufixit.js index 5bef00318..681cb1251 100644 --- a/assets/js/Services/Ufixit.js +++ b/assets/js/Services/Ufixit.js @@ -29,6 +29,9 @@ const UfixitForms = { VideoCaptionsMatchCourseLanguage: Video, VideosEmbeddedOrLinkedNeedCaptions: Video, VideosHaveAutoGeneratedCaptions: Video, + + img_alt_misuse: AltText, + text_contrast_sufficient: ContrastForm, } export function returnIssueForm(activeIssue) { diff --git a/assets/js/getInitialData.js b/assets/js/getInitialData.js index c9deca9e6..107f76fb2 100644 --- a/assets/js/getInitialData.js +++ b/assets/js/getInitialData.js @@ -16,7 +16,7 @@ export default function getInitialData() { data = JSON.parse(settingsElement.textContent) if (Object.keys(data).length > 0) { - console.log('data', data) + console.log('Data was found and loaded!') } else { console.error('No data loaded!') } diff --git a/build/nginx/deploy.conf b/build/nginx/deploy.conf index 48167ce8e..b82394454 100644 --- a/build/nginx/deploy.conf +++ b/build/nginx/deploy.conf @@ -9,6 +9,7 @@ server { rewrite ^/udoit3/(.*)$ /$1 break; try_files $uri @symfonyFront; + client_max_body_size 10M; } set $symfonyRoot /var/www/html/public; @@ -29,4 +30,4 @@ server { } error_log /var/log/nginx/project_error.log; access_log /var/log/nginx/project_access.log; -} \ No newline at end of file +} diff --git a/build/nginx/local.conf b/build/nginx/local.conf index 65ebd3eb8..4f037e1e4 100644 --- a/build/nginx/local.conf +++ b/build/nginx/local.conf @@ -9,6 +9,7 @@ server { rewrite ^/udoit3/(.*)$ /$1 break; try_files $uri @symfonyFront; + client_max_body_size 10M; } set $symfonyRoot /var/www/html/public; @@ -29,4 +30,4 @@ server { } error_log /var/log/nginx/project_error.log; access_log /var/log/nginx/project_access.log; -} \ No newline at end of file +} diff --git a/build/nginx/php-custom.ini b/build/nginx/php-custom.ini index 7c7de96e7..b5d5e8d37 100644 --- a/build/nginx/php-custom.ini +++ b/build/nginx/php-custom.ini @@ -1,2 +1,4 @@ max_execution_time = 180 memory_limit = 800M +upload_max_filesize = 10M +post_max_size = 10M diff --git a/composer.json b/composer.json index f0f4417b4..52a570ea6 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "ext-ctype": "*", "ext-iconv": "*", "ext-sodium": "*", + "aws/aws-sdk-php": "^3.322", "composer/package-versions-deprecated": "1.11.99.3", "doctrine/annotations": "^1.0", "doctrine/doctrine-bundle": "^2.4", diff --git a/composer.lock b/composer.lock index 06c8e3a5d..d17f04703 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,160 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8a1363e63701508bc0a787312cbf03ad", + "content-hash": "caf1ec6a614cce926108bac28d1d1456", "packages": [ + { + "name": "aws/aws-crt-php", + "version": "v1.2.6", + "source": { + "type": "git", + "url": "https://github.com/awslabs/aws-crt-php.git", + "reference": "a63485b65b6b3367039306496d49737cf1995408" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/a63485b65b6b3367039306496d49737cf1995408", + "reference": "a63485b65b6b3367039306496d49737cf1995408", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" + } + ], + "description": "AWS Common Runtime for PHP", + "homepage": "https://github.com/awslabs/aws-crt-php", + "keywords": [ + "amazon", + "aws", + "crt", + "sdk" + ], + "support": { + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.6" + }, + "time": "2024-06-13T17:21:28+00:00" + }, + { + "name": "aws/aws-sdk-php", + "version": "3.322.3", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "9a94b9a123e0a14cacd72a34c24624ab728aa398" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/9a94b9a123e0a14cacd72a34c24624ab728aa398", + "reference": "9a94b9a123e0a14cacd72a34c24624ab728aa398", + "shasum": "" + }, + "require": { + "aws/aws-crt-php": "^1.2.3", + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", + "guzzlehttp/promises": "^1.4.0 || ^2.0", + "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "mtdowling/jmespath.php": "^2.6", + "php": ">=7.2.5", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "composer/composer": "^1.10.22", + "dms/phpunit-arraysubset-asserts": "^0.4.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", + "nette/neon": "^2.3", + "paragonie/random_compat": ">= 2", + "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", + "psr/cache": "^1.0", + "psr/simple-cache": "^1.0", + "sebastian/comparator": "^1.2.3 || ^4.0", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Aws\\": "src/" + }, + "exclude-from-classmap": [ + "src/data/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "support": { + "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", + "issues": "https://github.com/aws/aws-sdk-php/issues", + "source": "https://github.com/aws/aws-sdk-php/tree/3.322.3" + }, + "time": "2024-09-23T18:11:39+00:00" + }, { "name": "composer/package-versions-deprecated", "version": "1.11.99.3", @@ -427,16 +579,16 @@ }, { "name": "doctrine/dbal", - "version": "3.8.6", + "version": "3.9.0", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "b7411825cf7efb7e51f9791dea19d86e43b399a1" + "reference": "d8f68ea6cc00912e5313237130b8c8decf4d28c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/b7411825cf7efb7e51f9791dea19d86e43b399a1", - "reference": "b7411825cf7efb7e51f9791dea19d86e43b399a1", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/d8f68ea6cc00912e5313237130b8c8decf4d28c6", + "reference": "d8f68ea6cc00912e5313237130b8c8decf4d28c6", "shasum": "" }, "require": { @@ -452,12 +604,12 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.11.5", + "phpstan/phpstan": "1.11.7", "phpstan/phpstan-strict-rules": "^1.6", - "phpunit/phpunit": "9.6.19", + "phpunit/phpunit": "9.6.20", "psalm/plugin-phpunit": "0.18.4", "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.10.1", + "squizlabs/php_codesniffer": "3.10.2", "symfony/cache": "^5.4|^6.0|^7.0", "symfony/console": "^4.4|^5.4|^6.0|^7.0", "vimeo/psalm": "4.30.0" @@ -520,7 +672,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.8.6" + "source": "https://github.com/doctrine/dbal/tree/3.9.0" }, "funding": [ { @@ -536,7 +688,7 @@ "type": "tidelift" } ], - "time": "2024-06-19T10:38:17+00:00" + "time": "2024-08-15T07:34:42+00:00" }, { "name": "doctrine/deprecations", @@ -1429,16 +1581,16 @@ }, { "name": "doctrine/sql-formatter", - "version": "1.4.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/doctrine/sql-formatter.git", - "reference": "d1ac84aef745c69ea034929eb6d65a6908b675cc" + "reference": "7f83911cc5eba870de7ebb11283972483f7e2891" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/d1ac84aef745c69ea034929eb6d65a6908b675cc", - "reference": "d1ac84aef745c69ea034929eb6d65a6908b675cc", + "url": "https://api.github.com/repos/doctrine/sql-formatter/zipball/7f83911cc5eba870de7ebb11283972483f7e2891", + "reference": "7f83911cc5eba870de7ebb11283972483f7e2891", "shasum": "" }, "require": { @@ -1478,9 +1630,9 @@ ], "support": { "issues": "https://github.com/doctrine/sql-formatter/issues", - "source": "https://github.com/doctrine/sql-formatter/tree/1.4.0" + "source": "https://github.com/doctrine/sql-formatter/tree/1.4.1" }, - "time": "2024-05-08T08:12:09+00:00" + "time": "2024-08-05T20:32:22+00:00" }, { "name": "egulias/email-validator", @@ -2250,32 +2402,31 @@ }, { "name": "knpuniversity/oauth2-client-bundle", - "version": "v2.18.1", + "version": "v2.18.2", "source": { "type": "git", "url": "https://github.com/knpuniversity/oauth2-client-bundle.git", - "reference": "1d59f49f164805b45f95f92cf743781bc2ba7d2b" + "reference": "0f8db87efa064bc1800315c027a80b53ef935524" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/knpuniversity/oauth2-client-bundle/zipball/1d59f49f164805b45f95f92cf743781bc2ba7d2b", - "reference": "1d59f49f164805b45f95f92cf743781bc2ba7d2b", + "url": "https://api.github.com/repos/knpuniversity/oauth2-client-bundle/zipball/0f8db87efa064bc1800315c027a80b53ef935524", + "reference": "0f8db87efa064bc1800315c027a80b53ef935524", "shasum": "" }, "require": { "league/oauth2-client": "^2.0", "php": ">=8.1", - "symfony/dependency-injection": "^4.4|^5.0|^6.0|^7.0", - "symfony/framework-bundle": "^4.4|^5.0|^6.0|^7.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0|^7.0", - "symfony/routing": "^4.4|^5.0|^6.0|^7.0" + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0" }, "require-dev": { "league/oauth2-facebook": "^1.1|^2.0", - "phpstan/phpstan": "^1.0", - "symfony/phpunit-bridge": "^5.3.1|^6.0|^7.0", - "symfony/security-guard": "^4.4|^5.0|^6.0|^7.0", - "symfony/yaml": "^4.4|^5.0|^6.0|^7.0" + "symfony/phpunit-bridge": "^5.4|^6.0|^7.0", + "symfony/security-guard": "^5.4", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "suggest": { "symfony/security-guard": "For integration with Symfony's Guard Security layer" @@ -2304,9 +2455,9 @@ ], "support": { "issues": "https://github.com/knpuniversity/oauth2-client-bundle/issues", - "source": "https://github.com/knpuniversity/oauth2-client-bundle/tree/v2.18.1" + "source": "https://github.com/knpuniversity/oauth2-client-bundle/tree/v2.18.2" }, - "time": "2024-02-14T17:41:28+00:00" + "time": "2024-08-12T15:26:07+00:00" }, { "name": "laminas/laminas-code", @@ -2782,6 +2933,72 @@ }, "time": "2023-05-03T06:19:36+00:00" }, + { + "name": "mtdowling/jmespath.php", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^3.0.3", + "phpunit/phpunit": "^8.5.33" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "files": [ + "src/JmesPath.php" + ], + "psr-4": { + "JmesPath\\": "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" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "support": { + "issues": "https://github.com/jmespath/jmespath.php/issues", + "source": "https://github.com/jmespath/jmespath.php/tree/2.8.0" + }, + "time": "2024-09-04T18:46:31+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.12.0", @@ -3492,16 +3709,16 @@ }, { "name": "psr/log", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + "reference": "79dff0b268932c640297f5208d6298f71855c03e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e", + "reference": "79dff0b268932c640297f5208d6298f71855c03e", "shasum": "" }, "require": { @@ -3536,9 +3753,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" + "source": "https://github.com/php-fig/log/tree/3.0.1" }, - "time": "2021-07-14T16:46:02+00:00" + "time": "2024-08-21T13:31:24+00:00" }, { "name": "ralouphie/getallheaders", @@ -9280,16 +9497,16 @@ }, { "name": "twig/twig", - "version": "v3.10.3", + "version": "v3.11.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "67f29781ffafa520b0bbfbd8384674b42db04572" + "reference": "e80fb8ebba85c7341a97a9ebf825d7fd4b77708d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/67f29781ffafa520b0bbfbd8384674b42db04572", - "reference": "67f29781ffafa520b0bbfbd8384674b42db04572", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/e80fb8ebba85c7341a97a9ebf825d7fd4b77708d", + "reference": "e80fb8ebba85c7341a97a9ebf825d7fd4b77708d", "shasum": "" }, "require": { @@ -9297,7 +9514,8 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-mbstring": "^1.3", - "symfony/polyfill-php80": "^1.22" + "symfony/polyfill-php80": "^1.22", + "symfony/polyfill-php81": "^1.29" }, "require-dev": { "psr/container": "^1.0|^2.0", @@ -9343,7 +9561,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.10.3" + "source": "https://github.com/twigphp/Twig/tree/v3.11.0" }, "funding": [ { @@ -9355,7 +9573,7 @@ "type": "tidelift" } ], - "time": "2024-05-16T10:04:27+00:00" + "time": "2024-08-08T16:15:16+00:00" }, { "name": "ucfopen/phpally", diff --git a/composer.phar b/composer.phar new file mode 100755 index 000000000..15c4a2081 Binary files /dev/null and b/composer.phar differ diff --git a/package.json b/package.json index 716cc14d7..41e82c343 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@instructure/ui-toggle-details": "^7.5.0", "@instructure/ui-tray": "^7.5.0", "@reactchartjs/react-chart.js": "^1.0.0-rc.3", - "axios": "^0.21.2", + "axios": "^1.6.0", "babel-loader": "^8.2.2", "chart.js": "^2.9.4", "moment": "^2.29.4", diff --git a/src/Controller/IssuesController.php b/src/Controller/IssuesController.php index d5998cc60..92637e968 100644 --- a/src/Controller/IssuesController.php +++ b/src/Controller/IssuesController.php @@ -158,6 +158,7 @@ public function markAsReviewed(Request $request, LmsPostService $lmsPost, Utilit } // Rescan an issue in PhpAlly + // TODO: implement equal access into this #[Route('/api/issues/{issue}/scan', name: 'scan_issue')] public function scanIssue(Issue $issue, PhpAllyService $phpAlly, UtilityService $util, EqualAccessService $equalAccess) { diff --git a/src/Controller/SyncController.php b/src/Controller/SyncController.php index 3c3e1ecfc..b8a9f6e1b 100644 --- a/src/Controller/SyncController.php +++ b/src/Controller/SyncController.php @@ -8,8 +8,7 @@ use App\Response\ApiResponse; use App\Services\LmsApiService; use App\Services\LmsFetchService; -use App\Services\PhpAllyService; -use App\Services\EqualAccessService; +use App\Services\ScannerService; use App\Services\UtilityService; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Routing\Annotation\Route; @@ -72,8 +71,61 @@ public function requestSync(Course $course, LmsFetchService $lmsFetch) return new JsonResponse($response); } + #[Route('/api/sync/rescan/{course}', name: 'full_rescan')] + public function fullCourseRescan(Course $course, LmsFetchService $lmsFetch) { + $response = new ApiResponse(); + $user = $this->getUser(); + $reportArr = false; + + try { + if (!$this->userHasCourseAccess($course)) { + throw new \Exception('msg.no_permissions'); + } + if ($course->isDirty()) { + throw new \Exception('msg.course_scanning'); + } + if (!$course->isActive()) { + $response->setData(0); + throw new \Exception('msg.sync.course_inactive'); + } + + $prevReport = $course->getPreviousReport(); + + $course->removeAllReports(); + + $lmsFetch->refreshLmsContent($course, $user); + + $report = $course->getLatestReport(); + + if (!$report) { + throw new \Exception('msg.no_report_created'); + } + + $reportArr = $report->toArray(); + $reportArr['files'] = $course->getFileItems(); + $reportArr['issues'] = $course->getAllIssues(); + $reportArr['contentItems'] = $course->getContentItems(); + + $response->setData($reportArr); + + if ($prevReport && ($prevReport->getIssueCount() == $report->getIssueCount())) { + $response->addMessage('msg.no_new_content', 'success', 5000); + } else { + $response->addMessage('msg.new_content', 'success', 5000); + } + } catch (\Exception $e) { + if ('msg.course_scanning' === $e->getMessage()) { + $response->addMessage($e->getMessage(), 'info', 0, false); + } else { + $response->addMessage($e->getMessage(), 'error', 0); + } + } + + return new JsonResponse($response); + } + #[Route('/api/sync/content/{contentItem}', name: 'content_sync', methods: ['GET'])] - public function requestContentSync(ContentItem $contentItem, LmsFetchService $lmsFetch, PhpAllyService $phpAlly, EqualAccessService $equalAccess) + public function requestContentSync(ContentItem $contentItem, LmsFetchService $lmsFetch, ScannerService $scanner) { $response = new ApiResponse(); $course = $contentItem->getCourse(); @@ -83,8 +135,7 @@ public function requestContentSync(ContentItem $contentItem, LmsFetchService $lm $lmsFetch->deleteContentItemIssues(array($contentItem)); // Rescan the contentItem - // $report = $phpAlly->scanContentItem($contentItem); - $report = $equalAccess->scanContentItem($contentItem); + $report = $scanner->scanContentItem($contentItem); // Add rescanned Issues to database foreach ($report->getIssues() as $issue) { diff --git a/src/Entity/Course.php b/src/Entity/Course.php index 2e40a3a6f..86d14c22d 100644 --- a/src/Entity/Course.php +++ b/src/Entity/Course.php @@ -259,6 +259,12 @@ public function removeReport(Report $report): self return $this; } + public function removeAllReports(): self + { + $this->reports->clear(); + return $this; + } + public function getPreviousReport(): ?Report { $reports = $this->reports->toArray(); diff --git a/src/Lms/Canvas/CanvasApi.php b/src/Lms/Canvas/CanvasApi.php index 845c6601c..7415b049e 100644 --- a/src/Lms/Canvas/CanvasApi.php +++ b/src/Lms/Canvas/CanvasApi.php @@ -106,7 +106,7 @@ public function apiPost($url, $options, $sendAuthorized = true) } // Posts a file to Canvas - public function apiFilePost(string $url, array $options, string $filepath): LmsResponse + public function apiFilePost(string $url, array $options, string $filepath, string $newFileName): LmsResponse { $fileResponse = $this->apiGet($url); $file = $fileResponse->getContent(); @@ -114,13 +114,16 @@ public function apiFilePost(string $url, array $options, string $filepath): LmsR // TODO: handle failed call $endpointOptions = [ - 'name' => urldecode($file['filename']), + 'name' => $newFileName, 'parent_folder_id' => $file['folder_id'], + 'content_type' => $file['content-type'], ]; $endpointResponse = $this->apiPost($options['postUrl'], ['query' => $endpointOptions], true); $endpointContent = $endpointResponse->getContent(); + $this->apiDelete($url); + // TODO: handle failed call $formFields = $endpointContent['upload_params']; @@ -158,4 +161,32 @@ public function apiPut($url, $options) return $lmsResponse; } + public function apiDelete($url) { + $lmsResponse = new LmsResponse(); + + if (strpos($url, 'https://') === false) { + $pattern = '/\/files\/\d+/'; + + preg_match($pattern, $url, $matches); + + $url = "https://" . $this->baseUrl . "/api/v1/" . $matches[0]; + } + + $response = $this->httpClient->request('DELETE', $url); + + $lmsResponse->setResponse($response); + + $content = $lmsResponse->getContent(); + if (!empty($content['errors'])) { + // TODO: If error is invalid token, refresh API token and try again + + foreach ($content['errors'] as $error) { + $lmsResponse->setError($error['message']); + } + } + + return $lmsResponse; + + } + } diff --git a/src/Lms/Canvas/CanvasLms.php b/src/Lms/Canvas/CanvasLms.php index ec30a2fc9..341d41df6 100644 --- a/src/Lms/Canvas/CanvasLms.php +++ b/src/Lms/Canvas/CanvasLms.php @@ -356,7 +356,7 @@ public function postContentItem(ContentItem $contentItem) return; } - $fileResponse = $canvasApi->apiFilePost($url, $options, $filepath); + $fileResponse = $canvasApi->apiFilePost($url, $options, $filepath, $contentItem->getTitle()); $fileObj = $fileResponse->getContent(); if (isset($fileObj['id'])) { @@ -370,7 +370,7 @@ public function postContentItem(ContentItem $contentItem) return $canvasApi->apiPut($url, ['body' => $options]); } - public function postFileItem(FileItem $file) + public function postFileItem(FileItem $file, string $newFileName) { $user = $this->security->getUser(); $apiDomain = $this->getApiDomain($user); @@ -382,7 +382,7 @@ public function postFileItem(FileItem $file) 'postUrl' => "courses/{$file->getCourse()->getLmsCourseId()}/files" ]; - $fileResponse = $canvasApi->apiFilePost($url, $options, $filepath); + $fileResponse = $canvasApi->apiFilePost($url, $options, $filepath, $newFileName); $fileObj = $fileResponse->getContent(); if (isset($fileObj['id'])) { diff --git a/src/Lms/D2l/D2lLms.php b/src/Lms/D2l/D2lLms.php index 1b403781f..055ff5229 100644 --- a/src/Lms/D2l/D2lLms.php +++ b/src/Lms/D2l/D2lLms.php @@ -381,7 +381,7 @@ public function updateFileItem(Course $course, $file) $this->entityManager->flush(); } - public function postFileItem(FileItem $file) + public function postFileItem(FileItem $file, string $newFileName) { return true; } diff --git a/src/Lms/LmsInterface.php b/src/Lms/LmsInterface.php index c155114db..acc6823ed 100644 --- a/src/Lms/LmsInterface.php +++ b/src/Lms/LmsInterface.php @@ -17,7 +17,7 @@ public function updateCourseData(Course $course, User $user); public function updateFileItem(Course $course, $file); public function updateContentItem(ContentItem $contentItem); public function postContentItem(ContentItem $contentItem); - public function postFileItem(FileItem $file); + public function postFileItem(FileItem $file, string $newFileName); public function getOauthUri(Institution $institution, UserSession $session); public function getAccountData(User $user, $accountId); public function getCourseUrl(Course $course, User $user); diff --git a/src/Services/AsyncEqualAccessReport.php b/src/Services/AsyncEqualAccessReport.php index 108fc4191..73ed97c20 100644 --- a/src/Services/AsyncEqualAccessReport.php +++ b/src/Services/AsyncEqualAccessReport.php @@ -3,22 +3,44 @@ namespace App\Services; use App\Entity\ContentItem; -use App\Services\EqualAccessService; use DOMDocument; -use DOMXPath; +use Aws\Credentials\Credentials; +use Aws\Signature\SignatureV4; + +use GuzzleHttp\Psr7; use GuzzleHttp\Promise; use GuzzleHttp\Client; -use Symfony\Component\VarExporter\VarExporter; +use Psr\Http\Message\RequestInterface; -// Asynchronously ... +// Take in a bundle of ContentItems and +// send asynchronous requests to a Lambda function's API gateway class AsyncEqualAccessReport { - private $client; - public function __construct() { + private $awsAccessKeyId; + private $awsSecretAccessKey; + private $awsRegion; + private $host; + private $endpoint; + private $canonicalUri; + + public function __construct() + { + $this->loadConfig(); + } + + private function loadConfig() + { + // Load variables for AWS + $this->awsAccessKeyId = $_ENV['AWS_ACCESS_KEY_ID']; + $this->awsSecretAccessKey = $_ENV['AWS_SECRET_ACCESS_KEY']; + $this->awsRegion = $_ENV['AWS_REGION']; + $this->host = $_ENV['AWS_HOST']; + $this->canonicalUri = $_ENV['AWS_CANONICAL_URI']; + $this->endpoint = "https://{$this->host}/{$this->canonicalUri}"; } public function logToServer(string $message) { @@ -34,49 +56,104 @@ public function logToServer(string $message) { file_get_contents("http://host.docker.internal:3000/log", false, $context); } - public function postMultipleAsync(array $contentItems) { - $promises = []; + function sign(RequestInterface $request): RequestInterface { + $signature = new SignatureV4('execute-api', $this->awsRegion); + $credentials = new Credentials($this->awsAccessKeyId, $this->awsSecretAccessKey); - $client = new Client([ - "base_uri" => "http://host.docker.internal:3000/", - ]); + return $signature->signRequest($request, $credentials); + } + + public function postMultipleAsync(array $contentItems): array { + $promises = []; + $client = new Client(); + $contentItemsReport = []; // Iterate through each scannable Canvas page and add a new // POST request to our array of promises foreach ($contentItems as $contentItem) { - $promises[] = $client->postAsync("check", [ - "headers" => [ - "Content-type" => "text/html", + $this->logToServer("Checking: {$contentItem->getTitle()}"); + // Clean up the content item's HTML document + $html = $contentItem->getBody(); + $document = $this->getDomDocument($html)->saveHTML(); + $payload = json_encode(["html" => $document]); + + $request = new Psr7\Request( + "POST", + "{$this->endpoint}", + [ + "Content-Type" => "application/json", ], - "body" => $contentItem->getBody(), - ]); + $payload, + ); + + $signedRequest = $this->sign($request); + $this->logToServer("Sending to promise array..."); + $promises[] = $client->sendAsync($signedRequest); } // Wait for all the POSTs to resolve and save them into an array // Each promise is resolved into an array with a "state" key (fulfilled/rejected) and "value" (the JSON) $results = Promise\Utils::unwrap($promises); - // $this->logToServer(json_encode($results, JSON_PRETTY_PRINT)); + // Save the report for the content item into an array. + // They should (in theory) be in the same order they were sent in. foreach ($results as $result) { - $this->logToServer("____________________"); $response = $result->getBody()->getContents(); $json = json_decode($response, true); // $this->logToServer(json_encode($json, JSON_PRETTY_PRINT)); - foreach ($json["results"] as $pageScan) { - $equalAccessRule = $pageScan["ruleId"]; - $this->logToServer($equalAccessRule); - } - // foreach ($json as $pageScan) { - // foreach ($pageScan["results"] as $pageScanResults) { - // $equalAccessRule = $pageScanResults["ruleId"]; - // $this->logToServer("{$equalAccessRule}"); - // } - // } - // $this->logToServer(); - $this->logToServer("____________________"); + + $this->logToServer("Saving to contentItemsReport..."); + $contentItemsReport[] = $json; + } + + return $contentItemsReport; + } + + public function postSingleAsync(ContentItem $contentItem) { + // Scan a single content item + $client = new Client(); + + // Clean up the content item's HTML document + $html = $contentItem->getBody(); + $document = $this->getDomDocument($html)->saveHTML(); + $payload = json_encode(["html" => $document]); + + $request = new Psr7\Request( + "POST", + "{$this->endpoint}", + [ + "Content-Type" => "application/json", + ], + $payload, + ); + + $signedRequest = $this->sign($request); + + // POST document to Lambda and wait for fulfillment + $this->logToServer("Sending to single promise..."); + $promise = $client->sendAsync($signedRequest); + $response = $promise->wait(); + + if ($response) { + $this->logToServer("Fulfilled!"); + $contents = $response->getBody()->getContents(); + $report = json_decode($contents, true); } + + // Return the Equal Access report + return $report; } + public function getDomDocument($html) { + $dom = new DOMDocument('1.0', 'utf-8'); + if (strpos($html, 'loadHTML("{$html}", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + } else { + $dom->loadHTML("{$html}", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + } + + return $dom; + } } diff --git a/src/Services/AwsApiAccessibilityService.php b/src/Services/AwsApiAccessibilityService.php new file mode 100644 index 000000000..7c0f499ca --- /dev/null +++ b/src/Services/AwsApiAccessibilityService.php @@ -0,0 +1,154 @@ +loadConfig(); + } + + private function loadConfig() + { + // Load variables for AWS + $this->awsAccessKeyId = $_ENV['AWS_ACCESS_KEY_ID']; + $this->awsSecretAccessKey = $_ENV['AWS_SECRET_ACCESS_KEY']; + $this->awsRegion = $_ENV['AWS_REGION']; + $this->service = $_ENV['AWS_SERVICE']; + $this->host = $_ENV['AWS_HOST']; + $this->canonicalUri = $_ENV['AWS_CANONICAL_URI']; + $this->endpoint = "https://{$this->host}/{$this->canonicalUri}"; + } + + public function logToServer(string $message) { + $options = [ + 'http' => [ + 'header' => "Content-type: text/html\r\n", + 'method' => 'POST', + 'content' => $message, + ], + ]; + + $context = stream_context_create($options); + file_get_contents("http://host.docker.internal:3000/log", false, $context); + } + + public function scanContentItem(ContentItem $contentItem) { + $html = HtmlService::clean($contentItem->getBody()); + + if (!$html) { + return; + } + + $data = $this->scanHtml($html); + + return $data; + } + + public function scanHtml($html) + { + $document = $this->getDomDocument($html); + $requestPayload = json_encode(["html" => [$document->saveHTML()]]); + + $amzDate = gmdate('Ymd\THis\Z'); + $dateStamp = gmdate('Ymd'); + $canonicalQuerystring = ""; + $canonicalHeaders = "content-type:application/json\nhost:{$this->host}\nx-amz-date:{$amzDate}\n"; + $signedHeaders = "content-type;host;x-amz-date"; + $payloadHash = hash('sha256', $requestPayload); + + $canonicalRequest = "POST\n/{$this->canonicalUri}\n{$canonicalQuerystring}\n{$canonicalHeaders}\n{$signedHeaders}\n{$payloadHash}"; + $algorithm = "AWS4-HMAC-SHA256"; + $credentialScope = "{$dateStamp}/{$this->awsRegion}/{$this->service}/aws4_request"; + $stringToSign = "{$algorithm}\n{$amzDate}\n{$credentialScope}\n" . hash('sha256', $canonicalRequest); + + $signingKey = $this->getSignatureKey($this->awsSecretAccessKey, $dateStamp, $this->awsRegion, $this->service); + $signature = hash_hmac('sha256', $stringToSign, $signingKey); + + $authorizationHeader = + "{$algorithm} Credential={$this->awsAccessKeyId}/{$credentialScope}, " . + "SignedHeaders={$signedHeaders}, Signature={$signature}"; + + $headers = [ + 'Content-Type: application/json', + "x-amz-date: {$amzDate}", + "Authorization: {$authorizationHeader}" + ]; + + $json = $this->makeRequest($requestPayload, $headers); + return $json; + } + + private function getSignatureKey($key, $dateStamp, $regionName, $serviceName) + { + $kDate = hash_hmac('sha256', $dateStamp, "AWS4" . $key, true); + $kRegion = hash_hmac('sha256', $regionName, $kDate, true); + $kService = hash_hmac('sha256', $serviceName, $kRegion, true); + return hash_hmac('sha256', "aws4_request", $kService, true); + } + + private function makeRequest($requestPayload, $headers) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $this->endpoint); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $requestPayload); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + try { + $response = curl_exec($ch); + + if ($response === false) { + throw new Exception(curl_error($ch), curl_errno($ch)); + } + + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + if ($httpCode >= 400) { + throw new Exception("HTTP Error: " . $httpCode); + } + + $jsonResponse = json_decode($response, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new Exception("Invalid JSON response"); + } + + return $jsonResponse; + } catch (Exception $e) { + error_log("An error occurred with the lambda function: " . $e->getMessage()); + return null; + } finally { + curl_close($ch); + } + } + + public function getDomDocument($html) + { + $dom = new DOMDocument('1.0', 'utf-8'); + if (strpos($html, 'loadHTML("{$html}", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + } else { + $dom->loadHTML("{$html}", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + } + + return $dom; + + } +} diff --git a/src/Services/EqualAccessService.php b/src/Services/EqualAccessService.php index 211aef603..a3df6461c 100644 --- a/src/Services/EqualAccessService.php +++ b/src/Services/EqualAccessService.php @@ -5,54 +5,37 @@ use App\Entity\ContentItem; use CidiLabs\PhpAlly\PhpAllyIssue; use CidiLabs\PhpAlly\PhpAllyReport; +use App\Services\AwsApiAccessibilityService; use DOMDocument; +use DOMElement; use DOMXPath; -// bridge between udoit and equalaccess +/* + Given a JSON report generated by accessibility-checker, + parse the JSON for all failed rules (according to Equal Access) + and put them in a phpAlly report -class EqualAccessService { + TODO: + - check for phpally-ignore on html snippets and ignore them + - think about how to migrate old database data to equal access + - find way to skip rules in aws perhaps(?) +*/ - /** @var App\Service\HtmlService */ - protected $htmlService; - - // Rule mappings from Equal Access names to generic UDOIT names - // TODO: Verify rules, some Equal Access rules may also be - private $ruleMappings = array( - // Image Alt - "img_alt_misuse" => "ImageAltIsDifferent", - "img_alt_valid" => "ImageHasAlt", - // Links - "a_text_purpose" => "AnchorMustContainText", - // Media - "caption_track_exists" => "VideoProvidesCaptions", // need to have kind="captions" in - // Tables - "table_headers_exist" => "TableDataShouldHaveTableHeader", - // Deprecated Elements (seems like all of these are overwritten by Canvas?) - "blink_elem_deprecated" => "BlinkIsNotUsed", // also maybe blink_css_review? - "marquee_elem_avoid" => "MarqueeIsNotUsed", - // Objects - "object_text_exists" => "ObjectMustContainText", - // Headings - "heading_content_exists" => "HeadersHaveText", - "text_block_heading" => "ParagraphNotUsedAsHeader", - // Color Contrast - "text_contrast_sufficient" => "CssTextHasContrast", +class EqualAccessService { + // probably should disable rules in equal access itself, this is temporary hopefully + private $skipRules = array( + "html_lang_exists", + "html_skipnav_exists", + "page_title_exists", + "skip_main_exists", + "style_highcontrast_visible", + "style_viewport_resizable", + "aria_accessiblename_exists", + "aria_content_in_landmark", ); - public function scanContentItem(ContentItem $contentItem) { - $html = HtmlService::clean($contentItem->getBody()); - - if (!$html) { - return; - } - - $data = $this->checkMany($html, [], []); - - return $data; - } - public function logToServer(string $message) { $options = [ 'http' => [ @@ -66,36 +49,7 @@ public function logToServer(string $message) { file_get_contents("http://host.docker.internal:3000/log", false, $context); } - public function postData(string $url, string $html) { - $options = [ - 'http' => [ - 'header' => "Content-type: text/html\r\n", - 'method' => 'POST', - 'content' => $html, - ], - ]; - - $context = stream_context_create($options); - $result = file_get_contents($url, false, $context); - - return $result; - } - - public function checkMany($content, $ruleIds = [], $options = []) { - $document = $this->getDomDocument($content); - $response = $this->postData("http://host.docker.internal:3000/check", $document->saveHTML()); - $json = json_decode($response, true); - $report = $this->generateReport($json, $document); - return $report; - } - - public function scanHtml($html, $rules = [], $options = []) { - $html = HtmlService::clean($html); - - return $this->checkMany($html, [], []); - } - - public function xpathToSnippet($domXPath, $xpathQuery): \DOMElement { + public function xpathToSnippet($domXPath, $xpathQuery) { // Query the document and save the results into an array // In a perfect world this array should only have one element $xpathResults = $domXPath->query($xpathQuery); @@ -109,9 +63,25 @@ public function xpathToSnippet($domXPath, $xpathQuery): \DOMElement { } } + // If no results are found, return null (meaning nothing was found) return $htmlSnippet; } + public function checkforIgnoreClass($htmlSnippet) { + // Assume no phpAllyIgnore by default + $phpAllyIgnore = false; + + if ($htmlSnippet) { + $classes = $htmlSnippet->getAttribute("class"); + + if (strlen($classes) > 0 && str_contains($classes, "phpally-ignore")) { + $phpAllyIgnore = true; + } + } + + return $phpAllyIgnore; + } + // Generate a UDOIT-style JSON report from the output of Equal Access public function generateReport($json, $document) { $report = new PhpAllyReport(); @@ -121,16 +91,14 @@ public function generateReport($json, $document) { $issueCounts = array(); foreach ($json["results"] as $results) { - $equalAccessRule = $results["ruleId"]; + $udoitRule = $results["ruleId"]; $xpathQuery = $results["path"]["dom"]; + $issueHtml = $this->xpathToSnippet($xpath, $xpathQuery); - // Map the Equal Access rule name to a UDOIT-style rule name - $udoitRule = $this->ruleMappings[$equalAccessRule] ?? "UnknownRule"; - - $ruleMapString = $equalAccessRule . " " . $udoitRule; - $this->logToServer($ruleMapString); - - if ($udoitRule != "UnknownRule") { + // First check if the HTML has phpally-ignore and also check if the rule isn't one we skip. + if (!$this->checkforIgnoreClass($issueHtml) && !in_array($udoitRule, $this->skipRules)) { + // Populate the issue counts field with how many total issues + // with the specific rule are found if(array_key_exists($udoitRule, $issueCounts)) { $issueCounts[$udoitRule]++; } @@ -138,43 +106,26 @@ public function generateReport($json, $document) { $issueCounts[$udoitRule] = 1; } - // UDOIT database has 'html' and 'preview_html', - // where 'preview_html' is the parent of the offending html - $issueHtml = $this->xpathToSnippet($xpath, $xpathQuery); - $parentIssueHtml = $issueHtml->parentNode; - - // Catch if the parent was already the root element - // TODO: If there's an or tag already in the page (somehow), then this could break? - // if ($parentIssueHtml->tagName === "body" || $parentIssueHtml->tagName === "html") { - // $nodeList = $xpath->query("/html[1]/body[1]/*"); - // $dom = new DOMDocument('1.0', 'utf-8'); - // foreach ($nodeList as $node) { - // $dom->appendChild($dom->importNode($node, true)); - // } - - // $this->logToServer("parent is root, printing entire document:"); - // $this->logToServer($dom->saveHtml()); - - // $parentIssueHtml = $dom; - // } - - - $issue = new PhpAllyIssue($udoitRule, $issueHtml, $parentIssueHtml, null); - - $report->setIssueCounts($udoitRule, $issueCounts[$udoitRule], -1); - - array_push($issues, $issue); - - $report->setErrors([]); + // Check for null (aka no XPath result was found) and skip. + // Otherwise, create a new issue with the HTML from the XPath query. + if (!is_null($issueHtml)) { + // UDOIT database has 'html' and 'preview_html', + // where 'preview_html' is the parent of the offending html + $parentIssueHtml = $issueHtml->parentNode; + + $issue = new PhpAllyIssue($udoitRule, $issueHtml, $parentIssueHtml, null); + $report->setIssueCounts($udoitRule, $issueCounts[$udoitRule], -1); + array_push($issues, $issue); + $report->setErrors([]); + } } - - } $report->setIssues($issues); - $this->logToServer("REPORT:"); - $this->logToServer(json_encode($report, JSON_PRETTY_PRINT)); + // Debug + $this->logToServer("Generated report! Sending back to ScannerService..."); + // $this->logToServer(json_encode($report, JSON_PRETTY_PRINT)); return $report; @@ -183,6 +134,7 @@ public function generateReport($json, $document) { public function getDomDocument($html) { $dom = new DOMDocument('1.0', 'utf-8'); + libxml_use_internal_errors(true); if (strpos($html, 'loadHTML("{$html}", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); } else { diff --git a/src/Services/LmsFetchService.php b/src/Services/LmsFetchService.php index a7686eebc..e9233338b 100644 --- a/src/Services/LmsFetchService.php +++ b/src/Services/LmsFetchService.php @@ -24,6 +24,9 @@ class LmsFetchService { /** @var PhpAllyService $phpAllyService */ private $phpAlly; + /** @var ScannerService $scannerService */ + private $scanner; + /** @var EqualAccessService $equalAccessService */ private $equalAccess; @@ -45,6 +48,7 @@ public function __construct( PhpAllyService $phpAlly, EqualAccessService $equalAccess, AsyncEqualAccessReport $asyncReport, + ScannerService $scanner, ManagerRegistry $doctrine, UtilityService $util ) @@ -54,6 +58,7 @@ public function __construct( $this->phpAlly = $phpAlly; $this->equalAccess = $equalAccess; $this->asyncReport = $asyncReport; + $this->scanner = $scanner; $this->doctrine = $doctrine; $this->util = $util; } @@ -192,19 +197,24 @@ public function updateReport(Course $course, User $user): Report // Performs PHPAlly scan on each Content Item. private function scanContentItems(array $contentItems) { - // Testing async post requests... - // $this->asyncReport->postMultipleAsync($contentItems); + $scanner = $_ENV['ACCESSIBILITY_CHECKER']; + $equalAccessReports = null; + + // If we're using Equal Access Lambda, send all the requests to Lambda for the + // reports at once and save them all into an array (which should be in the same order as the ContentItems) + if ($scanner == "equalaccess_lambda" && count($contentItems) > 0) { + $equalAccessReports = $this->asyncReport->postMultipleAsync($contentItems); + } // Scan each update content item for issues /** @var \App\Entity\ContentItem $contentItem */ + + $index = 0; foreach ($contentItems as $contentItem) { + try { - // Scan Content Item with PHPAlly - // $phpAllyReport = $this->phpAlly->scanContentItem($contentItem); - $this->equalAccess->logToServer("EQUAL ACCESS REPORT:"); - $report = $this->equalAccess->scanContentItem($contentItem); - // $this->equalAccess->logToServer("PHPALLY REPORT:"); - // $this->equalAccess->logToServer(json_encode($phpAllyReport, JSON_PRETTY_PRINT)); + // Scan the content item with the scanner set in the environment. + $report = $this->scanner->scanContentItem($contentItem, $equalAccessReports == null ? null : $equalAccessReports[$index++], $this->util); if ($report) { // TODO: Do something with report errors @@ -221,28 +231,14 @@ private function scanContentItems(array $contentItems) $this->createIssue($issue, $contentItem); } } - - // if ($phpAllyReport) { - // // TODO: Do something with report errors - // if (count($phpAllyReport->getErrors())) { - // foreach ($phpAllyReport->getErrors() as $error) { - // $msg = $error . ', item = #' . $contentItem->getId(); - // $this->util->createMessage($msg, 'error', $contentItem->getCourse(), null, true); - // } - // } - - // // Add Issues to report - // foreach ($phpAllyReport->getIssues() as $issue) { - // // Create issue entity - // $this->createIssue($issue, $contentItem); - // } - // } } catch (\Exception $e) { $this->util->createMessage($e->getMessage(), 'error', null, null, true); } } $this->doctrine->getManager()->flush(); + + $this->scanner->logToServer("done!!!!!!!!!\n"); } public function createIssue(PhpAllyIssue $issue, ContentItem $contentItem) diff --git a/src/Services/LmsPostService.php b/src/Services/LmsPostService.php index 4313d7db7..81fd0207b 100644 --- a/src/Services/LmsPostService.php +++ b/src/Services/LmsPostService.php @@ -76,7 +76,7 @@ public function saveFileToLms(FileItem $file, UploadedFile $uploadedFile, User $ return; } - return $lms->postFileItem($file); + return $lms->postFileItem($file, $uploadedFile->getClientOriginalName()); } public function replaceContent(Issue $issue, ContentItem $contentItem) diff --git a/src/Services/LocalApiAccessibilityService.php b/src/Services/LocalApiAccessibilityService.php new file mode 100644 index 000000000..3f2594e82 --- /dev/null +++ b/src/Services/LocalApiAccessibilityService.php @@ -0,0 +1,74 @@ +getBody()); + + if (!$html) { + return; + } + + $data = $this->checkMany($html, [], []); + + return $data; + } + + public function postData(string $url, string $html) { + $options = [ + 'http' => [ + 'header' => "Content-type: text/html\r\n", + 'method' => 'POST', + 'content' => $html, + ], + ]; + + $context = stream_context_create($options); + $result = file_get_contents($url, false, $context); + + return $result; + } + + public function checkMany($content, $ruleIds = [], $options = []) { + $document = $this->getDomDocument($content); + $response = $this->postData("http://host.docker.internal:3000/check", $document->saveHTML()); + $json = json_decode($response, true); + return $json; + } + + public function scanHtml($html, $rules = [], $options = []) { + $html = HtmlService::clean($html); + + return $this->checkMany($html, [], []); + } + + public function getDomDocument($html) + { + $dom = new DOMDocument('1.0', 'utf-8'); + libxml_use_internal_errors(true); + if (strpos($html, 'loadHTML("{$html}", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + } else { + $dom->loadHTML("{$html}", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + } + + return $dom; + + } + +} diff --git a/src/Services/ScannerService.php b/src/Services/ScannerService.php new file mode 100644 index 000000000..64175b22c --- /dev/null +++ b/src/Services/ScannerService.php @@ -0,0 +1,92 @@ + [ + 'header' => "Content-type: text/html\r\n", + 'method' => 'POST', + 'content' => $message, + ], + ]; + + $context = stream_context_create($options); + file_get_contents("http://host.docker.internal:3000/log", false, $context); + } + + public function scanContentItem(ContentItem $contentItem, $scannerReport = null, UtilityService $util) { + // Optional argument scannerReport is used when handling async Equal Access + // requests, so then all we have to do is just make those into a UDOIT report + $scanner = $_ENV['ACCESSIBILITY_CHECKER']; + $report = null; + + if ($scanner == 'phpally') { + // TODO: implement flow for phpally scanning + $htmlService = new HtmlService(); + $phpAlly = new PhpAllyService($htmlService, $util); + $report = $phpAlly->scanContentItem($contentItem); + } + else if ($scanner == 'equalaccess_local') { + // TODO: create a LocalAccessibilityService + } + else if ($scanner == 'equalaccess_lambda') { + if ($contentItem->getBody() != null) { + $equalAccess = new EqualAccessService(); + $document = $equalAccess->getDomDocument($contentItem->getBody()); + if (!$scannerReport) { + // Report is null, we need to call the lambda function for a single page most likely + $this->logToServer("No report passed in!"); + $asyncReport = new AsyncEqualAccessReport(); + $json = $asyncReport->postSingleAsync($contentItem); + $report = $equalAccess->generateReport($json, $document); + } + else { + // We already have the report, all we have to do is generate the UDOIT report + $report = $equalAccess->generateReport($scannerReport, $document); + } + } + } + else { + // Unknown scanner set in environment, should return error... + throw new \Exception("Unknown scanner type!"); + } + + return $report; + } + + public function getDomDocument($html) + { + $dom = new DOMDocument('1.0', 'utf-8'); + libxml_use_internal_errors(true); + if (strpos($html, 'loadHTML("{$html}", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + } else { + $dom->loadHTML("{$html}", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + } + + return $dom; + + } +} diff --git a/translations/en.json b/translations/en.json index 6f20e08e2..2409302a4 100644 --- a/translations/en.json +++ b/translations/en.json @@ -124,6 +124,7 @@ "menu.admin": "Admin", "menu.download_pdf": "Download PDF", "menu.scan_course": "Rescan Course", + "menu.full_rescan": "Force Full Rescan", "msg.no_permissions": "You do not have permission to access the specified course.", "msg.course_scanning": "Course scanning...", @@ -131,6 +132,8 @@ "msg.new_content": "Course scan complete. New content has been added.", "msg.sync.started": "Course scan started.", "msg.sync.failed": "Course scan failed. Course is missing.", + "msg.file.replace.file_type": "File type not accepted. Please input a file with the correct filetype.", + "msg.file.replace.file_size": "File size too large. Please input a file of a size less than 10MB", "msg.sync.completed": "Course scan completed.", "msg.sync.course_inactive": "Course scan failed. Course is inactive.", @@ -327,6 +330,7 @@ "rule.label.VideoProvidesCaptions": "Video Tags Must have Caption Track", "rule.label.VideosEmbeddedOrLinkedNeedCaptions": "No Closed Captions Found", "rule.label.VideosHaveAutoGeneratedCaptions": "Closed Captions Were Auto-Generated", + "rule.label.text_contrast_sufficient": "Ensure text has sufficient contrast", "rule.desc.AnchorLinksToMultiMediaRequireTranscript": "

Multimedia objects should be accompanied by a link to a transcript of the content.

", "rule.desc.AnchorLinksToSoundFilesNeedTranscripts": "

Links to a sound file should be followed by a link to a transcript of the file.

", @@ -367,6 +371,8 @@ "rule.desc.VideoProvidesCaptions": "

All video elements must have a caption using the track element with caption attribute. The caption should convey all meaningful information in the video element; this includes, but is not limited to, dialogue, musical cues, and sound effects. Good captions not only include dialogue, but also identify who is speaking and include non-speech information conveyed through sound, including meaningful sound effects.

", "rule.desc.VideosEmbeddedOrLinkedNeedCaptions": "

Captions should be included in the video to provide dialogue to users who are hearing impaired. (Please note that videos that have been removed, deleted, or are Unlisted will also cause this error, and will need to be manually verified.)

", "rule.desc.VideosHaveAutoGeneratedCaptions": "

Captions that are machine-generated by a service like YouTube are rarely if ever fully accurate and should not be relied upon for educational use.

", + + "rule.desc.text_contrast_sufficient": "

Text color should be easily viewable and should not be the only indicator of meaning or function. Color balance should have at least a 4.5:1 ratio for small text and 3:1 ratio for large text. Warning: using UDOIT to fix one section of text may invalidate the contrast in nested sections of text that are not the same color.

", "rule.example.AnchorLinksToSoundFilesNeedTranscripts": "
Wrong
<a href='interview.mp3'>Listen to the interview</a>
Right
<a href='interview.mp3'>Listen to the interview</a> <a href='transcript.html'>(transcript)</a>", "rule.example.BlinkIsNotUsed": "
Wrong

<blink>Please read me!</blink>

Right

<strong>Please read me!</strong>

", diff --git a/translations/es.json b/translations/es.json index 6e8b3ed84..b14aa810f 100644 --- a/translations/es.json +++ b/translations/es.json @@ -124,6 +124,7 @@ "menu.admin": "Admin", "menu.download_pdf": "Descargar PDF", "menu.scan_course": "Volver a escanear Curso", + "menu.full_rescan": "Reiniciar escaneo completo", "msg.no_permissions": "No tienes permiso para acceder al curso especificado.", "msg.course_scanning": "Escaneando el curso...", @@ -131,6 +132,8 @@ "msg.new_content": "Escaneo del curso completo. Se ha agregado nuevo contenido.", "msg.sync.started": "Se inició el escaneo del curso.", "msg.sync.failed": "Falló el escaneo del curso. Falta el curso.", + "msg.file.replace.file_type": "El tipo de archivo no es válido. Por favor aporte un archivo de tipo correcto.", + "msg.file.replace.file_size": "El tamaño del archivo es demasiado grande. Por favor aporte un archivo de tamaño menor a 10MB", "msg.sync.completed": "Escaneo del curso completado.", "msg.sync.course_inactive": "Falló el escaneo del curso. El curso está inactivo.", diff --git a/yarn.lock b/yarn.lock index fb8e8ae66..b706f4df0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,6 +16,14 @@ dependencies: "@babel/highlight" "^7.14.5" +"@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + "@babel/compat-data@^7.13.11", "@babel/compat-data@^7.14.4": version "7.14.4" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.14.4.tgz#45720fe0cecf3fd42019e1d12cc3d27fadc98d58" @@ -68,7 +76,7 @@ semver "^6.3.0" source-map "^0.5.0" -"@babel/generator@^7.14.2", "@babel/generator@^7.14.3": +"@babel/generator@^7.14.3": version "7.14.3" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.3.tgz#0c2652d91f7bddab7cccc6ba8157e4f40dcedb91" integrity sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA== @@ -86,6 +94,16 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== + dependencies: + "@babel/types" "^7.23.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.10.4", "@babel/helper-annotate-as-pure@^7.12.13", "@babel/helper-annotate-as-pure@^7.8.3": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz#0f58e86dfc4bb3b1fcd7db806570e177d439b6ab" @@ -155,6 +173,11 @@ resolve "^1.14.2" semver "^6.1.2" +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + "@babel/helper-explode-assignable-expression@^7.12.13": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.13.0.tgz#17b5c59ff473d9f956f40ef570cf3a76ca12657f" @@ -171,14 +194,13 @@ "@babel/template" "^7.12.13" "@babel/types" "^7.14.2" -"@babel/helper-function-name@^7.15.4": - version "7.15.4" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz#845744dafc4381a4a5fb6afa6c3d36f98a787ebc" - integrity sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw== +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== dependencies: - "@babel/helper-get-function-arity" "^7.15.4" - "@babel/template" "^7.15.4" - "@babel/types" "^7.15.4" + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" "@babel/helper-get-function-arity@^7.12.13": version "7.12.13" @@ -187,13 +209,6 @@ dependencies: "@babel/types" "^7.12.13" -"@babel/helper-get-function-arity@^7.15.4": - version "7.15.4" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz#098818934a137fce78b536a3e015864be1e2879b" - integrity sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA== - dependencies: - "@babel/types" "^7.15.4" - "@babel/helper-hoist-variables@^7.13.0": version "7.13.16" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.13.16.tgz#1b1651249e94b51f8f0d33439843e33e39775b30" @@ -202,12 +217,12 @@ "@babel/traverse" "^7.13.15" "@babel/types" "^7.13.16" -"@babel/helper-hoist-variables@^7.15.4": - version "7.15.4" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz#09993a3259c0e918f99d104261dfdfc033f178df" - integrity sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA== +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== dependencies: - "@babel/types" "^7.15.4" + "@babel/types" "^7.22.5" "@babel/helper-member-expression-to-functions@^7.13.12": version "7.13.12" @@ -348,6 +363,18 @@ dependencies: "@babel/types" "^7.15.4" +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + "@babel/helper-validator-identifier@^7.12.11", "@babel/helper-validator-identifier@^7.14.0": version "7.14.0" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz#d26cad8a47c65286b15df1547319a5d0bcf27288" @@ -358,6 +385,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz#6654d171b2024f6d8ee151bf2509699919131d48" integrity sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/helper-validator-option@^7.12.17": version "7.12.17" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz#d1fbf012e1a79b7eebbfdc6d270baaf8d9eb9831" @@ -414,7 +446,16 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.14.2", "@babel/parser@^7.14.3": +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.14.3": version "7.14.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.4.tgz#a5c560d6db6cd8e6ed342368dea8039232cbab18" integrity sha512-ArliyUsWDUqEGfWcmzpGUzNfLxTdTp6WU4IuP6QFSp9gGfWS6boxFCkJSJ/L4+RG8z/FnIU3WxCk6hPL9SSWeA== @@ -424,6 +465,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.4.tgz#02f2931b822512d3aad17d475ae83da74a255a84" integrity sha512-xmzz+7fRpjrvDUj+GV7zfz/R3gSK2cOxGlazaXooxspCr539cbTXJKvBJzSVI2pPhcRGquoOtaIkKCsHQUiO3w== +"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.13.12": version "7.13.12" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.13.12.tgz#a3484d84d0b549f3fc916b99ee4783f26fabad2a" @@ -1115,32 +1161,28 @@ "@babel/parser" "^7.15.4" "@babel/types" "^7.15.4" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.15", "@babel/traverse@^7.14.0", "@babel/traverse@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.2.tgz#9201a8d912723a831c2679c7ebbf2fe1416d765b" - integrity sha512-TsdRgvBFHMyHOOzcP9S6QU0QQtjxlRpEYOy3mcCO5RgmC305ki42aSAmfZEMSSYBla2oZ9BMqYlncBaKmD/7iA== - dependencies: - "@babel/code-frame" "^7.12.13" - "@babel/generator" "^7.14.2" - "@babel/helper-function-name" "^7.14.2" - "@babel/helper-split-export-declaration" "^7.12.13" - "@babel/parser" "^7.14.2" - "@babel/types" "^7.14.2" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/traverse@^7.15.4": - version "7.15.4" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.15.4.tgz#ff8510367a144bfbff552d9e18e28f3e2889c22d" - integrity sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA== - dependencies: - "@babel/code-frame" "^7.14.5" - "@babel/generator" "^7.15.4" - "@babel/helper-function-name" "^7.15.4" - "@babel/helper-hoist-variables" "^7.15.4" - "@babel/helper-split-export-declaration" "^7.15.4" - "@babel/parser" "^7.15.4" - "@babel/types" "^7.15.4" +"@babel/template@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + +"@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.15", "@babel/traverse@^7.14.0", "@babel/traverse@^7.14.2", "@babel/traverse@^7.15.4": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" debug "^4.1.0" globals "^11.1.0" @@ -1160,6 +1202,15 @@ "@babel/helper-validator-identifier" "^7.14.9" to-fast-properties "^2.0.0" +"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -2997,6 +3048,38 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.19" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" + integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@mswjs/cookies@^0.1.4": version "0.1.6" resolved "https://registry.yarnpkg.com/@mswjs/cookies/-/cookies-0.1.6.tgz#176f77034ab6d7373ae5c94bcbac36fee8869249" @@ -3862,12 +3945,14 @@ axe-core@^3.3.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227" integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q== -axios@^0.21.2: - version "0.21.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017" - integrity sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg== +axios@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102" + integrity sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg== dependencies: - follow-redirects "^1.14.0" + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" babel-jest@^26.6.3: version "26.6.3" @@ -6147,10 +6232,10 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@^1.0.0, follow-redirects@^1.14.0: - version "1.14.8" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" - integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== +follow-redirects@^1.0.0, follow-redirects@^1.15.0: + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== for-in@^1.0.2: version "1.0.2" @@ -6166,6 +6251,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -6261,9 +6355,9 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-func-name@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" - integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= + version "2.0.2" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== get-intrinsic@^1.0.2: version "1.1.3" @@ -9447,6 +9541,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -11607,16 +11706,16 @@ write-file-atomic@^3.0.0: typedarray-to-buffer "^3.1.5" ws@^6.2.1: - version "6.2.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" - integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== + version "6.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.3.tgz#ccc96e4add5fd6fedbc491903075c85c5a11d9ee" + integrity sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA== dependencies: async-limiter "~1.0.0" ws@^7.4.5: - version "7.4.6" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" - integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== xml-name-validator@^3.0.0: version "3.0.0"