diff --git a/.distignore b/.distignore index b69d2af9c..9a5cba0e4 100644 --- a/.distignore +++ b/.distignore @@ -10,6 +10,7 @@ .env.example .gitignore .gitattributes +.php-cs-fixer.dist.php composer.json composer.lock package.json diff --git a/.gitignore b/.gitignore index 44fa2850b..83c571da2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .phpunit.result.cache node_modules .DS_Store +.php-cs-fixer.cache # Playwright test-results/ diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 000000000..7989348bd --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,19 @@ +in( __DIR__ ) + ->exclude( + [ + 'tests/', + 'vendor/', + 'node_modules/', + ] + ); + +return ( new PhpCsFixer\Config() )->setRules( + [ + 'native_function_invocation' => [ + 'include' => [ '@all' ], + ], + ] +)->setFinder( $finder ); \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d63e9e1b..1cd1497c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ += 1.6.0 = + +Enhancements: + +* Allow users to collect extra points for previous months' badges. +* Added WP-CLI commands to manage recommendations. + +Under the hood: + +* Ravi's Recommendations are now a custom post type. + = 1.5.0 = Added these recommendations from Ravi: diff --git a/assets/css/admin.css b/assets/css/admin.css index 164d4e5b6..ce430cbbb 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -137,8 +137,6 @@ } } } - - } .prpl-hidden { @@ -281,7 +279,6 @@ button.prpl-info-icon { } } - /*------------------------------------*\ Buttons \*------------------------------------*/ @@ -481,3 +478,33 @@ button.prpl-info-icon { grid-template-columns: repeat(2, 1fr); gap: var(--prpl-padding); } + +/*------------------------------------*\ + Loader. + See https://cssloaders.github.io/ for more. +\*------------------------------------*/ +.prpl-loader { + width: 48px; + height: 48px; + border: 5px solid #fff; + border-bottom-color: transparent; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: rotation 1s linear infinite; + z-index: 20; + position: absolute; + top: calc(50% - 24px); + left: calc(50% - 24px); +} + +@keyframes rotation { + + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} diff --git a/assets/css/dashboard-widgets/score.css b/assets/css/dashboard-widgets/score.css index fbebb3769..fca269f99 100644 --- a/assets/css/dashboard-widgets/score.css +++ b/assets/css/dashboard-widgets/score.css @@ -3,7 +3,7 @@ /** * Admin widget. * - * Dependencies: progress-planner/web-components/prpl-suggested-task, progress-planner/web-components/prpl-badge + * Dependencies: progress-planner/suggested-task, progress-planner/web-components/prpl-badge */ #progress_planner_dashboard_widget_score { diff --git a/assets/css/page-widgets/suggested-tasks.css b/assets/css/page-widgets/suggested-tasks.css index dfd884dac..d0b1e4666 100644 --- a/assets/css/page-widgets/suggested-tasks.css +++ b/assets/css/page-widgets/suggested-tasks.css @@ -3,7 +3,7 @@ /** * Suggested tasks widget. * - * Dependencies: progress-planner/web-components/prpl-suggested-task, progress-planner/web-components/prpl-badge + * Dependencies: progress-planner/suggested-task, progress-planner/web-components/prpl-badge */ .prpl-widget-wrapper.prpl-suggested-tasks { @@ -96,16 +96,19 @@ .prpl-dashboard-widget-suggested-tasks { + &:not(:has(.prpl-suggested-tasks-loading)):not(:has(.prpl-suggested-tasks-list li)) { + + .prpl-no-suggested-tasks { + display: block; + } + } + &:has(.prpl-suggested-tasks-list li) { .prpl-widget-title { display: flex; } - .prpl-no-suggested-tasks { - display: none; - } - hr { display: block; } @@ -116,11 +119,16 @@ display: none; } - .prpl-no-suggested-tasks { - display: block; + .prpl-no-suggested-tasks, + .prpl-suggested-tasks-loading { + display: none; background-color: var(--prpl-background-green); padding: calc(var(--prpl-padding) / 2); } + + .prpl-suggested-tasks-loading { + display: block; + } } .prpl-suggested-tasks-list { @@ -132,11 +140,8 @@ border-bottom: none; } - prpl-suggested-task:nth-child(odd) { - - .prpl-suggested-task { - background-color: #f9fafb; - } + .prpl-suggested-task:nth-child(odd) { + background-color: #f9fafb; } /* If task has disabled checkbox it's title should be italic. */ @@ -223,7 +228,6 @@ flex-direction: column; justify-content: space-between; - .progress-label { display: inline-block; } @@ -544,7 +548,6 @@ } } - /* Checkmark */ .prpl-custom-control::after { content: ""; diff --git a/assets/css/page-widgets/todo.css b/assets/css/page-widgets/todo.css index 296baff86..6c72876a9 100644 --- a/assets/css/page-widgets/todo.css +++ b/assets/css/page-widgets/todo.css @@ -1,7 +1,7 @@ /** * TODOs widget. * - * Dependencies: progress-planner/web-components/prpl-suggested-task + * Dependencies: progress-planner/suggested-task */ .prpl-widget-wrapper.prpl-todo { @@ -145,7 +145,6 @@ } } - #create-todo-item { padding: 0 16px; } @@ -192,3 +191,22 @@ display: none; } } + +#todo-list { + + &:has(.prpl-loader) { + position: relative; + + &::before { + content: ""; + display: block; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.2); + position: absolute; + top: 0; + left: 0; + z-index: 10; + } + } +} diff --git a/assets/css/page-widgets/whats-new.css b/assets/css/page-widgets/whats-new.css index 56a9ea72d..46d3f4e1e 100644 --- a/assets/css/page-widgets/whats-new.css +++ b/assets/css/page-widgets/whats-new.css @@ -25,7 +25,6 @@ } } - img { width: 100%; } diff --git a/assets/css/settings-page.css b/assets/css/settings-page.css index 8f4c68a13..638834814 100644 --- a/assets/css/settings-page.css +++ b/assets/css/settings-page.css @@ -48,8 +48,6 @@ padding: var(--prpl-settings-page-gap) var(--prpl-settings-page-gap) 2rem var(--prpl-settings-page-gap); } } - - } .prpl-settings-section-title { @@ -108,7 +106,6 @@ } } - .item-actions, .prpl-select-page { display: flex; @@ -251,7 +248,6 @@ } - /* License */ .prpl-column-license { @@ -298,7 +294,6 @@ } - /* Grid layout for wrapper for: - Valuable post types - Default login destination diff --git a/assets/css/web-components/prpl-suggested-task.css b/assets/css/suggested-task.css similarity index 99% rename from assets/css/web-components/prpl-suggested-task.css rename to assets/css/suggested-task.css index 92d8986de..d9263d3f9 100644 --- a/assets/css/web-components/prpl-suggested-task.css +++ b/assets/css/suggested-task.css @@ -211,8 +211,6 @@ } } - - } &[data-task-action="celebrate"] { diff --git a/assets/css/upgrade-tasks.css b/assets/css/upgrade-tasks.css index c7a3f158b..4e497ed42 100644 --- a/assets/css/upgrade-tasks.css +++ b/assets/css/upgrade-tasks.css @@ -40,7 +40,6 @@ } } - .prpl-onboarding-task-status { display: block; width: 1.5rem; diff --git a/assets/js/celebrate.js b/assets/js/celebrate.js index 7e37d501e..05479ac62 100644 --- a/assets/js/celebrate.js +++ b/assets/js/celebrate.js @@ -4,7 +4,7 @@ * * A script that triggers confetti on the container element. * - * Dependencies: particles-confetti + * Dependencies: particles-confetti, progress-planner/suggested-task */ /* eslint-disable camelcase */ @@ -13,7 +13,7 @@ document.addEventListener( 'prpl/celebrateTasks', ( event ) => { /** * Trigger the confetti on the container element. */ - const containerElement = event.detail?.element + const containerEl = event.detail?.element ? event.detail.element.closest( '.prpl-suggested-tasks-list' ) : document.querySelector( '.prpl-widget-wrapper.prpl-suggested-tasks .prpl-suggested-tasks-list' @@ -30,14 +30,14 @@ document.addEventListener( 'prpl/celebrateTasks', ( event ) => { const prplRenderAttemptshoot = () => { // Get the tasks list position - const origin = containerElement + const origin = containerEl ? { x: - ( containerElement.getBoundingClientRect().left + - containerElement.offsetWidth / 2 ) / + ( containerEl.getBoundingClientRect().left + + containerEl.offsetWidth / 2 ) / window.innerWidth, y: - ( containerElement.getBoundingClientRect().top + 50 ) / + ( containerEl.getBoundingClientRect().top + 50 ) / window.innerHeight, } : { x: 0.5, y: 0.3 }; // fallback if list not found @@ -83,75 +83,23 @@ document.addEventListener( 'prpl/celebrateTasks', ( event ) => { setTimeout( prplRenderAttemptshoot, 0 ); setTimeout( prplRenderAttemptshoot, 100 ); setTimeout( prplRenderAttemptshoot, 200 ); - - /** - * Strike completed tasks. - */ - document.dispatchEvent( new CustomEvent( 'prpl/strikeCelebratedTasks' ) ); - - // Remove celebrated tasks and add them to the completed tasks. - setTimeout( () => { - document.dispatchEvent( - new CustomEvent( 'prpl/markTasksAsCompleted' ) - ); - }, 2000 ); -} ); - -/** - * Mark tasks as completed. - */ -document.addEventListener( 'prpl/markTasksAsCompleted', ( event ) => { - const taskList = event.detail?.taskList || 'prplSuggestedTasks'; - document - .querySelectorAll( '.prpl-suggested-task-celebrated' ) - .forEach( ( item ) => { - const task_id = item.getAttribute( 'data-task-id' ); - const providerID = item.getAttribute( 'data-task-provider-id' ); - const category = item.getAttribute( 'data-task-category' ); - const el = document.querySelector( - `.prpl-suggested-task[data-task-id="${ task_id }"]` - ); - - if ( el ) { - el.parentElement.remove(); - } - - // Get the task index. - let taskIndex = false; - window[ taskList ].tasks.forEach( ( taskItem, index ) => { - if ( taskItem.task_id === task_id ) { - taskIndex = index; - } - } ); - - // Mark the task as completed. - if ( false !== taskIndex ) { - window[ taskList ].tasks[ taskIndex ].status = 'completed'; - } - - // Refresh the list. - document.dispatchEvent( - new CustomEvent( 'prpl/suggestedTask/maybeInjectItem', { - detail: { - task_id, - providerID, - category, - }, - } ) - ); - } ); } ); /** - * Strike completed tasks. + * Remove tasks from the DOM. + * The task will be striked through, before removed, if it has points. */ -document.addEventListener( 'prpl/strikeCelebratedTasks', () => { +document.addEventListener( 'prpl/removeCelebratedTasks', () => { document .querySelectorAll( '.prpl-suggested-task[data-task-action="celebrate"]' ) .forEach( ( item ) => { + // Triggers the strikethrough animation. item.classList.add( 'prpl-suggested-task-celebrated' ); + + // Remove the item from the DOM. + setTimeout( () => item.remove(), 2000 ); } ); } ); @@ -163,9 +111,7 @@ document.addEventListener( 'prpl/celebrateTasks', () => { '#adminmenu #toplevel_page_progress-planner .update-plugins' ); if ( points ) { - points.forEach( ( point ) => { - point.remove(); - } ); + points.forEach( ( point ) => point.remove() ); } } ); diff --git a/assets/js/external-link-accessibility-helper.js b/assets/js/external-link-accessibility-helper.js index 81f3e98f0..91241173c 100644 --- a/assets/js/external-link-accessibility-helper.js +++ b/assets/js/external-link-accessibility-helper.js @@ -118,9 +118,9 @@ prplDocumentReady( () => { } ); // Recheck the accessibility of the page when a new task is injected. -document.addEventListener( 'prpl/suggestedTask/injectItem', () => { +document.addEventListener( 'prpl/suggestedTask/itemInjected', () => { // Wait for the new task to be added to the DOM. setTimeout( () => { externalLinkHelper.applyAccessibility(); - }, 500 ); + }, 10 ); } ); diff --git a/assets/js/suggested-task-terms.js b/assets/js/suggested-task-terms.js new file mode 100644 index 000000000..9aa71e191 --- /dev/null +++ b/assets/js/suggested-task-terms.js @@ -0,0 +1,122 @@ +/* global prplDocumentReady */ +/* + * Populate prplSuggestedTasksTerms with the terms for the taxonomies we use. + * + * Dependencies: wp-api, progress-planner/document-ready + */ + +const prplSuggestedTasksTerms = {}; + +const prplTerms = { + category: 'prpl_recommendations_category', + provider: 'prpl_recommendations_provider', + + /** + * Get the terms for a given taxonomy. + * + * @param {string} taxonomy The taxonomy. + * @return {Object} The terms. + */ + // eslint-disable-next-line no-unused-vars + get: ( taxonomy ) => { + if ( 'category' === taxonomy ) { + taxonomy = prplTerms.category; + } else if ( 'provider' === taxonomy ) { + taxonomy = prplTerms.provider; + } + return prplSuggestedTasksTerms[ taxonomy ] || {}; + }, + + /** + * Get a promise for the terms collection for a given taxonomy. + * + * @param {string} taxonomy The taxonomy. + * @return {Promise} A promise for the terms collection. + */ + getCollectionPromise: ( taxonomy ) => { + return new Promise( ( resolve ) => { + if ( prplSuggestedTasksTerms[ taxonomy ] ) { + console.info( + `Terms already fetched for taxonomy: ${ taxonomy }` + ); + resolve( prplSuggestedTasksTerms[ taxonomy ] ); + } + wp.api.loadPromise.done( () => { + console.info( `Fetching terms for taxonomy: ${ taxonomy }...` ); + + const typeName = taxonomy.replace( 'prpl_', 'Prpl_' ); + prplSuggestedTasksTerms[ taxonomy ] = + prplSuggestedTasksTerms[ taxonomy ] || {}; + const TermsCollection = new wp.api.collections[ typeName ](); + TermsCollection.fetch( { data: { per_page: 100 } } ).done( + ( data ) => { + let userTermFound = false; + // 100 is the maximum number of terms that can be fetched in one request. + data.forEach( ( term ) => { + prplSuggestedTasksTerms[ taxonomy ][ term.slug ] = + term; + if ( 'user' === term.slug ) { + userTermFound = true; + } + } ); + + if ( userTermFound ) { + resolve( prplSuggestedTasksTerms[ taxonomy ] ); + } else { + // If the `user` term doesn't exist, create it. + const newTermModel = new wp.api.models[ typeName ]( + { + slug: 'user', + name: 'user', + } + ); + newTermModel + .save() + .then( ( response ) => { + prplSuggestedTasksTerms[ taxonomy ].user = + response; + return prplSuggestedTasksTerms[ taxonomy ]; + } ) + .then( resolve ); // Resolve the promise after all requests are complete. + } + } + ); + } ); + } ); + }, + + /** + * Get promises for the terms collections for the taxonomies we use. + * + * @return {Promise} A promise for the terms collections. + */ + getCollectionsPromises: () => { + return new Promise( ( resolve ) => { + prplDocumentReady( () => { + Promise.all( [ + prplTerms.getCollectionPromise( prplTerms.category ), + prplTerms.getCollectionPromise( prplTerms.provider ), + ] ).then( () => resolve( prplSuggestedTasksTerms ) ); + } ); + } ); + }, + + /** + * Get a term object from the terms array. + * + * @param {number} termId The term ID. + * @param {string} taxonomy The taxonomy. + * @return {Object} The term object. + */ + getTerm: ( termId, taxonomy ) => { + let termObject = {}; + Object.values( prplSuggestedTasksTerms[ taxonomy ] ).forEach( + ( term ) => { + if ( parseInt( term.id ) === parseInt( termId ) ) { + termObject = term; + } + } + ); + return termObject; + }, +}; diff --git a/assets/js/suggested-task.js b/assets/js/suggested-task.js new file mode 100644 index 000000000..c7d25cdb5 --- /dev/null +++ b/assets/js/suggested-task.js @@ -0,0 +1,646 @@ +/* global HTMLElement, prplSuggestedTask, prplL10n, prplUpdateRaviGauge, prplTerms, prplSuggestedTasksWidget */ +/* + * Suggested Task scripts & helpers. + * + * Dependencies: wp-api, progress-planner/l10n, progress-planner/suggested-task-terms, progress-planner/web-components/prpl-gauge, progress-planner/widgets/suggested-tasks + */ +/* eslint-disable camelcase, jsdoc/require-param-type, jsdoc/require-param, jsdoc/check-param-names */ + +prplSuggestedTask = { + ...prplSuggestedTask, + injectedItemIds: [], + l10n: { + info: prplL10n( 'info' ), + moveUp: prplL10n( 'moveUp' ), + moveDown: prplL10n( 'moveDown' ), + snooze: prplL10n( 'snooze' ), + snoozeThisTask: prplL10n( 'snoozeThisTask' ), + howLong: prplL10n( 'howLong' ), + snoozeDurationOneWeek: prplL10n( 'snoozeDurationOneWeek' ), + snoozeDurationOneMonth: prplL10n( 'snoozeDurationOneMonth' ), + snoozeDurationThreeMonths: prplL10n( 'snoozeDurationThreeMonths' ), + snoozeDurationSixMonths: prplL10n( 'snoozeDurationSixMonths' ), + snoozeDurationOneYear: prplL10n( 'snoozeDurationOneYear' ), + snoozeDurationForever: prplL10n( 'snoozeDurationForever' ), + disabledRRCheckboxTooltip: prplL10n( 'disabledRRCheckboxTooltip' ), + markAsComplete: prplL10n( 'markAsComplete' ), + }, + + /** + * Fetch items for arguments. + * + * @param {Object} args The arguments to pass to the injectItems method. + * @return {Promise} A promise that resolves with the collection of posts. + */ + fetchItems: ( args ) => { + console.info( + `Fetching recommendations with args: ${ JSON.stringify( args ) }...` + ); + + const fetchData = { + status: args.status, + per_page: args.per_page || 1, + _embed: true, + exclude: prplSuggestedTask.injectedItemIds, + filter: { + orderby: 'menu_order', + order: 'ASC', + }, + }; + if ( args.category ) { + fetchData[ prplTerms.category ] = + prplTerms.get( 'category' )[ args.category ].id; + } + + return prplSuggestedTask + .getPostsCollectionPromise( { data: fetchData } ) + .then( ( response ) => response.data ); + }, + + /** + * Inject items from a category. + * + * @param {string} taskCategorySlug The task category slug. + * @param {string[]} taskStatus The task status. + */ + injectItemsFromCategory: ( args ) => + prplSuggestedTask + .fetchItems( { + category: args.category, + status: args.status || [ 'publish' ], + per_page: args.per_page || 1, + } ) + .then( ( data ) => { + if ( data.length ) { + // Inject the items into the DOM. + data.forEach( ( item ) => { + document.dispatchEvent( + new CustomEvent( 'prpl/suggestedTask/injectItem', { + detail: { + item, + listId: 'prpl-suggested-tasks-list', + insertPosition: 'beforeend', + }, + } ) + ); + prplSuggestedTask.injectedItemIds.push( item.id ); + } ); + } + + return data; + } ) + .then( ( data ) => { + // Toggle the "Loading..." text. + prplSuggestedTasksWidget.removeLoadingItems(); + + // Trigger the grid resize event. + window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); + + return data; + } ), + + /** + * Inject items. + * + * @param {Object[]} items The items to inject. + */ + injectItems: ( items ) => { + if ( items.length ) { + // Inject the items into the DOM. + items.forEach( ( item ) => { + document.dispatchEvent( + new CustomEvent( 'prpl/suggestedTask/injectItem', { + detail: { + item, + listId: 'prpl-suggested-tasks-list', + insertPosition: 'beforeend', + }, + } ) + ); + prplSuggestedTask.injectedItemIds.push( item.id ); + } ); + } + + // Trigger the grid resize event. + window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); + }, + + /** + * Get a collection of posts. + * + * @param {Object} fetchArgs The arguments to pass to the fetch method. + * @return {Promise} A promise that resolves with the collection of posts. + */ + getPostsCollectionPromise: ( fetchArgs ) => { + const collectionsPromise = new Promise( ( resolve ) => { + const postsCollection = + new wp.api.collections.Prpl_recommendations(); + postsCollection + .fetch( fetchArgs ) + .done( ( data ) => resolve( { data, postsCollection } ) ); + } ); + + return collectionsPromise; + }, + + /** + * Render a new item. + * + * @param {Object} post The post object. + * @param {boolean} useCheckbox Whether to use a checkbox. + */ + getNewItemTemplatePromise: ( { + post = {}, + useCheckbox = true, + listId = '', + } ) => + new Promise( ( resolve ) => { + const { + prpl_recommendations_provider, + prpl_recommendations_category, + } = post; + const terms = { + prpl_recommendations_provider, + prpl_recommendations_category, + }; + + Object.values( prplTerms.get( 'provider' ) ).forEach( ( term ) => { + if ( term.id === terms[ prplTerms.provider ][ 0 ] ) { + terms[ prplTerms.provider ] = term; + } + } ); + + Object.values( prplTerms.get( 'category' ) ).forEach( ( term ) => { + if ( term.id === terms[ prplTerms.category ][ 0 ] ) { + terms[ prplTerms.category ] = term; + } + } ); + + const template = wp.template( 'prpl-suggested-task' ); + const data = { + post, + terms, + useCheckbox, + listId, + assets: prplSuggestedTask.assets, + action: 'pending' === post.status ? 'celebrate' : '', + l10n: prplSuggestedTask.l10n, + }; + + resolve( template( data ) ); + } ), + + /** + * Run a task action. + * + * @param {number} postId The post ID. + * @param {string} actionType The action type. + * @return {Promise} A promise that resolves with the response from the server. + */ + runTaskAction: ( postId, actionType ) => + wp.ajax.post( 'progress_planner_suggested_task_action', { + post_id: postId, + nonce: prplSuggestedTask.nonce, + action_type: actionType, + } ), + + /** + * Trash (delete) a task. + * Only user tasks can be trashed. + * + * @param {number} postId The post ID. + */ + trash: ( postId ) => { + const post = new wp.api.models.Prpl_recommendations( { + id: postId, + } ); + post.fetch().then( () => { + // Handle the case when plain URL structure is used, it used to result in invalid URL (404): http://localhost:8080/index.php?rest_route=/wp/v2/prpl_recommendations/35?force=true + const url = post.url().includes( 'rest_route=' ) + ? post.url() + '&force=true' + : post.url() + '?force=true'; + + post.destroy( { url } ).then( () => { + // Remove the task from the todo list. + prplSuggestedTask.removeTaskElement( postId ); + setTimeout( + () => + window.dispatchEvent( + new CustomEvent( 'prpl/grid/resize' ) + ), + 500 + ); + + prplSuggestedTask.runTaskAction( postId, 'delete' ); + } ); + } ); + }, + + /** + * Maybe complete a task. + * + * @param {number} postId The post ID. + */ + maybeComplete: ( postId ) => { + // Get the task. + const post = new wp.api.models.Prpl_recommendations( { id: postId } ); + post.fetch().then( ( postData ) => { + const taskProviderId = prplTerms.getTerm( + postData?.[ prplTerms.provider ], + prplTerms.provider + ).slug; + const taskCategorySlug = prplTerms.getTerm( + postData?.[ prplTerms.category ], + prplTerms.category + ).slug; + + const el = prplSuggestedTask.getTaskElement( postId ); + + // Dismissable tasks don't have pending status, it's either publish or trash. + const newStatus = + 'publish' === postData.status ? 'trash' : 'publish'; + + // Disable the checkbox for RR tasks, to prevent multiple clicks. + el.querySelector( '.prpl-suggested-task-checkbox' ).setAttribute( + 'disabled', + 'disabled' + ); + + post.set( 'status', newStatus ) + .save() + .then( () => { + prplSuggestedTask.runTaskAction( + postId, + 'trash' === newStatus ? 'complete' : 'pending' + ); + const eventPoints = parseInt( postData?.meta?.prpl_points ); + + // Task is trashed, check if we need to celebrate. + if ( 'trash' === newStatus ) { + el.setAttribute( 'data-task-action', 'celebrate' ); + if ( 'user' === taskProviderId ) { + // Set class to trigger strike through animation. + el.classList.add( + 'prpl-suggested-task-celebrated' + ); + + setTimeout( () => { + // Move task from published to trash. + document + .getElementById( 'todo-list-completed' ) + .insertAdjacentElement( 'beforeend', el ); + + // Remove the class to trigger the strike through animation. + el.classList.remove( + 'prpl-suggested-task-celebrated' + ); + + window.dispatchEvent( + new CustomEvent( 'prpl/grid/resize' ) + ); + + // Remove the disabled attribute for user tasks, so they can be clicked again. + el.querySelector( + '.prpl-suggested-task-checkbox' + ).removeAttribute( 'disabled' ); + }, 2000 ); + } else { + /** + * Strike completed tasks and remove them from the DOM. + */ + document.dispatchEvent( + new CustomEvent( 'prpl/removeCelebratedTasks' ) + ); + + // Inject more tasks from the same category. + prplSuggestedTask.injectItemsFromCategory( { + category: taskCategorySlug, + status: [ 'publish' ], + } ); + } + + // We trigger celebration only if the task has points. + if ( 0 < eventPoints ) { + prplUpdateRaviGauge( eventPoints ); + + // Trigger the celebration event (confetti). + document.dispatchEvent( + new CustomEvent( 'prpl/celebrateTasks', { + detail: { element: el }, + } ) + ); + } + } else if ( + 'publish' === newStatus && + 'user' === taskProviderId + ) { + // This is only possible for user tasks. + // Set the task action to publish. + el.setAttribute( 'data-task-action', 'publish' ); + + // Update the Ravi gauge. + prplUpdateRaviGauge( 0 - eventPoints ); + + // Move task from trash to published. + document + .getElementById( 'todo-list' ) + .insertAdjacentElement( 'beforeend', el ); + + window.dispatchEvent( + new CustomEvent( 'prpl/grid/resize' ) + ); + + // Remove the disabled attribute for user tasks, so they can be clicked again. + el.querySelector( + '.prpl-suggested-task-checkbox' + ).removeAttribute( 'disabled' ); + } + } ); + } ); + }, + + /** + * Snooze a task. + * + * @param {number} postId The post ID. + * @param {string} snoozeDuration The snooze duration. + */ + snooze: ( postId, snoozeDuration ) => { + const snoozeDurationMap = { + '1-week': 7, + '2-weeks': 14, + '1-month': 30, + '3-months': 90, + '6-months': 180, + '1-year': 365, + forever: 3650, + }; + + const snoozeDurationDays = snoozeDurationMap[ snoozeDuration ]; + const date = new Date( + Date.now() + snoozeDurationDays * 24 * 60 * 60 * 1000 + ) + .toISOString() + .split( '.' )[ 0 ]; + const postModelToSave = new wp.api.models.Prpl_recommendations( { + id: postId, + status: 'future', + date, + date_gmt: date, + } ); + postModelToSave.save().then( ( postData ) => { + const taskCategorySlug = prplTerms.getTerm( + postData?.[ prplTerms.category ], + prplTerms.category + ).slug; + + prplSuggestedTask.removeTaskElement( postId ); + + // Inject more tasks from the same category. + prplSuggestedTask.injectItemsFromCategory( { + category: taskCategorySlug, + status: [ 'publish' ], + } ); + } ); + }, + + /** + * Run a tooltip action. + * + * @param {HTMLElement} button The button that was clicked. + */ + runButtonAction: ( button ) => { + let action = button.getAttribute( 'data-action' ); + const target = button.getAttribute( 'data-target' ); + const item = button.closest( 'li.prpl-suggested-task' ); + const tooltipActions = item.querySelector( '.tooltip-actions' ); + const elClass = '.prpl-suggested-task-' + target; + + // If the tooltip was already open, close it. + if ( + !! tooltipActions.querySelector( + `${ elClass }[data-tooltip-visible]` + ) + ) { + action = 'close-' + target; + } else { + const closestTaskListVisible = item + .closest( '.prpl-suggested-tasks-list' ) + .querySelector( `[data-tooltip-visible]` ); + // Close the any opened radio group. + closestTaskListVisible?.classList.remove( + 'prpl-toggle-radio-group-open' + ); + // Remove any existing tooltip visible attribute, in the entire list. + closestTaskListVisible?.removeAttribute( 'data-tooltip-visible' ); + } + + switch ( action ) { + case 'snooze': + tooltipActions + .querySelector( elClass ) + ?.setAttribute( 'data-tooltip-visible', 'true' ); + break; + + case 'close-snooze': + // Close the radio group. + tooltipActions + .querySelector( + `${ elClass }.prpl-toggle-radio-group-open` + ) + ?.classList.remove( 'prpl-toggle-radio-group-open' ); + // Close the tooltip. + tooltipActions + .querySelector( `${ elClass }[data-tooltip-visible]` ) + ?.removeAttribute( 'data-tooltip-visible' ); + break; + + case 'info': + tooltipActions + .querySelector( elClass ) + ?.setAttribute( 'data-tooltip-visible', 'true' ); + break; + + case 'close-info': + tooltipActions + .querySelector( elClass ) + .removeAttribute( 'data-tooltip-visible' ); + break; + + case 'move-up': + case 'move-down': + if ( 'move-up' === action && item.previousElementSibling ) { + item.parentNode.insertBefore( + item, + item.previousElementSibling + ); + } else if ( + 'move-down' === action && + item.nextElementSibling + ) { + item.parentNode.insertBefore( + item.nextElementSibling, + item + ); + } + // Trigger a custom event. + document.dispatchEvent( + new CustomEvent( 'prpl/suggestedTask/move', { + detail: { node: item }, + } ) + ); + break; + } + }, + + /** + * Update the task title. + * + * @param {HTMLElement} el The element that was edited. + */ + updateTaskTitle: ( el ) => { + // Add debounce to the input event. + clearTimeout( this.debounceTimeout ); + this.debounceTimeout = setTimeout( () => { + // Update an existing post. + const title = el.textContent.replace( /\n/g, '' ); + const postModel = new wp.api.models.Prpl_recommendations( { + id: parseInt( el.getAttribute( 'data-post-id' ) ), + title, + } ); + postModel.save().then( () => + // Update the task title. + document.dispatchEvent( + new CustomEvent( 'prpl/suggestedTask/update', { + detail: { + node: el.closest( 'li.prpl-suggested-task' ), + }, + } ) + ) + ); + el + .closest( 'li.prpl-suggested-task' ) + .querySelector( + 'label:has(.prpl-suggested-task-checkbox) .screen-reader-text' + ).innerHTML = `${ title }: ${ prplL10n( 'markAsComplete' ) }`; + }, 300 ); + }, + + /** + * Get the task element. + * + * @param {number} postId The post ID. + * @return {HTMLElement} The task element. + */ + getTaskElement: ( postId ) => + document.querySelector( + `.prpl-suggested-task[data-post-id="${ postId }"]` + ), + + /** + * Remove the task element. + * + * @param {number} postId The post ID. + */ + removeTaskElement: ( postId ) => + prplSuggestedTask.getTaskElement( postId )?.remove(), +}; + +/** + * Inject an item. + */ +document.addEventListener( 'prpl/suggestedTask/injectItem', ( event ) => { + prplSuggestedTask + .getNewItemTemplatePromise( { + post: event.detail.item, + listId: event.detail.listId, + } ) + .then( ( itemHTML ) => { + /** + * @todo Implement the parent task functionality. + * Use this code: `const parent = event.detail.item.parent && '' !== event.detail.item.parent ? event.detail.item.parent : null; + */ + const parent = false; + + if ( ! parent ) { + // Inject the item into the list. + document + .getElementById( event.detail.listId ) + .insertAdjacentHTML( + event.detail.insertPosition, + itemHTML + ); + + return; + } + + // If we could not find the parent item, try again after 500ms. + window.prplRenderAttempts = window.prplRenderAttempts || 0; + if ( window.prplRenderAttempts > 20 ) { + return; + } + const parentItem = document.querySelector( + `.prpl-suggested-task[data-task-id="${ parent }"]` + ); + if ( ! parentItem ) { + setTimeout( () => { + document.dispatchEvent( + new CustomEvent( 'prpl/suggestedTask/injectItem', { + detail: { + item: event.detail.item, + listId: event.detail.listId, + insertPosition: event.detail.insertPosition, + }, + } ) + ); + window.prplRenderAttempts++; + }, 100 ); + return; + } + + // If the child list does not exist, create it. + if ( + ! parentItem.querySelector( '.prpl-suggested-task-children' ) + ) { + const childListElement = document.createElement( 'ul' ); + childListElement.classList.add( + 'prpl-suggested-task-children' + ); + parentItem.appendChild( childListElement ); + } + + // Inject the item into the child list. + parentItem + .querySelector( '.prpl-suggested-task-children' ) + .insertAdjacentHTML( 'beforeend', itemHTML ); + } ); +} ); + +// When the 'prpl/suggestedTask/move' event is triggered, +// update the menu_order of the todo items. +document.addEventListener( 'prpl/suggestedTask/move', ( event ) => { + const listUl = event.detail.node.closest( 'ul' ); + const todoItemsIDs = []; + // Get all the todo items. + const todoItems = listUl.querySelectorAll( '.prpl-suggested-task' ); + let menuOrder = 0; + todoItems.forEach( ( todoItem ) => { + const itemID = parseInt( todoItem.getAttribute( 'data-post-id' ) ); + todoItemsIDs.push( itemID ); + todoItem.setAttribute( 'data-task-order', menuOrder ); + + listUl + .querySelector( `.prpl-suggested-task[data-post-id="${ itemID }"]` ) + .setAttribute( 'data-task-order', menuOrder ); + + // Update an existing post. + const post = new wp.api.models.Prpl_recommendations( { + id: itemID, + menu_order: menuOrder, + } ); + post.save(); + menuOrder++; + } ); +} ); + +/* eslint-enable camelcase, jsdoc/require-param-type, jsdoc/require-param, jsdoc/check-param-names */ diff --git a/assets/js/tour.js b/assets/js/tour.js index 6878a3500..754f87b91 100644 --- a/assets/js/tour.js +++ b/assets/js/tour.js @@ -115,10 +115,8 @@ function prplStartTour() { // Start the tour if the URL contains the query parameter. if ( window.location.href.includes( 'content-scan-finished=true' ) ) { - // If there are pending celebration tasks, delay the tour until celebration is done. + // If there are pending tasks, delay the tour until celebration is done. const delay = window.location.href.includes( 'delay-tour=true' ) ? 5000 : 0; - setTimeout( () => { - prplStartTour(); - }, delay ); + setTimeout( prplStartTour, delay ); } diff --git a/assets/js/upgrade-tasks.js b/assets/js/upgrade-tasks.js index de56557d2..340dbb107 100644 --- a/assets/js/upgrade-tasks.js +++ b/assets/js/upgrade-tasks.js @@ -12,8 +12,8 @@ * * @return {Promise} The promise of the tasks. */ -async function prplOnboardTasks() { - return new Promise( ( resolve ) => { +const prplOnboardTasks = async () => + new Promise( ( resolve ) => { ( async () => { const tasksElement = document.getElementById( 'prpl-onboarding-tasks' @@ -79,7 +79,6 @@ async function prplOnboardTasks() { resolve(); } )(); } ); -} /** * Redirect user to the stats page after onboarding or plugin upgrade. diff --git a/assets/js/web-components/prpl-badge-progress-bar.js b/assets/js/web-components/prpl-badge-progress-bar.js new file mode 100644 index 000000000..132fa4f8d --- /dev/null +++ b/assets/js/web-components/prpl-badge-progress-bar.js @@ -0,0 +1,164 @@ +/* global customElements, HTMLElement */ +/* + * Badge Progress Bar + * + * A web component to display a badge progress bar. + * + * Dependencies: progress-planner/l10n, progress-planner/web-components/prpl-badge + */ + +/** + * Register the custom web component. + */ +customElements.define( + 'prpl-badge-progress-bar', + class extends HTMLElement { + constructor( badgeId, points, maxPoints ) { + // Get parent class properties + super(); + badgeId = badgeId || this.getAttribute( 'data-badge-id' ); + points = points || this.getAttribute( 'data-points' ); + maxPoints = maxPoints || this.getAttribute( 'data-max-points' ); + const progress = ( points / maxPoints ) * 100; + + this.innerHTML = ` +
+
+
+ +
+
+ `; + } + } +); + +/** + * Update the previous month badge progress bar. + * + * @param {number} pointsDiff The points difference. + * + * @return {void} + */ +// eslint-disable-next-line no-unused-vars +const prplUpdatePreviousMonthBadgeProgressBar = ( pointsDiff ) => { + const progressBars = document.querySelectorAll( + '.prpl-previous-month-badge-progress-bar-wrapper prpl-badge-progress-bar' + ); + + // Bail early if no badge progress bars are found. + if ( ! progressBars.length ) { + return; + } + + // Get the 1st incomplete badge progress bar. + const progressBar = + parseInt( progressBars[ 0 ]?.getAttribute( 'data-points' ) ) >= + parseInt( progressBars[ 0 ]?.getAttribute( 'data-max-points' ) ) + ? progressBars[ 1 ] + : progressBars[ 0 ]; + + // Bail early if no badge progress bar is found. + if ( ! progressBar ) { + return; + } + + // Get the badge progress bar properties. + const badgeId = progressBar.getAttribute( 'data-badge-id' ); + const badgePoints = progressBar.getAttribute( 'data-points' ); + const badgeMaxPoints = progressBar.getAttribute( 'data-max-points' ); + const badgeProgress = customElements.get( 'prpl-badge-progress-bar' ); + const badgeNewPoints = parseInt( badgePoints ) + pointsDiff; + + // Create a new badge progress bar. + const newProgressBar = new badgeProgress( + badgeId, + badgeNewPoints, + badgeMaxPoints + ); + newProgressBar.setAttribute( 'data-badge-id', badgeId ); + newProgressBar.setAttribute( 'data-points', badgeNewPoints ); + newProgressBar.setAttribute( 'data-max-points', badgeMaxPoints ); + + // Replace the old badge progress bar with the new one. + progressBar.replaceWith( newProgressBar ); + + // Update the remaining points. + const remainingPointsEl = document.querySelector( + `.prpl-previous-month-badge-progress-bar-wrapper[data-badge-id="${ badgeId }"] .prpl-previous-month-badge-progress-bar-remaining` + ); + + if ( remainingPointsEl ) { + remainingPointsEl.textContent = remainingPointsEl.textContent.replace( + remainingPointsEl.getAttribute( 'data-remaining' ), + badgeMaxPoints - badgeNewPoints + ); + remainingPointsEl.setAttribute( + 'data-remaining', + badgeMaxPoints - badgeNewPoints + ); + } + + // Update the previous month badge points number. + const badgePointsNumberEl = document.querySelector( + `.prpl-previous-month-badge-progress-bar-wrapper[data-badge-id="${ badgeId }"] .prpl-widget-previous-ravi-points-number` + ); + if ( badgePointsNumberEl ) { + badgePointsNumberEl.textContent = badgeNewPoints + 'pt'; + } + + // If the previous month badge is completed, update badge elements. + if ( badgeNewPoints >= parseInt( badgeMaxPoints ) ) { + document + .querySelectorAll( + `.prpl-badge-row-wrapper-inner .prpl-badge prpl-badge[complete="false"][badge-id="${ badgeId }"]` + ) + ?.forEach( ( badge ) => { + badge.setAttribute( 'complete', 'true' ); + } ); + + // Remove the previous month badge progress bar. + document + .querySelector( + `.prpl-previous-month-badge-progress-bar-wrapper[data-badge-id="${ badgeId }"]` + ) + ?.remove(); + + // If there are no more progress bars, remove the previous month badge progress bar wrapper. + if ( + ! document.querySelector( + '.prpl-previous-month-badge-progress-bar-wrapper' + ) + ) { + document + .querySelector( + '.prpl-previous-month-badge-progress-bars-wrapper' + ) + ?.remove(); + } + } +}; diff --git a/assets/js/web-components/prpl-gauge.js b/assets/js/web-components/prpl-gauge.js index 1f4d84461..e740967f8 100644 --- a/assets/js/web-components/prpl-gauge.js +++ b/assets/js/web-components/prpl-gauge.js @@ -1,10 +1,10 @@ -/* global customElements, HTMLElement */ +/* global customElements, HTMLElement, prplUpdatePreviousMonthBadgeProgressBar */ /* * Web Component: prpl-gauge * * A web component that displays a gauge. * - * Dependencies: progress-planner/web-components/prpl-badge + * Dependencies: progress-planner/web-components/prpl-badge, progress-planner/web-components/prpl-badge-progress-bar */ /** @@ -97,3 +97,93 @@ customElements.define( } } ); + +/** + * Update the Ravi gauge. + * + * @param {number} pointsDiff The points difference. + * + * @return {void} + */ +// eslint-disable-next-line no-unused-vars +const prplUpdateRaviGauge = ( pointsDiff ) => { + if ( ! pointsDiff ) { + return; + } + + const gaugeElement = document.getElementById( 'prpl-gauge-ravi' ); + if ( ! gaugeElement ) { + return; + } + + const gaugeProps = { + id: gaugeElement.id, + background: gaugeElement.getAttribute( 'background' ), + color: gaugeElement.getAttribute( 'color' ), + max: gaugeElement.getAttribute( 'data-max' ), + value: gaugeElement.getAttribute( 'data-value' ), + badgeId: gaugeElement.getAttribute( 'data-badge-id' ), + }; + + if ( ! gaugeProps ) { + return; + } + + let newValue = parseInt( gaugeProps.value ) + pointsDiff; + newValue = Math.round( newValue ); + newValue = Math.max( 0, newValue ); + newValue = Math.min( newValue, parseInt( gaugeProps.max ) ); + + const Gauge = customElements.get( 'prpl-gauge' ); + const gauge = new Gauge( + { + max: parseInt( gaugeProps.max ), + value: parseFloat( newValue / parseInt( gaugeProps.max ) ), + background: gaugeProps.background, + color: gaugeProps.color, + maxDeg: '180deg', + start: '270deg', + cutout: '57%', + contentFontSize: 'var(--prpl-font-size-6xl)', + contentPadding: + 'var(--prpl-padding) var(--prpl-padding) calc(var(--prpl-padding) * 2) var(--prpl-padding)', + marginBottom: 'var(--prpl-padding)', + }, + `` + ); + gauge.id = gaugeProps.id; + gauge.setAttribute( 'background', gaugeProps.background ); + gauge.setAttribute( 'color', gaugeProps.color ); + gauge.setAttribute( 'data-max', gaugeProps.max ); + gauge.setAttribute( 'data-value', newValue ); + gauge.setAttribute( 'data-badge-id', gaugeProps.badgeId ); + + // Replace the old gauge with the new one. + const oldGauge = document.getElementById( gaugeProps.id ); + if ( oldGauge ) { + oldGauge.replaceWith( gauge ); + } + + const oldCounter = document.getElementById( + 'prpl-widget-content-ravi-points-number' + ); + if ( oldCounter ) { + oldCounter.textContent = newValue + 'pt'; + } + + // Mark badge as completed, in the a Monthly badges widgets, if we reached the max points. + if ( newValue >= parseInt( gaugeProps.max ) ) { + // We have multiple badges, one in widget and the other in the popover. + document + .querySelectorAll( + `.prpl-badge-row-wrapper-inner .prpl-badge prpl-badge[complete="false"][badge-id="${ gaugeProps.badgeId }"]` + ) + ?.forEach( ( badge ) => { + badge.setAttribute( 'complete', 'true' ); + } ); + + if ( gaugeProps.value >= parseInt( gaugeProps.max ) ) { + prplUpdatePreviousMonthBadgeProgressBar( pointsDiff ); + } + } +}; diff --git a/assets/js/web-components/prpl-suggested-task.js b/assets/js/web-components/prpl-suggested-task.js deleted file mode 100644 index 8778713e1..000000000 --- a/assets/js/web-components/prpl-suggested-task.js +++ /dev/null @@ -1,655 +0,0 @@ -/* global customElements, HTMLElement, prplSuggestedTask, prplL10n */ -/* - * Suggested Task - * - * A web component to display a suggested task. - * - * Dependencies: progress-planner/l10n - */ -/* eslint-disable camelcase */ - -/** - * Register the custom web component. - */ -customElements.define( - 'prpl-suggested-task', - class extends HTMLElement { - constructor( { - task_id, - title, - description, - points = 0, - action = '', - url = '', - url_target = '_self', - dismissable = false, - provider_id = '', - category = '', - snoozable = true, - order = false, - deletable = false, - useCheckbox = true, - taskList = '', // prplSuggestedTasks or progressPlannerTodo. - popover_id, - } ) { - // Get parent class properties - super(); - - this.setAttribute( 'role', 'listitem' ); - - let taskHeading = title; - if ( url ) { - taskHeading = `${ title }`; - } - - if ( popover_id ) { - taskHeading = `${ title }`; - } - - const getTaskStatus = () => { - let status = 'pending'; - window[ taskList ].tasks.forEach( ( task ) => { - if ( task.task_id === task_id ) { - status = task.status; - } - } ); - return status; - }; - - const actionButtons = { - move: - false !== order - ? ` - - - ` - : '', - info: description - ? ` - - - - - ${ description } - - ` - : '', - snooze: snoozable - ? ` - - - - - -
- - - ${ prplL10n( 'snoozeThisTask' ) } - - - - -
- - - - - - -
-
-
-
` - : '', - complete: - dismissable && ! useCheckbox - ? `` - : '', - delete: deletable - ? `` - : '', - completeCheckbox: ( () => { - if ( ! useCheckbox ) { - return ''; - } - let output = ''; - let checkboxStyle = 'margin-top: 2px;'; - - // If the task is not dismissable, checkbox is disabled and we want to show a tooltip. - if ( ! dismissable ) { - checkboxStyle += 'pointer-events: none;'; - output += ` - `; - } - - output += ``; - - if ( ! dismissable ) { - output += ` - - - ${ prplL10n( 'disabledRRCheckboxTooltip' ) } - - - `; - } - - return output; - } )(), - }; - - const taskPointsElement = points - ? ` - +${ points } - ` - : ''; - - this.innerHTML = ` -
  • - ${ actionButtons.completeCheckbox } -

    - ${ taskHeading } -

    -
    -
    - ${ actionButtons.info } - ${ actionButtons.move } - ${ actionButtons.snooze } - ${ actionButtons.complete } - ${ actionButtons.delete } -
    - ${ taskPointsElement } -
    -
  • `; - - this.taskListeners(); - } - - /** - * Add listeners to the item. - */ - taskListeners = () => { - const thisObj = this, - item = thisObj.querySelector( 'li' ); - - item.querySelector( - '.prpl-suggested-task-checkbox' - ).addEventListener( 'change', function ( e ) { - thisObj.runTaskAction( - item.getAttribute( 'data-task-id' ), - e.target.checked ? 'complete' : 'pending' - ); - } ); - - item.querySelectorAll( '.prpl-suggested-task-button' ).forEach( - ( button ) => { - button.addEventListener( 'click', function () { - let action = button.getAttribute( 'data-action' ); - const target = button.getAttribute( 'data-target' ); - const tooltipActions = - item.querySelector( '.tooltip-actions' ); - - // If the tooltip was already open, close it. - if ( - !! tooltipActions.querySelector( - '.prpl-suggested-task-' + - target + - '[data-tooltip-visible]' - ) - ) { - action = 'close-' + target; - } else { - const closestTaskListVisible = item - .closest( '.prpl-suggested-tasks-list' ) - .querySelector( `[data-tooltip-visible]` ); - // Close the any opened radio group. - closestTaskListVisible?.classList.remove( - 'prpl-toggle-radio-group-open' - ); - // Remove any existing tooltip visible attribute, in the entire list. - closestTaskListVisible?.removeAttribute( - 'data-tooltip-visible' - ); - } - - switch ( action ) { - case 'snooze': - tooltipActions - .querySelector( - '.prpl-suggested-task-' + target - ) - .setAttribute( - 'data-tooltip-visible', - 'true' - ); - break; - - case 'close-snooze': - // Close the radio group. - tooltipActions - .querySelector( - '.prpl-suggested-task-' + - target + - '.prpl-toggle-radio-group-open' - ) - ?.classList.remove( - 'prpl-toggle-radio-group-open' - ); - // Close the tooltip. - tooltipActions - .querySelector( - '.prpl-suggested-task-' + - target + - '[data-tooltip-visible]' - ) - ?.removeAttribute( 'data-tooltip-visible' ); - break; - - case 'info': - tooltipActions - .querySelector( - '.prpl-suggested-task-' + target - ) - .setAttribute( - 'data-tooltip-visible', - 'true' - ); - break; - - case 'close-info': - tooltipActions - .querySelector( - '.prpl-suggested-task-' + target - ) - .removeAttribute( 'data-tooltip-visible' ); - break; - - case 'move-up': - case 'move-down': - // Move `thisObj` before or after the previous or next sibling. - if ( - 'move-up' === action && - thisObj.previousElementSibling - ) { - thisObj.parentNode.insertBefore( - thisObj, - thisObj.previousElementSibling - ); - } else if ( - 'move-down' === action && - thisObj.nextElementSibling - ) { - thisObj.parentNode.insertBefore( - thisObj.nextElementSibling, - thisObj - ); - } - // Trigger a custom event. - document.dispatchEvent( - new CustomEvent( - 'prpl/suggestedTask/move', - { - detail: { node: thisObj }, - } - ) - ); - break; - - default: - thisObj.runTaskAction( - item.getAttribute( 'data-task-id' ), - action - ); - break; - } - } ); - } - ); - - // Toggle snooze duration radio group. - item.querySelector( '.prpl-toggle-radio-group' )?.addEventListener( - 'click', - function () { - this.closest( - '.prpl-suggested-task-snooze' - ).classList.toggle( 'prpl-toggle-radio-group-open' ); - } - ); - - // Handle snooze duration radio group change. - item.querySelectorAll( - '.prpl-snooze-duration-radio-group input[type="radio"]' - ).forEach( ( radioElement ) => { - radioElement.addEventListener( 'change', function () { - thisObj.runTaskAction( - item.getAttribute( 'data-task-id' ), - 'snooze', - this.value - ); - } ); - } ); - - // When an item's contenteditable element is edited, - // save the new content to the database - const h3Span = this.querySelector( 'h3 span' ); - h3Span.addEventListener( 'keydown', ( event ) => { - // Prevent insering newlines (this catches both Enter and Return). - if ( event.key === 'Enter' ) { - event.preventDefault(); - } - - // Add debounce to the input event. - clearTimeout( this.debounceTimeout ); - this.debounceTimeout = setTimeout( () => { - const title = h3Span.textContent; - wp.ajax - .post( 'progress_planner_save_user_suggested_task', { - task: { - task_id: item.getAttribute( 'data-task-id' ), - title, - provider_id: 'user', - category: 'user', - dismissable: true, - }, - nonce: prplSuggestedTask.nonce, - } ) - .done( () => { - // Update the task title. - document.dispatchEvent( - new CustomEvent( 'prpl/suggestedTask/update', { - detail: { node: thisObj }, - } ) - ); - - h3Span - .closest( '.prpl-suggested-task' ) - .querySelector( - 'label:has(.prpl-suggested-task-checkbox) .screen-reader-text' - ).innerHTML = title; - } ); - }, 300 ); - } ); - }; - - /** - * Snooze a task. - * - * @param {string} task_id The task ID. - * @param {string} actionType The action type. - * @param {string} snoozeDuration If the action is `snooze`, - * the duration to snooze the task for. - */ - runTaskAction = ( task_id, actionType, snoozeDuration ) => { - task_id = task_id.toString(); - const providerID = this.querySelector( 'li' ).getAttribute( - 'data-task-provider-id' - ); - const category = - this.querySelector( 'li' ).getAttribute( 'data-task-category' ); - const taskPoints = parseInt( - this.querySelector( 'li' ).getAttribute( 'data-task-points' ) - ); - const taskList = - this.querySelector( 'li' ).getAttribute( 'data-task-list' ); - - const data = { - task_id, - nonce: prplSuggestedTask.nonce, - action_type: actionType, - }; - if ( 'snooze' === actionType ) { - data.duration = snoozeDuration; - } - - // Save the todo list to the database. - const request = wp.ajax.post( - 'progress_planner_suggested_task_action', - data - ); - request.done( () => { - const el = document.querySelector( - `.prpl-suggested-task[data-task-id="${ task_id }"]` - ); - - switch ( actionType ) { - case 'snooze': - el.remove(); - // Update the global var. - window[ taskList ].tasks.forEach( ( task, index ) => { - if ( task.task_id === task_id ) { - window[ taskList ].tasks[ index ].status = - 'snoozed'; - } - } ); - break; - - case 'complete': - // Add the task to the pending celebration. - window[ taskList ].tasks.forEach( ( task, index ) => { - if ( task.task_id === task_id ) { - window[ taskList ].tasks[ index ].status = - 'pending_celebration'; - } - } ); - // Set the task action to celebrate. - el.setAttribute( 'data-task-action', 'celebrate' ); - - document.dispatchEvent( - new CustomEvent( 'prpl/updateRaviGauge', { - detail: { - pointsDiff: parseInt( - this.querySelector( 'li' ).getAttribute( - 'data-task-points' - ) - ), - }, - } ) - ); - - const celebrateEvents = - 0 < taskPoints - ? [ 'prpl/celebrateTasks' ] - : [ - 'prpl/strikeCelebratedTasks', - 'prpl/markTasksAsCompleted', - ]; - - // Trigger the celebration events. - celebrateEvents.forEach( ( event ) => { - document.dispatchEvent( - new CustomEvent( event, { - detail: { - element: el, - taskList, - }, - } ) - ); - } ); - - break; - - case 'pending': - // Change the task status to pending. - window[ taskList ].tasks.forEach( ( task, index ) => { - if ( task.task_id === task_id ) { - window[ taskList ].tasks[ index ].status = - 'pending'; - } - } ); - // Set the task action to pending. - el.setAttribute( 'data-task-action', 'pending' ); - - // Update the Ravi gauge. - document.dispatchEvent( - new CustomEvent( 'prpl/updateRaviGauge', { - detail: { - pointsDiff: - 0 - - parseInt( - this.querySelector( - 'li' - ).getAttribute( 'data-task-points' ) - ), - }, - } ) - ); - - break; - - case 'delete': - // Update the Ravi gauge. - document.dispatchEvent( - new CustomEvent( 'prpl/updateRaviGauge', { - detail: { - pointsDiff: - 0 - - parseInt( - this.querySelector( - 'li' - ).getAttribute( 'data-task-points' ) - ), - }, - } ) - ); - - // Remove the task from the todo list. - el.closest( 'prpl-suggested-task' ).remove(); - document.dispatchEvent( - new CustomEvent( 'prpl/grid/resize' ) - ); - break; - } - - document.dispatchEvent( - new CustomEvent( 'prpl/suggestedTask/maybeInjectItem', { - detail: { - task_id, - providerID, - actionType, - category, - }, - } ) - ); - } ); - }; - } -); - -/* eslint-enable camelcase */ diff --git a/assets/js/widgets/suggested-tasks-badge-scroller.js b/assets/js/widgets/suggested-tasks-badge-scroller.js new file mode 100644 index 000000000..028f7185f --- /dev/null +++ b/assets/js/widgets/suggested-tasks-badge-scroller.js @@ -0,0 +1,150 @@ +/* global prplDocumentReady */ +/* + * Widget: Badge Scroller + * + * A widget that displays a list of badges. + * + * Dependencies: progress-planner/document-ready + */ + +// Handle the monthly badges scrolling. +prplDocumentReady( () => { + // Initialize the badge scroller. + document + .querySelectorAll( + '.prpl-widget-wrapper:not(.in-popover) > .badge-group-monthly' + ) + .forEach( ( element ) => { + new BadgeScroller( element ); + } ); +} ); +class BadgeScroller { + constructor( element ) { + this.element = element; + + this.badgeButtonUp = this.element.querySelector( + '.prpl-badge-row-button-up' + ); + this.badgeButtonDown = this.element.querySelector( + '.prpl-badge-row-button-down' + ); + this.badgeRowWrapper = this.element.querySelector( + '.prpl-badge-row-wrapper' + ); + this.badgeRowWrapperInner = this.element.querySelector( + '.prpl-badge-row-wrapper-inner' + ); + this.badges = + this.badgeRowWrapperInner.querySelectorAll( '.prpl-badge' ); + this.totalRows = this.badges.length / 3; + + this.init(); + } + + init() { + this.addEventListeners(); + + // On page load, when all images are loaded. + const images = [ ...this.element.querySelectorAll( 'img' ) ]; + if ( images.length ) { + Promise.all( + images.map( + ( im ) => + new Promise( ( resolve ) => ( im.onload = resolve ) ) + ) + ).then( () => this.setWrapperHeight() ); + } + + // When popover is opened. + document + .querySelector( '#prpl-popover-monthly-badges' ) + .addEventListener( 'toggle', ( event ) => { + if ( 'open' === event.newState ) { + this.setWrapperHeight(); + } + } ); + + // Handle window resize. + window.addEventListener( 'resize', () => { + this.setWrapperHeight(); + } ); + } + + setWrapperHeight() { + const computedStyle = window.getComputedStyle( + this.badgeRowWrapperInner + ); + const gridGap = parseInt( computedStyle.gap ); + + // Set CSS variables for the transform calculation. + this.badgeRowWrapper.style.setProperty( + '--row-height', + `${ this.badges[ 0 ].offsetHeight }px` + ); + this.badgeRowWrapper.style.setProperty( + '--grid-gap', + `${ gridGap }px` + ); + + // Set wrapper height to show 2 rows. + const twoRowsHeight = this.badges[ 0 ].offsetHeight * 2 + gridGap; + this.badgeRowWrapperInner.style.height = twoRowsHeight + 'px'; + } + + addEventListeners() { + this.badgeButtonUp.addEventListener( 'click', () => + this.handleUpClick() + ); + this.badgeButtonDown.addEventListener( 'click', () => + this.handleDownClick() + ); + } + + handleUpClick() { + const computedStyle = window.getComputedStyle( + this.badgeRowWrapperInner + ); + const currentRow = + computedStyle.getPropertyValue( '--prpl-current-row' ); + const nextRow = parseInt( currentRow ) - 1; + + this.badgeButtonDown + .closest( '.prpl-badge-row-button-wrapper' ) + .classList.remove( 'prpl-badge-row-button-disabled' ); + + this.badgeRowWrapperInner.style.setProperty( + '--prpl-current-row', + nextRow + ); + + if ( nextRow <= 1 ) { + this.badgeButtonUp + .closest( '.prpl-badge-row-button-wrapper' ) + .classList.add( 'prpl-badge-row-button-disabled' ); + } + } + + handleDownClick() { + const computedStyle = window.getComputedStyle( + this.badgeRowWrapperInner + ); + const currentRow = + computedStyle.getPropertyValue( '--prpl-current-row' ); + const nextRow = parseInt( currentRow ) + 1; + + this.badgeButtonUp + .closest( '.prpl-badge-row-button-wrapper' ) + .classList.remove( 'prpl-badge-row-button-disabled' ); + + this.badgeRowWrapperInner.style.setProperty( + '--prpl-current-row', + nextRow + ); + + if ( nextRow >= this.totalRows - 1 ) { + this.badgeButtonDown + .closest( '.prpl-badge-row-button-wrapper' ) + .classList.add( 'prpl-badge-row-button-disabled' ); + } + } +} diff --git a/assets/js/widgets/suggested-tasks.js b/assets/js/widgets/suggested-tasks.js index f6495d5f9..77196eb4a 100644 --- a/assets/js/widgets/suggested-tasks.js +++ b/assets/js/widgets/suggested-tasks.js @@ -1,448 +1,183 @@ -/* global customElements, prplSuggestedTasks, prplDocumentReady */ +/* global prplSuggestedTask, prplTerms, prplTodoWidget */ /* * Widget: Suggested Tasks * * A widget that displays a list of suggested tasks. * - * Dependencies: progress-planner/web-components/prpl-suggested-task, progress-planner/celebrate, progress-planner/grid-masonry, progress-planner/web-components/prpl-suggested-task, progress-planner/document-ready, progress-planner/web-components/prpl-tooltip + * Dependencies: wp-api, progress-planner/suggested-task, progress-planner/widgets/todo, progress-planner/celebrate, progress-planner/grid-masonry, progress-planner/web-components/prpl-tooltip, progress-planner/suggested-task-terms */ /* eslint-disable camelcase */ -/** - * Get the next item to inject. - * - * @param {string} category The category of items to get the next item from. - * @return {Object} The next item to inject. - */ -const prplSuggestedTasksGetNextPendingItemFromCategory = ( category ) => { - // Get items of this category. - const itemsOfCategory = prplSuggestedTasks.tasks.filter( - ( task ) => category === task.category - ); - - // If there are no items of this category, return null. - if ( 0 === itemsOfCategory.length || 'user' === category ) { - return null; - } - - // Create an array of items that are in the list. - const inList = []; - document - .querySelectorAll( '.prpl-suggested-task' ) - .forEach( function ( item ) { - inList.push( item.getAttribute( 'data-task-id' ).toString() ); - } ); - - const items = itemsOfCategory.filter( function ( item ) { - // Skip items which are not pending. - if ( 'pending' !== item.status ) { - return false; - } - // Remove items which are already in the list. - if ( inList.includes( item.task_id.toString() ) ) { - return false; - } - return true; - } ); - - // Do nothing if there are no items left. - if ( 0 === items.length ) { - return null; - } - - // Return the first item. - return items[ 0 ]; -}; - -/** - * Inject the next item from a category. - */ -document.addEventListener( - 'prpl/suggestedTask/injectCategoryItem', - ( event ) => { - const nextItem = prplSuggestedTasksGetNextPendingItemFromCategory( - event.detail.category - ); - if ( ! nextItem ) { - return; - } - - document.dispatchEvent( - new CustomEvent( 'prpl/suggestedTask/injectItem', { - detail: nextItem, - } ) +const prplSuggestedTasksWidget = { + /** + * Remove the "Loading..." text and resize the grid items. + */ + removeLoadingItems: () => { + document.querySelector( '.prpl-suggested-tasks-loading' )?.remove(); + setTimeout( + () => window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ), + 2000 ); - } -); - -/** - * Inject a todo item. - */ -document.addEventListener( 'prpl/suggestedTask/injectItem', ( event ) => { - const Item = customElements.get( 'prpl-suggested-task' ); - const item = new Item( { - ...event.detail, - taskList: 'prplSuggestedTasks', - } ); + }, /** - * @todo Implement the parent task functionality. - * Use this code: `const parent = event.detail.parent && '' !== event.detail.parent ? event.detail.parent : null; + * Populate the suggested tasks list. */ - const parent = false; - - if ( ! parent ) { - // Inject the item into the list. - document - .querySelector( '.prpl-suggested-tasks-list' ) - .insertAdjacentElement( 'beforeend', item ); - - return; - } - - // If we could not find the parent item, try again after 500ms. - window.prplRenderAttempts = window.prplRenderAttempts || 0; - if ( window.prplRenderAttempts > 500 ) { - return; - } - const parentItem = document.querySelector( - `.prpl-suggested-task[data-task-id="${ parent }"]` - ); - if ( ! parentItem ) { - setTimeout( () => { - document.dispatchEvent( - new CustomEvent( 'prpl/suggestedTask/injectItem', { - detail: event.detail, - } ) - ); - window.prplRenderAttempts++; - }, 10 ); - return; - } - - // If the child list does not exist, create it. - if ( ! parentItem.querySelector( '.prpl-suggested-task-children' ) ) { - const childListElement = document.createElement( 'ul' ); - childListElement.classList.add( 'prpl-suggested-task-children' ); - parentItem.appendChild( childListElement ); - } - - // Inject the item into the child list. - parentItem - .querySelector( '.prpl-suggested-task-children' ) - .insertAdjacentElement( 'beforeend', item ); -} ); - -if ( - ! prplSuggestedTasks.delayCelebration && - prplSuggestedTasks.tasks.filter( - ( task ) => 'pending_celebration' === task.status - ).length -) { - setTimeout( () => { - // Trigger the celebration event. - document.dispatchEvent( new CustomEvent( 'prpl/celebrateTasks' ) ); - }, 3000 ); -} - -// Populate the list on load. -prplDocumentReady( () => { - // Do nothing if the list does not exist. - if ( ! document.querySelector( '.prpl-suggested-tasks-list' ) ) { - return; - } - - // Loop through each provider and inject items. - for ( const category in prplSuggestedTasks.maxItemsPerCategory ) { - // Inject items, until we reach the maximum number of channel items. - while ( - document.querySelectorAll( - `.prpl-suggested-task[data-task-category="${ category }"]` - ).length < - parseInt( - prplSuggestedTasks.maxItemsPerCategory[ category ] - ) && - prplSuggestedTasksGetNextPendingItemFromCategory( category ) - ) { - document.dispatchEvent( - new CustomEvent( 'prpl/suggestedTask/injectCategoryItem', { - detail: { category }, - } ) - ); + populateList: () => { + // Do nothing if the list does not exist. + if ( ! document.querySelector( '.prpl-suggested-tasks-list' ) ) { + return; } - } - // Inject ALL pending celebration tasks. - prplSuggestedTasks.tasks - .filter( ( task ) => 'pending_celebration' === task.status ) - .forEach( ( task ) => { - document.dispatchEvent( - new CustomEvent( 'prpl/suggestedTask/injectItem', { - detail: task, - } ) - ); - } ); + // If preloaded tasks are available, inject them. + if ( 'undefined' !== typeof prplSuggestedTask.tasks ) { + // Inject the tasks. + if ( Object.keys( prplSuggestedTask.tasks.pendingTasks ).length ) { + Object.keys( prplSuggestedTask.tasks.pendingTasks ).forEach( + ( category ) => { + prplSuggestedTask.injectItems( + prplSuggestedTask.tasks.pendingTasks[ category ] + ); + } + ); + } - window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); + // Inject the pending celebration tasks, but only on Progress Planner dashboard page. + if ( + ! prplSuggestedTask.delayCelebration && + Object.keys( prplSuggestedTask.tasks.pendingCelebrationTasks ) + .length + ) { + Object.keys( + prplSuggestedTask.tasks.pendingCelebrationTasks + ).forEach( ( category ) => { + prplSuggestedTask.injectItems( + prplSuggestedTask.tasks.pendingCelebrationTasks[ + category + ] + ); + + // Set post status to trash. + prplSuggestedTask.tasks.pendingCelebrationTasks[ + category + ].forEach( ( task ) => { + const post = new wp.api.models.Prpl_recommendations( { + id: task.id, + } ); + // Destroy the post, without the force parameter. + post.destroy( { url: post.url() } ); + } ); + } ); - // Initialize the badge scroller. - document - .querySelectorAll( - '.prpl-widget-wrapper:not(.in-popover) > .badge-group-monthly' - ) - .forEach( ( element ) => { - new BadgeScroller( element ); - } ); -} ); + // Trigger the celebration event (trigger confetti, strike through tasks, remove from DOM). + setTimeout( () => { + // Trigger the celebration event. + document.dispatchEvent( + new CustomEvent( 'prpl/celebrateTasks' ) + ); + + /** + * Strike completed tasks and remove them from the DOM. + */ + document.dispatchEvent( + new CustomEvent( 'prpl/removeCelebratedTasks' ) + ); + + // Trigger the grid resize event. + window.dispatchEvent( + new CustomEvent( 'prpl/grid/resize' ) + ); + }, 3000 ); + } -// Handle the monthly badges scrolling. -class BadgeScroller { - constructor( element ) { - this.element = element; + // Toggle the "Loading..." text. + prplSuggestedTasksWidget.removeLoadingItems(); + } else { + // Otherwise, inject tasks from the API. + const celebrationPromises = []; - this.badgeButtonUp = this.element.querySelector( - '.prpl-badge-row-button-up' - ); - this.badgeButtonDown = this.element.querySelector( - '.prpl-badge-row-button-down' - ); - this.badgeRowWrapper = this.element.querySelector( - '.prpl-badge-row-wrapper' - ); - this.badgeRowWrapperInner = this.element.querySelector( - '.prpl-badge-row-wrapper-inner' - ); - this.badges = - this.badgeRowWrapperInner.querySelectorAll( '.prpl-badge' ); - this.totalRows = this.badges.length / 3; - - this.init(); - } + // Loop through each provider and inject items. + for ( const category in prplSuggestedTask.maxItemsPerCategory ) { + if ( 'user' === category ) { + continue; + } - init() { - this.addEventListeners(); + // Inject published tasks. + prplSuggestedTask.injectItemsFromCategory( { + category, + status: [ 'publish' ], + per_page: prplSuggestedTask.maxItemsPerCategory[ category ], + } ); - // On page load, when all images are loaded. - const images = [ ...this.element.querySelectorAll( 'img' ) ]; - if ( images.length ) { - Promise.all( - images.map( - ( im ) => - new Promise( ( resolve ) => ( im.onload = resolve ) ) - ) - ).then( () => { - this.setWrapperHeight(); - } ); - } + // We trigger celebration only on Progress Planner dashboard page. + if ( ! prplSuggestedTask.delayCelebration ) { + // Inject pending celebration tasks. + celebrationPromises.push( + prplSuggestedTask + .injectItemsFromCategory( { + category, + status: [ 'pending' ], + per_page: 100, + } ) + .then( ( data ) => { + // If there were pending tasks. + if ( data.length ) { + // Set post status to trash. + data.forEach( ( task ) => { + const post = + new wp.api.models.Prpl_recommendations( + { + id: task.id, + } + ); + // Destroy the post, without the force parameter. + post.destroy( { url: post.url() } ); + } ); + } + } ) + ); + } + } - // When popover is opened. - document - .querySelector( '#prpl-popover-monthly-badges' ) - .addEventListener( 'toggle', ( event ) => { - if ( 'open' === event.newState ) { - this.setWrapperHeight(); + // Trigger celebration once, for all categories. + Promise.all( celebrationPromises ).then( () => { + if ( + 0 < + document.querySelectorAll( + '.prpl-suggested-tasks-list [data-task-action="celebrate"]' + ).length + ) { + // Trigger the celebration event (trigger confetti, strike through tasks, remove from DOM). + setTimeout( () => { + // Trigger the celebration event. + document.dispatchEvent( + new CustomEvent( 'prpl/celebrateTasks' ) + ); + + /** + * Strike completed tasks and remove them from the DOM. + */ + document.dispatchEvent( + new CustomEvent( 'prpl/removeCelebratedTasks' ) + ); + + // Trigger the grid resize event. + window.dispatchEvent( + new CustomEvent( 'prpl/grid/resize' ) + ); + }, 3000 ); } } ); - - // Handle window resize. - window.addEventListener( 'resize', () => { - this.setWrapperHeight(); - } ); - } - - setWrapperHeight() { - const computedStyle = window.getComputedStyle( - this.badgeRowWrapperInner - ); - const gridGap = parseInt( computedStyle.gap ); - - // Set CSS variables for the transform calculation. - this.badgeRowWrapper.style.setProperty( - '--row-height', - `${ this.badges[ 0 ].offsetHeight }px` - ); - this.badgeRowWrapper.style.setProperty( - '--grid-gap', - `${ gridGap }px` - ); - - // Set wrapper height to show 2 rows. - const twoRowsHeight = this.badges[ 0 ].offsetHeight * 2 + gridGap; - this.badgeRowWrapperInner.style.height = twoRowsHeight + 'px'; - } - - addEventListeners() { - this.badgeButtonUp.addEventListener( 'click', () => - this.handleUpClick() - ); - this.badgeButtonDown.addEventListener( 'click', () => - this.handleDownClick() - ); - } - - handleUpClick() { - const computedStyle = window.getComputedStyle( - this.badgeRowWrapperInner - ); - const currentRow = - computedStyle.getPropertyValue( '--prpl-current-row' ); - const nextRow = parseInt( currentRow ) - 1; - - this.badgeButtonDown - .closest( '.prpl-badge-row-button-wrapper' ) - .classList.remove( 'prpl-badge-row-button-disabled' ); - - this.badgeRowWrapperInner.style.setProperty( - '--prpl-current-row', - nextRow - ); - - if ( nextRow <= 1 ) { - this.badgeButtonUp - .closest( '.prpl-badge-row-button-wrapper' ) - .classList.add( 'prpl-badge-row-button-disabled' ); } - } - - handleDownClick() { - const computedStyle = window.getComputedStyle( - this.badgeRowWrapperInner - ); - const currentRow = - computedStyle.getPropertyValue( '--prpl-current-row' ); - const nextRow = parseInt( currentRow ) + 1; - - this.badgeButtonUp - .closest( '.prpl-badge-row-button-wrapper' ) - .classList.remove( 'prpl-badge-row-button-disabled' ); - - this.badgeRowWrapperInner.style.setProperty( - '--prpl-current-row', - nextRow - ); - - if ( nextRow >= this.totalRows - 1 ) { - this.badgeButtonDown - .closest( '.prpl-badge-row-button-wrapper' ) - .classList.add( 'prpl-badge-row-button-disabled' ); - } - } -} + }, +}; /** - * Update the Ravi gauge. + * Populate the suggested tasks list when the terms are loaded. */ -document.addEventListener( - 'prpl/updateRaviGauge', - ( e ) => { - if ( ! e.detail.pointsDiff ) { - return; - } - - const gaugeElement = document.getElementById( 'prpl-gauge-ravi' ); - if ( ! gaugeElement ) { - return; - } - - const gaugeProps = { - id: gaugeElement.id, - background: gaugeElement.getAttribute( 'background' ), - color: gaugeElement.getAttribute( 'color' ), - max: gaugeElement.getAttribute( 'data-max' ), - value: gaugeElement.getAttribute( 'data-value' ), - badgeId: gaugeElement.getAttribute( 'data-badge-id' ), - }; - - if ( ! gaugeProps ) { - return; - } - - let newValue = parseInt( gaugeProps.value ) + e.detail.pointsDiff; - newValue = Math.round( newValue ); - newValue = Math.max( 0, newValue ); - newValue = Math.min( newValue, parseInt( gaugeProps.max ) ); - - const Gauge = customElements.get( 'prpl-gauge' ); - const gauge = new Gauge( - { - max: parseInt( gaugeProps.max ), - value: parseFloat( newValue / parseInt( gaugeProps.max ) ), - background: gaugeProps.background, - color: gaugeProps.color, - maxDeg: '180deg', - start: '270deg', - cutout: '57%', - contentFontSize: 'var(--prpl-font-size-6xl)', - contentPadding: - 'var(--prpl-padding) var(--prpl-padding) calc(var(--prpl-padding) * 2) var(--prpl-padding)', - marginBottom: 'var(--prpl-padding)', - }, - `` - ); - gauge.id = gaugeProps.id; - gauge.setAttribute( 'background', gaugeProps.background ); - gauge.setAttribute( 'color', gaugeProps.color ); - gauge.setAttribute( 'data-max', gaugeProps.max ); - gauge.setAttribute( 'data-value', newValue ); - gauge.setAttribute( 'data-badge-id', gaugeProps.badgeId ); - - // Replace the old gauge with the new one. - const oldGauge = document.getElementById( gaugeProps.id ); - if ( oldGauge ) { - oldGauge.replaceWith( gauge ); - } - - const oldCounter = document.getElementById( - 'prpl-widget-content-ravi-points-number' - ); - if ( oldCounter ) { - oldCounter.textContent = newValue + 'pt'; - } - - // Mark badge as completed, in the a Monthly badges widgets, if we reached the max points. - if ( newValue >= parseInt( gaugeProps.max ) ) { - // We have multiple badges, one in widget and the other in the popover. - const badges = document.querySelectorAll( - '.prpl-badge-row-wrapper-inner .prpl-badge prpl-badge[complete="false"][badge-id="' + - gaugeProps.badgeId + - '"]' - ); - - if ( badges ) { - badges.forEach( ( badge ) => { - badge.setAttribute( 'complete', 'true' ); - } ); - } - } - }, - false -); - -// Listen for the event. -document.addEventListener( - 'prpl/suggestedTask/maybeInjectItem', - ( e ) => { - // TODO: Something seems off here, take a look at this. - const category = e.detail.category; - while ( - document.querySelectorAll( - `.prpl-suggested-task[data-task-category="${ category }"]` - ).length < - parseInt( - prplSuggestedTasks.maxItemsPerCategory[ category ] - ) && - prplSuggestedTasksGetNextPendingItemFromCategory( category ) - ) { - document.dispatchEvent( - new CustomEvent( 'prpl/suggestedTask/injectCategoryItem', { - detail: { category }, - } ) - ); - } - - window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); - }, - false -); +prplTerms.getCollectionsPromises().then( () => { + prplSuggestedTasksWidget.populateList(); + prplTodoWidget.populateList(); +} ); /* eslint-enable camelcase */ diff --git a/assets/js/widgets/todo.js b/assets/js/widgets/todo.js index 3d54c7565..e2d295118 100644 --- a/assets/js/widgets/todo.js +++ b/assets/js/widgets/todo.js @@ -1,135 +1,174 @@ -/* global progressPlannerTodo, customElements, prplDocumentReady */ +/* global prplSuggestedTask, prplTerms */ /* * Widget: Todo * * A widget that displays a todo list. * - * Dependencies: progress-planner/web-components/prpl-suggested-task, wp-util, wp-a11y, progress-planner/ajax-request, progress-planner/grid-masonry, progress-planner/document-ready, progress-planner/celebrate + * Dependencies: wp-api, progress-planner/suggested-task, wp-util, wp-a11y, progress-planner/grid-masonry, progress-planner/celebrate, progress-planner/suggested-task-terms */ -/** - * Get a random UUID. - * - * @return {string} The random UUID. - */ -const prplGetRandomUUID = () => { - if ( - typeof crypto !== 'undefined' && - typeof crypto.randomUUID === 'function' - ) { - return crypto.randomUUID(); - } - return ( - Math.random().toString( 36 ).substring( 2, 15 ) + - Math.random().toString( 36 ).substring( 2, 15 ) - ); -}; - -/** - * Get the highest `order` value from the todo items. - * - * @return {number} The highest `order` value. - */ -const prplGetHighestTodoItemOrder = () => { - const todoItems = document.querySelectorAll( - '#todo-list .prpl-suggested-task' - ); - let highestOrder = 0; - todoItems.forEach( ( todoItem ) => { - const order = parseInt( todoItem.getAttribute( 'data-task-order' ) ); - if ( order > highestOrder ) { - highestOrder = order; - } - } ); - return highestOrder; -}; - -document.addEventListener( 'prpl/todo/injectItem', ( event ) => { - const details = event.detail.item; - const addToStart = event.detail.addToStart; - const listId = event.detail.listId; +const prplTodoWidget = { + /** + * Get the highest `order` value from the todo items. + * + * @return {number} The highest `order` value. + */ + getHighestItemOrder: () => { + const items = document.querySelectorAll( + '#todo-list .prpl-suggested-task' + ); + let highestOrder = 0; + items.forEach( ( item ) => { + highestOrder = Math.max( + parseInt( item.getAttribute( 'data-task-order' ) ), + highestOrder + ); + } ); + return highestOrder; + }, - const Item = customElements.get( 'prpl-suggested-task' ); - const todoItemElement = new Item( { - ...details, - deletable: true, - taskList: 'progressPlannerTodo', - } ); + /** + * Remove the "Loading..." text and resize the grid items. + */ + removeLoadingItems: () => { + // Remove the "Loading..." text. + document.querySelector( '#prpl-todo-list-loading' )?.remove(); - if ( addToStart ) { - document.getElementById( listId ).prepend( todoItemElement ); - } else { - document.getElementById( listId ).appendChild( todoItemElement ); - } -} ); - -prplDocumentReady( () => { - // Inject the existing todo list items into the DOM - progressPlannerTodo.tasks.forEach( ( todoItem, index, array ) => { - document.dispatchEvent( - new CustomEvent( 'prpl/todo/injectItem', { - detail: { - item: todoItem, - addToStart: 1 === todoItem.points, // Add golden task to the start of the list. - listId: - todoItem.status === 'completed' - ? 'todo-list-completed' - : 'todo-list', - }, - } ) - ); + // Resize the grid items. + window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); + }, + + /** + * Populate the todo list. + */ + populateList: () => { + // If preloaded tasks are available, inject them. + if ( 'undefined' !== typeof prplSuggestedTask.tasks ) { + // Inject the tasks. + if ( Object.keys( prplSuggestedTask.tasks.userTasks ).length ) { + Object.values( prplSuggestedTask.tasks.userTasks ).forEach( + ( item ) => { + // Inject the items into the DOM. + document.dispatchEvent( + new CustomEvent( 'prpl/suggestedTask/injectItem', { + detail: { + item, + insertPosition: + 1 === item?.meta?.prpl_points + ? 'afterbegin' // Add golden task to the start of the list. + : 'beforeend', + listId: + item.status === 'publish' + ? 'todo-list' + : 'todo-list-completed', + }, + } ) + ); + prplSuggestedTask.injectedItemIds.push( item.id ); + } + ); + } + prplTodoWidget.removeLoadingItems(); + } else { + // Otherwise, inject tasks from the API. + prplSuggestedTask + .fetchItems( { + category: 'user', + status: [ 'publish', 'trash' ], + per_page: 100, + } ) + .then( ( data ) => { + if ( ! data.length ) { + return data; + } - // If this is the last item in the array, resize the grid items. - if ( index === array.length - 1 ) { - window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); + // Inject the items into the DOM. + data.forEach( ( item ) => { + document.dispatchEvent( + new CustomEvent( 'prpl/suggestedTask/injectItem', { + detail: { + item, + insertPosition: + 1 === item?.meta?.prpl_points + ? 'afterbegin' // Add golden task to the start of the list. + : 'beforeend', + listId: + item.status === 'publish' + ? 'todo-list' + : 'todo-list-completed', + }, + } ) + ); + prplSuggestedTask.injectedItemIds.push( item.id ); + } ); + + return data; + } ) + .then( () => prplTodoWidget.removeLoadingItems() ); } - } ); - // When the '#create-todo-item' form is submitted, - // add a new todo item to the list - document - .getElementById( 'create-todo-item' ) - .addEventListener( 'submit', ( event ) => { - event.preventDefault(); - const newTask = { - description: '', - parent: 0, - points: 0, - priority: 'medium', - task_id: 'user-task-' + prplGetRandomUUID(), - title: document.getElementById( 'new-todo-content' ).value, - provider_id: 'user', - category: 'user', - url: '', - dismissable: true, - snoozable: false, - order: prplGetHighestTodoItemOrder() + 1, - }; - - // Save the new task. - wp.ajax - .post( 'progress_planner_save_user_suggested_task', { - task: newTask, - nonce: progressPlannerTodo.nonce, - } ) - .then( ( response ) => { - if ( 'undefined' !== typeof response.points ) { - newTask.points = response.points; + // When the '#create-todo-item' form is submitted, + // add a new todo item to the list + document + .getElementById( 'create-todo-item' ) + .addEventListener( 'submit', ( event ) => { + event.preventDefault(); + + // Add the loader. + prplTodoWidget.addLoader(); + + // Create a new post + const post = new wp.api.models.Prpl_recommendations( { + // Set the post title. + title: document.getElementById( 'new-todo-content' ).value, + status: 'publish', + // Set the `prpl_recommendations_category` term. + prpl_recommendations_category: + prplTerms.get( 'category' ).user.id, + // Set the `prpl_recommendations_provider` term. + prpl_recommendations_provider: + prplTerms.get( 'provider' ).user.id, + menu_order: prplTodoWidget.getHighestItemOrder() + 1, + meta: { + prpl_snoozable: false, + prpl_dismissable: true, + }, + } ); + post.save().then( ( response ) => { + if ( ! response.id ) { + return; } + const newTask = { + ...response, + meta: { + prpl_points: 0, + prpl_snoozable: false, + prpl_dismissable: true, + prpl_url: '', + prpl_url_target: '_self', + ...( response.meta || {} ), + }, + provider: 'user', + category: 'user', + order: prplTodoWidget.getHighestItemOrder() + 1, + }; // Inject the new task into the DOM. document.dispatchEvent( - new CustomEvent( 'prpl/todo/injectItem', { + new CustomEvent( 'prpl/suggestedTask/injectItem', { detail: { item: newTask, - addToStart: 1 === newTask.points, // Add golden task to the start of the list. + insertPosition: + 1 === newTask.points + ? 'afterbegin' + : 'beforeend', // Add golden task to the start of the list. listId: 'todo-list', }, } ) ); - // Add the new task to the tasks array. - progressPlannerTodo.tasks.push( newTask ); + // Remove the loader. + prplTodoWidget.removeLoader(); // Resize the grid items. window.dispatchEvent( @@ -137,103 +176,64 @@ prplDocumentReady( () => { ); } ); - // Clear the new task input element. - document.getElementById( 'new-todo-content' ).value = ''; - - // Focus the new task input element. - document.getElementById( 'new-todo-content' ).focus(); - } ); -} ); + // Clear the new task input element. + document.getElementById( 'new-todo-content' ).value = ''; + + // Focus the new task input element. + document.getElementById( 'new-todo-content' ).focus(); + } ); + }, + + /** + * Add the loader. + */ + addLoader: () => { + const loader = document.createElement( 'span' ); + loader.className = 'prpl-loader'; + document.getElementById( 'todo-list' ).appendChild( loader ); + }, + + /** + * Remove the loader. + */ + removeLoader: () => { + document.querySelector( '#todo-list .prpl-loader' )?.remove(); + }, +}; -// When the 'prpl/suggestedTask/move' event is triggered, -// update the order of the todo items. -document.addEventListener( 'prpl/suggestedTask/move', () => { - const todoItemsIDs = []; - // Get all the todo items. - const todoItems = document.querySelectorAll( - '#todo-list .prpl-suggested-task' - ); - let order = 0; - todoItems.forEach( ( todoItem ) => { - todoItemsIDs.push( todoItem.getAttribute( 'data-task-id' ) ); - todoItem.setAttribute( 'data-task-order', order ); - progressPlannerTodo.tasks.find( - ( item ) => item.task_id === todoItem.getAttribute( 'data-task-id' ) - ).order = order; - order++; - } ); - wp.ajax.post( 'progress_planner_save_suggested_user_tasks_order', { - tasks: todoItemsIDs.toString(), - nonce: progressPlannerTodo.nonce, +document + .getElementById( 'todo-list-completed-details' ) + .addEventListener( 'toggle', () => { + window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); } ); -} ); -// When the 'prpl/suggestedTask/update' event is triggered, -// update the task title in the tasks array. -document.addEventListener( 'prpl/suggestedTask/update', ( event ) => { - const task = progressPlannerTodo.tasks.find( - ( item ) => - item.task_id === - event.detail.node - .querySelector( 'li' ) - .getAttribute( 'data-task-id' ) - ); - - if ( task ) { - task.title = event.detail.node.querySelector( 'h3 span' ).textContent; - } -} ); - -document.addEventListener( 'prpl/suggestedTask/maybeInjectItem', ( event ) => { - if ( - 'complete' !== event.detail.actionType && - 'pending' !== event.detail.actionType - ) { +document.addEventListener( 'prpl/suggestedTask/itemInjected', ( event ) => { + if ( 'todo-list' !== event.detail.listId ) { return; } - setTimeout( () => { - progressPlannerTodo.tasks.forEach( ( todoItem, index ) => { - if ( todoItem.task_id === event.detail.task_id ) { - // Change the status. - progressPlannerTodo.tasks[ index ].status = - 'complete' === event.detail.actionType - ? 'completed' - : 'pending'; - - // Inject the todo item into the DOM. - document.dispatchEvent( - new CustomEvent( 'prpl/todo/injectItem', { - detail: { - item: todoItem, - addToStart: 1 === todoItem.points, // Add golden task to the start of the list. - listId: - 'complete' === event.detail.actionType - ? 'todo-list-completed' - : 'todo-list', - }, - } ) - ); - - // Remove item from completed-todos list if necessary. - if ( 'pending' === event.detail.actionType ) { - const el = document.querySelector( - `#todo-list-completed .prpl-suggested-task[data-task-id="${ todoItem.task_id }"]` - ); - if ( el ) { - el.parentNode.remove(); - } - } + // Get all items in the list. + const items = document.querySelectorAll( + `#${ event.detail.listId } .prpl-suggested-task` + ); - // Resize the grid items. - window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); - } + // Reorder items based on their `data-task-order` attribute. + const orderedItems = Array.from( items ).sort( ( a, b ) => { + return ( + parseInt( a.getAttribute( 'data-task-order' ) ) - + parseInt( b.getAttribute( 'data-task-order' ) ) + ); } ); - }, 10 ); -} ); -document - .getElementById( 'todo-list-completed-details' ) - .addEventListener( 'toggle', () => { + // Remove all items from the list. + items.forEach( ( item ) => item.remove() ); + + // Inject the ordered items back into the list. + orderedItems.forEach( ( item ) => + document.getElementById( event.detail.listId ).appendChild( item ) + ); + + // Resize the grid items. window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); } ); +} ); diff --git a/autoload.php b/autoload.php index 388a2a463..1f2d3a451 100644 --- a/autoload.php +++ b/autoload.php @@ -5,7 +5,12 @@ * @package Progress_Planner */ -spl_autoload_register( +use Progress_Planner\Utils\Deprecations; + +// Require the Deprecations class. +require_once __DIR__ . '/classes/utils/class-deprecations.php'; + +\spl_autoload_register( /** * Autoload classes. * @@ -19,86 +24,17 @@ function ( $class_name ) { } // Deprecated classes. - $deprecated = [ - 'Progress_Planner\Activity' => [ 'Progress_Planner\Activities\Activity', '1.1.1' ], - 'Progress_Planner\Query' => [ 'Progress_Planner\Activities\Query', '1.1.1' ], - 'Progress_Planner\Date' => [ 'Progress_Planner\Utils\Date', '1.1.1' ], - 'Progress_Planner\Cache' => [ 'Progress_Planner\Utils\Cache', '1.1.1' ], - 'Progress_Planner\Widgets\Activity_Scores' => [ 'Progress_Planner\Admin\Widgets\Activity_Scores', '1.1.1' ], - 'Progress_Planner\Widgets\Badge_Streak' => [ 'Progress_Planner\Admin\Widgets\Badge_Streak', '1.1.1' ], - 'Progress_Planner\Widgets\Challenge' => [ 'Progress_Planner\Admin\Widgets\Challenge', '1.1.1' ], - 'Progress_Planner\Widgets\Latest_Badge' => [ 'Progress_Planner\Admin\Widgets\Latest_Badge', '1.1.1' ], - 'Progress_Planner\Widgets\Published_Content' => [ 'Progress_Planner\Admin\Widgets\Published_Content', '1.1.1' ], - 'Progress_Planner\Widgets\Todo' => [ 'Progress_Planner\Admin\Widgets\Todo', '1.1.1' ], - 'Progress_Planner\Widgets\Whats_New' => [ 'Progress_Planner\Admin\Widgets\Whats_New', '1.1.1' ], - 'Progress_Planner\Widgets\Widget' => [ 'Progress_Planner\Admin\Widgets\Widget', '1.1.1' ], - 'Progress_Planner\Rest_API_Stats' => [ 'Progress_Planner\Rest\Stats', '1.1.1' ], - 'Progress_Planner\Rest_API_Tasks' => [ 'Progress_Planner\Rest\Tasks', '1.1.1' ], - 'Progress_Planner\Data_Collector\Base_Data_Collector' => [ 'Progress_Planner\Suggested_Tasks\Data_Collector\Base_Data_Collector', '1.1.1' ], - 'Progress_Planner\Data_Collector\Data_Collector_Manager' => [ 'Progress_Planner\Suggested_Tasks\Data_Collector\Data_Collector_Manager', '1.1.1' ], - 'Progress_Planner\Data_Collector\Hello_World' => [ 'Progress_Planner\Suggested_Tasks\Data_Collector\Hello_World', '1.1.1' ], - 'Progress_Planner\Data_Collector\Inactive_Plugins' => [ 'Progress_Planner\Suggested_Tasks\Data_Collector\Inactive_Plugins', '1.1.1' ], - 'Progress_Planner\Data_Collector\Last_Published_Post' => [ 'Progress_Planner\Suggested_Tasks\Data_Collector\Last_Published_Post', '1.1.1' ], - 'Progress_Planner\Data_Collector\Post_Author' => [ 'Progress_Planner\Suggested_Tasks\Data_Collector\Post_Author', '1.1.1' ], - 'Progress_Planner\Data_Collector\Sample_Page' => [ 'Progress_Planner\Suggested_Tasks\Data_Collector\Sample_Page', '1.1.1' ], - 'Progress_Planner\Data_Collector\Uncategorized_Category' => [ 'Progress_Planner\Suggested_Tasks\Data_Collector\Uncategorized_Category', '1.1.1' ], - 'Progress_Planner\Chart' => [ 'Progress_Planner\UI\Chart', '1.1.1' ], - 'Progress_Planner\Popover' => [ 'Progress_Planner\UI\Popover', '1.1.1' ], - 'Progress_Planner\Debug_Tools' => [ 'Progress_Planner\Utils\Debug_Tools', '1.1.1' ], - 'Progress_Planner\Onboard' => [ 'Progress_Planner\Utils\Onboard', '1.1.1' ], - 'Progress_Planner\Playground' => [ 'Progress_Planner\Utils\Playground', '1.1.1' ], - - 'Progress_Planner\Admin\Widgets\Published_Content' => [ 'Progress_Planner\Admin\Widgets\Content_Activity', '1.3.0' ], - - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Task_Local' => [ 'Progress_Planner\Suggested_Tasks\Task', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Local_Tasks_Interface' => [ 'Progress_Planner\Suggested_Tasks\Tasks_Interface', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks_Manager' => [ 'Progress_Planner\Suggested_Tasks\Tasks_Manager', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Local_Task_Factory' => [ 'Progress_Planner\Suggested_Tasks\Task_Factory', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time' => [ 'Progress_Planner\Suggested_Tasks\Providers\Task', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Repetitive' => [ 'Progress_Planner\Suggested_Tasks\Providers\Repetitive', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Local_Tasks' => [ 'Progress_Planner\Suggested_Tasks\Providers\Tasks', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\User' => [ 'Progress_Planner\Suggested_Tasks\Providers\User', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Add_Yoast_Providers' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Add_Yoast_Providers', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Archive_Author' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Archive_Author', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Archive_Date' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Archive_Date', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Archive_Format' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Archive_Format', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Crawl_Settings_Emoji_Scripts' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Crawl_Settings_Emoji_Scripts', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Crawl_Settings_Feed_Authors' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Crawl_Settings_Feed_Authors', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Crawl_Settings_Feed_Global_Comments' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Crawl_Settings_Feed_Global_Comments', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Media_Pages' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Media_Pages', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Organization_Logo' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Organization_Logo', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Yoast_Provider' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Yoast_Provider', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Blog_Description' => [ 'Progress_Planner\Suggested_Tasks\Providers\Blog_Description', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Debug_Display' => [ 'Progress_Planner\Suggested_Tasks\Providers\Debug_Display', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Disable_Comments' => [ 'Progress_Planner\Suggested_Tasks\Providers\Disable_Comments', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Hello_World' => [ 'Progress_Planner\Suggested_Tasks\Providers\Hello_World', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Permalink_Structure' => [ 'Progress_Planner\Suggested_Tasks\Providers\Permalink_Structure', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Php_Version' => [ 'Progress_Planner\Suggested_Tasks\Providers\Php_Version', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Remove_Inactive_Plugins' => [ 'Progress_Planner\Suggested_Tasks\Providers\Remove_Inactive_Plugins', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Rename_Uncategorized_Category' => [ 'Progress_Planner\Suggested_Tasks\Providers\Rename_Uncategorized_Category', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Sample_Page' => [ 'Progress_Planner\Suggested_Tasks\Providers\Sample_Page', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Search_Engine_Visibility' => [ 'Progress_Planner\Suggested_Tasks\Providers\Search_Engine_Visibility', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Set_Valuable_Post_Types' => [ 'Progress_Planner\Suggested_Tasks\Providers\Set_Valuable_Post_Types', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Settings_Saved' => [ 'Progress_Planner\Suggested_Tasks\Providers\Settings_Saved', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Site_Icon' => [ 'Progress_Planner\Suggested_Tasks\Providers\Site_Icon', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Repetitive\Core_Update' => [ 'Progress_Planner\Suggested_Tasks\Providers\Core_Update', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Repetitive\Create' => [ 'Progress_Planner\Suggested_Tasks\Providers\Repetitive\Create', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Repetitive\Review' => [ 'Progress_Planner\Suggested_Tasks\Providers\Repetitive\Review', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Remote_Tasks\Remote_Task_Factory' => [ 'Progress_Planner\Suggested_Tasks\Task_Factory', '1.4.0' ], - 'Progress_Planner\Suggested_Tasks\Remote_Tasks\Remote_Task' => [ 'Progress_Planner\Suggested_Tasks\Task', '1.4.0' ], - ]; - - if ( isset( $deprecated[ $class_name ] ) ) { + if ( isset( Deprecations::CLASSES[ $class_name ] ) ) { \trigger_error( // phpcs:ignore - sprintf( + \sprintf( 'Class %1$s is deprecated since version %2$s! Use %3$s instead.', \esc_html( $class_name ), - \esc_html( $deprecated[ $class_name ][1] ), - \esc_html( $deprecated[ $class_name ][0] ) + \esc_html( Deprecations::CLASSES[ $class_name ][1] ), + \esc_html( Deprecations::CLASSES[ $class_name ][0] ) ), E_USER_DEPRECATED ); - class_alias( $deprecated[ $class_name ][0], $class_name ); + \class_alias( Deprecations::CLASSES[ $class_name ][0], $class_name ); } $class_name = \str_replace( $prefix, '', $class_name ); @@ -108,7 +44,7 @@ class_alias( $deprecated[ $class_name ][0], $class_name ); $last = \array_pop( $parts ); foreach ( $parts as $part ) { - $file .= str_replace( '_', '-', strtolower( $part ) ) . '/'; + $file .= \str_replace( '_', '-', \strtolower( $part ) ) . '/'; } $file .= 'class-' . \str_replace( '_', '-', \strtolower( $last ) ) . '.php'; diff --git a/classes/actions/class-content-scan.php b/classes/actions/class-content-scan.php index c647796c2..97b93f5e6 100644 --- a/classes/actions/class-content-scan.php +++ b/classes/actions/class-content-scan.php @@ -138,7 +138,7 @@ public function update_stats() { return [ 'lastScannedPage' => $current_page, 'lastPage' => $total_pages, - 'progress' => round( ( $current_page / max( 1, $total_pages ) ) * 100 ), + 'progress' => \round( ( $current_page / \max( 1, $total_pages ) ) * 100 ), ]; } diff --git a/classes/actions/class-content.php b/classes/actions/class-content.php index ea6944bcb..5bff37303 100644 --- a/classes/actions/class-content.php +++ b/classes/actions/class-content.php @@ -27,7 +27,6 @@ public function __construct() { * @return void */ public function register_hooks() { - // Add activity when a post is added or updated. \add_action( 'wp_insert_post', [ $this, 'insert_post' ], 10, 3 ); \add_action( 'transition_post_status', [ $this, 'transition_post_status' ], 10, 3 ); @@ -226,7 +225,6 @@ private function is_there_recent_activity( $post, $type ) { * @return void */ private function add_post_activity( $post, $type ) { - // Post was updated to publish for the first time, ie draft was published. if ( 'update' === $type && 'publish' === $post->post_status ) { // Check if there is a publish activity for this post. @@ -248,7 +246,6 @@ private function add_post_activity( $post, $type ) { // Post was updated, but it was published previously. if ( 'update' === $type ) { - // Check if there are any activities for this post, on this date. $existing = \progress_planner()->get_activities__query()->query_activities( [ @@ -270,7 +267,6 @@ private function add_post_activity( $post, $type ) { // Update the badges. if ( 'publish' === $type ) { - // Check if there is a publish activity for this post. $existing = \progress_planner()->get_activities__query()->query_activities( [ diff --git a/classes/activities/class-activity.php b/classes/activities/class-activity.php index 5baca13de..f3acf7140 100644 --- a/classes/activities/class-activity.php +++ b/classes/activities/class-activity.php @@ -114,7 +114,7 @@ public function get_points( $date ) { if ( isset( $this->points[ $date_ymd ] ) ) { return $this->points[ $date_ymd ]; } - $days = abs( \progress_planner()->get_utils__date()->get_days_between_dates( $date, $this->date ) ); + $days = \abs( \progress_planner()->get_utils__date()->get_days_between_dates( $date, $this->date ) ); // Default points. $default_points = 10; @@ -128,7 +128,7 @@ public function get_points( $date ) { $this->points[ $date_ymd ] = ( $days < 7 ) ? $default_points - : round( $default_points * max( 0, ( 1 - $days / 30 ) ) ); + : \round( $default_points * \max( 0, ( 1 - $days / 30 ) ) ); return $this->points[ $date_ymd ]; } diff --git a/classes/activities/class-content.php b/classes/activities/class-content.php index 5caf926ec..1fd4fee3b 100644 --- a/classes/activities/class-content.php +++ b/classes/activities/class-content.php @@ -19,7 +19,6 @@ class Content extends Activity { */ public $category = 'content'; - /** * Points configuration for content activities. * @@ -54,7 +53,7 @@ public function get_points( $date ) { } // Get the number of days between the activity date and the given date. - $days = absint( \progress_planner()->get_utils__date()->get_days_between_dates( $date, $this->date ) ); + $days = \absint( \progress_planner()->get_utils__date()->get_days_between_dates( $date, $this->date ) ); // Maximum range for awarded points is 30 days. if ( $days >= 30 ) { @@ -72,8 +71,8 @@ public function get_points( $date ) { // Calculate the points based on the age of the activity. $this->points[ $date_ymd ] = ( $days < 7 ) - ? round( $this->points[ $date_ymd ] ) // If the activity is new (less than 7 days old), award full points. - : round( $this->points[ $date_ymd ] * max( 0, ( 1 - $days / 30 ) ) ); // Decay the points based on the age of the activity. + ? \round( $this->points[ $date_ymd ] ) // If the activity is new (less than 7 days old), award full points. + : \round( $this->points[ $date_ymd ] * \max( 0, ( 1 - $days / 30 ) ) ); // Decay the points based on the age of the activity. return (int) $this->points[ $date_ymd ]; } diff --git a/classes/activities/class-maintenance.php b/classes/activities/class-maintenance.php index 94db9c98f..ccacc1303 100644 --- a/classes/activities/class-maintenance.php +++ b/classes/activities/class-maintenance.php @@ -74,7 +74,7 @@ public function get_points( $date ) { return $this->points[ $date_ymd ]; } $this->points[ $date_ymd ] = self::$points_config; - $days = abs( \progress_planner()->get_utils__date()->get_days_between_dates( $date, $this->date ) ); + $days = \abs( \progress_planner()->get_utils__date()->get_days_between_dates( $date, $this->date ) ); $this->points[ $date_ymd ] = ( $days < 7 ) ? $this->points[ $date_ymd ] : 0; diff --git a/classes/activities/class-query.php b/classes/activities/class-query.php index 57ad502db..f016bf52f 100644 --- a/classes/activities/class-query.php +++ b/classes/activities/class-query.php @@ -104,7 +104,7 @@ public function query_activities( $args, $return_type = 'ACTIVITIES' ) { $args = \wp_parse_args( $args, $defaults ); - $cache_key = 'progress-planner-activities-' . md5( (string) \wp_json_encode( $args ) ); + $cache_key = 'progress-planner-activities-' . \md5( (string) \wp_json_encode( $args ) ); $results = \wp_cache_get( $cache_key, static::CACHE_GROUP ); if ( false === $results ) { @@ -153,11 +153,11 @@ public function query_activities( $args, $return_type = 'ACTIVITIES' ) { : $wpdb->get_results( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- This is a false positive. $wpdb->prepare( - sprintf( + \sprintf( 'SELECT * FROM %%i WHERE %s', \implode( ' AND ', $where_args ) ), - array_merge( + \array_merge( [ $wpdb->prefix . static::TABLE_NAME ], $prepare_args ) @@ -182,7 +182,7 @@ public function query_activities( $args, $return_type = 'ACTIVITIES' ) { } $results_unique[ $result->category . $result->type . $result->data_id . $result->date ] = $result; } - $results = array_values( $results_unique ); + $results = \array_values( $results_unique ); return 'RAW' === $return_type ? $results @@ -211,7 +211,6 @@ public function insert_activities( $activities ) { return $ids; } - /** * Insert an activity into the database. * @@ -439,8 +438,8 @@ public function get_oldest_activity() { * @return string The class name of the Activity. */ protected function get_activity_class_name( $category ) { - if ( class_exists( '\Progress_Planner\Activities\\' . ucfirst( $category ) ) ) { - return '\Progress_Planner\Activities\\' . ucfirst( $category ); + if ( \class_exists( '\Progress_Planner\Activities\\' . \ucfirst( $category ) ) ) { + return '\Progress_Planner\Activities\\' . \ucfirst( $category ); } return '\Progress_Planner\Activities\Activity'; } diff --git a/classes/activities/class-suggested-task.php b/classes/activities/class-suggested-task.php index ca1294455..565995e45 100644 --- a/classes/activities/class-suggested-task.php +++ b/classes/activities/class-suggested-task.php @@ -7,8 +7,6 @@ namespace Progress_Planner\Activities; -use Progress_Planner\Suggested_Tasks\Providers\Content_Create; - /** * Handler for suggested tasks activities. */ @@ -66,19 +64,10 @@ public function get_points( $date ) { // Default points for a suggested task. $points = 1; - $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $this->data_id ); - - if ( ! empty( $tasks ) && isset( $tasks[0]['provider_id'] ) ) { - if ( 'user' === $tasks[0]['provider_id'] ) { - $points = isset( $tasks[0]['points'] ) ? (int) $tasks[0]['points'] : 0; - } else { - $task_provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( $tasks[0]['provider_id'] ); + $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $this->data_id ] ); - if ( $task_provider ) { - // Create post task provider had a different points system, this is for backwards compatibility. - $points = $task_provider instanceof Content_Create ? $task_provider->get_points( $this->data_id ) : $task_provider->get_points(); - } - } + if ( ! empty( $tasks ) ) { + $points = $tasks[0]->points; } $this->points[ $date_ymd ] = $points; diff --git a/classes/admin/class-dashboard-widget-todo.php b/classes/admin/class-dashboard-widget-todo.php index 7f9916c0b..7f8c08c76 100644 --- a/classes/admin/class-dashboard-widget-todo.php +++ b/classes/admin/class-dashboard-widget-todo.php @@ -45,6 +45,8 @@ public function render_widget() { } \progress_planner()->the_view( "dashboard-widgets/{$this->id}.php" ); + + \progress_planner()->the_view( 'js-templates/suggested-task.html' ); } } // phpcs:enable Generic.Commenting.Todo diff --git a/classes/admin/class-editor.php b/classes/admin/class-editor.php index 2ec2058e0..ee6f490a8 100644 --- a/classes/admin/class-editor.php +++ b/classes/admin/class-editor.php @@ -35,7 +35,7 @@ public function enqueue_editor_script() { // Check if the page-type is set in the URL (user is coming from the Settings page). if ( isset( $_GET['prpl_page_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $prpl_pt = sanitize_text_field( wp_unslash( $_GET['prpl_page_type'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $prpl_pt = \sanitize_text_field( \wp_unslash( $_GET['prpl_page_type'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended foreach ( $page_types as $page_type ) { if ( $page_type['slug'] === $prpl_pt ) { diff --git a/classes/admin/class-enqueue.php b/classes/admin/class-enqueue.php index 0ba45627d..392766eee 100644 --- a/classes/admin/class-enqueue.php +++ b/classes/admin/class-enqueue.php @@ -71,7 +71,7 @@ public function enqueue_script( $handle, $localize_data = [] ) { // Enqueue the script dependencies. foreach ( $file_details['dependencies'] as $dependency ) { - if ( ! in_array( $dependency, $this->enqueued_assets['js'], true ) ) { + if ( ! \in_array( $dependency, $this->enqueued_assets['js'], true ) ) { $this->enqueue_script( $dependency ); $final_dependencies[] = $dependency; } @@ -102,7 +102,7 @@ public function enqueue_style( $handle ) { // Enqueue the script dependencies. foreach ( $file_details['dependencies'] as $dependency ) { - if ( ! in_array( $dependency, $this->enqueued_assets['css'], true ) ) { + if ( ! \in_array( $dependency, $this->enqueued_assets['css'], true ) ) { $this->enqueue_style( $dependency ); } } @@ -119,8 +119,8 @@ public function enqueue_style( $handle ) { * @return array */ public function get_file_details( $context, $handle ) { - if ( str_starts_with( $handle, 'progress-planner/' ) ) { - $handle = str_replace( 'progress-planner/', '', $handle ); + if ( \str_starts_with( $handle, 'progress-planner/' ) ) { + $handle = \str_replace( 'progress-planner/', '', $handle ); } if ( 'js' === $context ) { @@ -132,7 +132,7 @@ public function get_file_details( $context, $handle ) { } } // The file path. - $file_path = constant( 'PROGRESS_PLANNER_DIR' ) . "/assets/{$context}/{$handle}.{$context}"; + $file_path = \constant( 'PROGRESS_PLANNER_DIR' ) . "/assets/{$context}/{$handle}.{$context}"; // If the file does not exist, bail early. if ( ! \file_exists( $file_path ) ) { @@ -140,7 +140,7 @@ public function get_file_details( $context, $handle ) { } // The file URL. - $file_url = constant( 'PROGRESS_PLANNER_URL' ) . "/assets/{$context}/{$handle}.{$context}"; + $file_url = \constant( 'PROGRESS_PLANNER_URL' ) . "/assets/{$context}/{$handle}.{$context}"; // The handle. $handle = 'js' === $context && isset( self::VENDOR_SCRIPTS[ $handle ] ) @@ -197,15 +197,53 @@ public function localize_script( $handle, $localize_data = [] ) { ]; break; - case 'progress-planner/web-components/prpl-suggested-task': + case 'progress-planner/suggested-task': + // Celebrate only on the Progress Planner Dashboard page. + $delay_celebration = true; + if ( \progress_planner()->is_on_progress_planner_dashboard_page() ) { + // should_show_upgrade_popover() also checks if we're on the Progress Planner Dashboard page - but let's be explicit since that method might change in the future. + $delay_celebration = \progress_planner()->get_plugin_upgrade_tasks()->should_show_upgrade_popover(); + } + + // Get tasks from task providers. + $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_in_rest_format( + [ + 'post_status' => 'publish', + 'exclude_provider' => [ 'user' ], + ] + ); + // Get pending celebration tasks. + $pending_celebration_tasks = \progress_planner()->get_suggested_tasks()->get_tasks_in_rest_format( + [ + 'post_status' => 'pending', + 'posts_per_page' => 100, + 'exclude_provider' => [ 'user' ], + ] + ); + + // Get user tasks. + $user_tasks = \progress_planner()->get_suggested_tasks()->get_tasks_in_rest_format( + [ + 'post_status' => [ 'publish', 'trash' ], + 'include_provider' => [ 'user' ], + ] + ); + $localize_data = [ 'name' => 'prplSuggestedTask', 'data' => [ - 'nonce' => \wp_create_nonce( 'progress_planner' ), - 'assets' => [ - 'infoIcon' => constant( 'PROGRESS_PLANNER_URL' ) . '/assets/images/icon_info.svg', - 'snoozeIcon' => constant( 'PROGRESS_PLANNER_URL' ) . '/assets/images/icon_snooze.svg', + 'nonce' => \wp_create_nonce( 'progress_planner' ), + 'assets' => [ + 'infoIcon' => \constant( 'PROGRESS_PLANNER_URL' ) . '/assets/images/icon_info.svg', + 'snoozeIcon' => \constant( 'PROGRESS_PLANNER_URL' ) . '/assets/images/icon_snooze.svg', + ], + 'tasks' => [ + 'pendingTasks' => $tasks, + 'pendingCelebrationTasks' => $pending_celebration_tasks, + 'userTasks' => isset( $user_tasks['user'] ) ? $user_tasks['user'] : [], ], + 'maxItemsPerCategory' => \progress_planner()->get_suggested_tasks()->get_max_items_per_category(), + 'delayCelebration' => $delay_celebration, ], ]; break; @@ -237,7 +275,7 @@ public function localize_script( $handle, $localize_data = [] ) { $localize_data = [ 'name' => 'prplCelebrate', 'data' => [ - 'raviIconUrl' => constant( 'PROGRESS_PLANNER_URL' ) . '/assets/images/icon_progress_planner.svg', + 'raviIconUrl' => \constant( 'PROGRESS_PLANNER_URL' ) . '/assets/images/icon_progress_planner.svg', 'confettiOptions' => $confetti_options, ], ]; @@ -295,7 +333,7 @@ public function get_localized_strings() { // Strings alphabetically ordered. return [ 'badge' => \esc_html__( 'Badge', 'progress-planner' ), - 'checklistProgressDescription' => sprintf( + 'checklistProgressDescription' => \sprintf( /* translators: %s: the checkmark icon. */ \esc_html__( 'Check off all required elements %s in the element checks below', 'progress-planner' ), '' @@ -309,7 +347,7 @@ public function get_localized_strings() { 'prevBtnText' => \esc_html__( '← Previous', 'progress-planner' ), 'pageType' => \esc_html__( 'Page type', 'progress-planner' ), 'progressPlannerSidebar' => \esc_html__( 'Progress Planner Sidebar', 'progress-planner' ), - 'progressText' => sprintf( + 'progressText' => \sprintf( /* translators: %1$s: The current step number. %2$s: The total number of steps. */ \esc_html__( 'Step %1$s of %2$s', 'progress-planner' ), '{{current}}', diff --git a/classes/admin/class-page-settings.php b/classes/admin/class-page-settings.php index 07b6d69a2..d7e05c79b 100644 --- a/classes/admin/class-page-settings.php +++ b/classes/admin/class-page-settings.php @@ -81,14 +81,14 @@ public function get_settings() { $settings[ $page_type['slug'] ]['isset'] = 'yes'; // If there is more than one page, we need to check if the page has a parent with the same page-type assigned. - if ( 1 < count( $type_pages ) ) { + if ( 1 < \count( $type_pages ) ) { $type_pages_ids = []; foreach ( $type_pages as $type_page ) { $type_pages_ids[] = (int) $type_page->ID; } foreach ( $type_pages as $type_page ) { $parent = \get_post_field( 'post_parent', $type_page->ID ); - if ( $parent && in_array( (int) $parent, $type_pages_ids, true ) ) { + if ( $parent && \in_array( (int) $parent, $type_pages_ids, true ) ) { $settings[ $page_type['slug'] ]['value'] = $parent; break; } @@ -132,7 +132,7 @@ public function store_settings_form_options() { \check_admin_referer( 'progress_planner' ); if ( isset( $_POST['pages'] ) ) { - foreach ( wp_unslash( $_POST['pages'] ) as $type => $page_args ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + foreach ( \wp_unslash( $_POST['pages'] ) as $type => $page_args ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $need_page = \sanitize_text_field( \wp_unslash( $page_args['have_page'] ) ); @@ -185,7 +185,6 @@ public function store_settings_form_options() { * @return void */ public function save_settings() { - // Check the nonce. \check_admin_referer( 'progress_planner' ); @@ -202,14 +201,13 @@ public function save_settings() { * @return void */ public function save_post_types() { - // Check the nonce. \check_admin_referer( 'progress_planner' ); $include_post_types = isset( $_POST['prpl-post-types-include'] ) - ? array_map( 'sanitize_text_field', \wp_unslash( $_POST['prpl-post-types-include'] ) ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + ? \array_map( 'sanitize_text_field', \wp_unslash( $_POST['prpl-post-types-include'] ) ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized // If no post types are selected, use the default post types (post and page can be deregistered). - : array_intersect( [ 'post', 'page' ], \progress_planner()->get_settings()->get_public_post_types() ); + : \array_intersect( [ 'post', 'page' ], \progress_planner()->get_settings()->get_public_post_types() ); \progress_planner()->get_settings()->set( 'include_post_types', $include_post_types ); } @@ -220,7 +218,6 @@ public function save_post_types() { * @return void */ public function save_license() { - // Check the nonce. \check_admin_referer( 'progress_planner' ); @@ -253,9 +250,9 @@ public function save_license() { 'edd_action' => 'activate_license', 'license' => $license, 'item_id' => 1136, - 'item_name' => rawurlencode( 'Progress Planner Pro' ), + 'item_name' => \rawurlencode( 'Progress Planner Pro' ), 'url' => \home_url(), - 'environment' => function_exists( 'wp_get_environment_type' ) ? \wp_get_environment_type() : 'production', + 'environment' => \function_exists( 'wp_get_environment_type' ) ? \wp_get_environment_type() : 'production', ], ] ); @@ -304,7 +301,7 @@ public function save_license() { case 'item_name_mismatch': \wp_send_json_error( - sprintf( + \sprintf( /* translators: the plugin name */ \esc_html__( 'This appears to be an invalid license key for %s.', 'progress-planner' ), 'Progress Planner Pro' diff --git a/classes/admin/class-page.php b/classes/admin/class-page.php index e90614a82..184ff4b57 100644 --- a/classes/admin/class-page.php +++ b/classes/admin/class-page.php @@ -110,18 +110,16 @@ public function add_page() { * @return string The notification count in HTML format. */ protected function get_notification_counter() { - - $pending_celebration_tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'status', 'pending_celebration' ); - $notification_count = count( $pending_celebration_tasks ); + $notification_count = \wp_count_posts( 'prpl_recommendations' )->pending; if ( 0 === $notification_count ) { return ''; } /* translators: Hidden accessibility text; %s: number of notifications. */ - $notifications = sprintf( _n( '%s pending celebration', '%s pending celebrations', $notification_count, 'progress-planner' ), number_format_i18n( $notification_count ) ); + $notifications = \sprintf( \_n( '%s pending celebration', '%s pending celebrations', $notification_count, 'progress-planner' ), \number_format_i18n( $notification_count ) ); - return sprintf( '%2$s', $notification_count, $notifications ); + return \sprintf( '%2$s', $notification_count, $notifications ); } /** @@ -162,7 +160,6 @@ public function enqueue_scripts() { } if ( 'toplevel_page_progress-planner' === $current_screen->id ) { - $default_localization_data = [ 'name' => 'progressPlanner', 'data' => [ @@ -175,6 +172,7 @@ public function enqueue_scripts() { if ( true === \progress_planner()->is_privacy_policy_accepted() ) { \progress_planner()->get_admin__enqueue()->enqueue_script( 'web-components/prpl-gauge' ); + \progress_planner()->get_admin__enqueue()->enqueue_script( 'web-components/prpl-badge-progress-bar' ); \progress_planner()->get_admin__enqueue()->enqueue_script( 'web-components/prpl-chart-bar' ); \progress_planner()->get_admin__enqueue()->enqueue_script( 'web-components/prpl-chart-line' ); \progress_planner()->get_admin__enqueue()->enqueue_script( 'web-components/prpl-big-counter' ); @@ -213,8 +211,7 @@ public function enqueue_scripts() { * @return void */ public function maybe_enqueue_focus_el_script( $hook ) { - $suggested_tasks = \progress_planner()->get_suggested_tasks(); - $tasks_providers = $suggested_tasks->get_tasks_manager()->get_task_providers(); + $tasks_providers = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_providers(); $tasks_details = []; $total_points = 0; $completed_points = 0; @@ -222,15 +219,23 @@ public function maybe_enqueue_focus_el_script( $hook ) { if ( 'configuration' !== $provider->get_provider_category() ) { continue; } - $details = $provider->get_task_details(); - if ( ! isset( $details['link_setting']['hook'] ) || - $hook !== $details['link_setting']['hook'] + + $link_setting = $provider->get_link_setting(); + if ( ! isset( $link_setting['hook'] ) || + $hook !== $link_setting['hook'] ) { continue; } - $details['is_complete'] = $provider->is_task_completed(); - $tasks_details[] = $details; - $total_points += $details['points']; + + $details = [ + 'link_setting' => $link_setting, + 'task_id' => $provider->get_task_id(), + 'points' => $provider->get_points(), + 'is_complete' => $provider->is_task_completed(), + ]; + + $tasks_details[] = $details; + $total_points += $details['points']; if ( $details['is_complete'] ) { $completed_points += $details['points']; } @@ -249,7 +254,7 @@ public function maybe_enqueue_focus_el_script( $hook ) { 'tasks' => $tasks_details, 'totalPoints' => $total_points, 'completedPoints' => $completed_points, - 'base_url' => constant( 'PROGRESS_PLANNER_URL' ), + 'base_url' => \constant( 'PROGRESS_PLANNER_URL' ), 'l10n' => [ /* translators: %d: The number of points. */ 'fixThisIssue' => \esc_html__( 'Fix this issue to get %d point(s) in Progress Planner', 'progress-planner' ), @@ -315,6 +320,7 @@ public function remove_admin_notices() { } \remove_all_actions( 'admin_notices' ); + \remove_all_actions( 'all_admin_notices' ); } /** diff --git a/classes/admin/widgets/class-activity-scores.php b/classes/admin/widgets/class-activity-scores.php index 2563bd9de..a0fd69361 100644 --- a/classes/admin/widgets/class-activity-scores.php +++ b/classes/admin/widgets/class-activity-scores.php @@ -82,14 +82,14 @@ public function get_score() { foreach ( $activities as $activity ) { $score += $activity->get_points( $current_date ); } - $score = min( 100, max( 0, $score ) ); + $score = \min( 100, \max( 0, $score ) ); // Get the number of pending updates. $pending_updates = \wp_get_update_data()['counts']['total']; // Reduce points for pending updates. - $score -= min( min( $score / 2, 25 ), $pending_updates * 5 ); - return (int) floor( $score ); + $score -= \min( \min( $score / 2, 25 ), $pending_updates * 5 ); + return (int) \floor( $score ); } /** @@ -123,7 +123,7 @@ public function get_checklist() { 'type' => 'publish', ] ); - return count( $events ) > 0; + return \count( $events ) > 0; }, ], [ @@ -136,7 +136,7 @@ public function get_checklist() { 'type' => 'update', ] ); - return count( $events ) > 0; + return \count( $events ) > 0; }, ], [ @@ -205,7 +205,7 @@ public function personal_record_callback() { ); // Cache the activities. - $cached_activities[ $weekly_cache_key ] = (bool) count( $activities ); + $cached_activities[ $weekly_cache_key ] = (bool) \count( $activities ); \progress_planner()->get_settings()->set( $this->cache_key, $cached_activities ); // Return the cached value. diff --git a/classes/admin/widgets/class-challenge.php b/classes/admin/widgets/class-challenge.php index 90b79458d..cf5fd0237 100644 --- a/classes/admin/widgets/class-challenge.php +++ b/classes/admin/widgets/class-challenge.php @@ -39,25 +39,25 @@ public function get_challenge( $force_free = false ) { } // Transient expired, fetch new feed. - if ( $feed_data['expires'] < time() ) { + if ( $feed_data['expires'] < \time() ) { // Get the feed using the REST API. $response = \wp_remote_get( $this->get_remote_api_url( $force_free ) ); - if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { + if ( 200 !== \wp_remote_retrieve_response_code( $response ) ) { // Fallback to free response if PRO but the license is invalid. if ( ! $force_free && \progress_planner()->is_pro_site() ) { return $this->get_challenge( true ); } // If we cant fetch the feed, we will try again later. - $feed_data['expires'] = time() + 5 * MINUTE_IN_SECONDS; + $feed_data['expires'] = \time() + 5 * MINUTE_IN_SECONDS; } else { - $feed = json_decode( \wp_remote_retrieve_body( $response ), true ); + $feed = \json_decode( \wp_remote_retrieve_body( $response ), true ); $feed_data['feed'] = $feed; - $feed_data['expires'] = time() + 1 * DAY_IN_SECONDS; + $feed_data['expires'] = \time() + 1 * DAY_IN_SECONDS; if ( empty( $feed ) ) { - $feed_data['expires'] = time() + 1 * HOUR_IN_SECONDS; + $feed_data['expires'] = \time() + 1 * HOUR_IN_SECONDS; } } @@ -88,7 +88,7 @@ public function render() { * @return string */ public function get_cache_key( $force_free = false ) { - return md5( $this->get_remote_api_url( $force_free ) ); + return \md5( $this->get_remote_api_url( $force_free ) ); } /** diff --git a/classes/admin/widgets/class-content-activity.php b/classes/admin/widgets/class-content-activity.php index 9f15b04cb..0a6f45073 100644 --- a/classes/admin/widgets/class-content-activity.php +++ b/classes/admin/widgets/class-content-activity.php @@ -28,11 +28,11 @@ final class Content_Activity extends Widget { * @return array The chart args. */ public function get_chart_args_content_count( $type = 'publish', $color = '#534786' ) { - return array_merge( + return \array_merge( $this->get_chart_args( $type, $color ), [ 'count_callback' => function ( $activities, $date = null ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed - return count( $activities ); + return \count( $activities ); }, ] ); @@ -80,11 +80,11 @@ public function get_chart_args( $type = 'publish', $color = '#534786' ) { * @return \Progress_Planner\Activities\Content[] */ public function filter_activities( $activities ) { - return array_filter( + return \array_filter( $activities, function ( $activity ) { $post = $activity->get_post(); - return 'delete' === $activity->type || ( is_object( $post ) + return 'delete' === $activity->type || ( \is_object( $post ) && \in_array( $post->post_type, \progress_planner()->get_activities__content_helpers()->get_post_types_names(), true ) ); } ); diff --git a/classes/admin/widgets/class-suggested-tasks.php b/classes/admin/widgets/class-suggested-tasks.php index 31577bb0a..c1b944b5b 100644 --- a/classes/admin/widgets/class-suggested-tasks.php +++ b/classes/admin/widgets/class-suggested-tasks.php @@ -8,8 +8,6 @@ namespace Progress_Planner\Admin\Widgets; use Progress_Planner\Badges\Monthly; -use Progress_Planner\Suggested_Tasks\Task_Factory; -use Progress_Planner\Suggested_Tasks\Providers\Content_Review; /** * Suggested_Tasks class. @@ -26,7 +24,7 @@ final class Suggested_Tasks extends Widget { /** * Get the score. * - * @return int The score. + * @return array The scores. */ public function get_score() { $activities = \progress_planner()->get_activities__query()->query_activities( @@ -42,103 +40,47 @@ public function get_score() { $score += $activity->get_points( $activity->date ); } - return (int) min( Monthly::TARGET_POINTS, max( 0, floor( $score ) ) ); + return [ + 'score' => (int) $score, + 'target' => (int) Monthly::TARGET_POINTS, + 'target_score' => (int) \min( Monthly::TARGET_POINTS, \max( 0, \floor( $score ) ) ), + ]; } /** - * Enqueue the scripts. + * Get previous month badge. * - * @return void + * @return \Progress_Planner\Badges\Monthly[] */ - public function enqueue_scripts() { - // Get tasks from task providers and pending_celebration tasks. - $tasks = \progress_planner()->get_suggested_tasks()->get_pending_tasks_with_details(); - $delay_celebration = false; - - // Celebrate only on the Progress Planner Dashboard page. - if ( \progress_planner()->is_on_progress_planner_dashboard_page() ) { - - // If there are newly added task providers, delay the celebration in order not to get confetti behind the popover. - $delay_celebration = \progress_planner()->get_plugin_upgrade_tasks()->should_show_upgrade_popover(); - - // If we're not delaying the celebration, we need to get the pending_celebration tasks. - if ( ! $delay_celebration ) { - $pending_celebration_tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'status', 'pending_celebration' ); + public function get_previous_incomplete_months_badges() { + $previous_incomplete_month_badges = []; - foreach ( $pending_celebration_tasks as $key => $task ) { - $task_id = $task['task_id']; - - $task_provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( - Task_Factory::create_task_from( 'id', $task_id )->get_provider_id() - ); - - if ( $task_provider && $task_provider->capability_required() ) { - $task_details = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_details( $task_id ); - - if ( $task_details ) { - $task_details['priority'] = 'high'; // Celebrate tasks are always on top. - $task_details['action'] = 'celebrate'; - $task_details['status'] = 'pending_celebration'; - - $tasks[] = $task_details; - } - - // Mark the pending celebration tasks as completed. - \progress_planner()->get_suggested_tasks()->transition_task_status( $task_id, 'pending_celebration', 'completed' ); - } - } - } + $minus_one_month = ( new \DateTime() )->modify( 'first day of previous month' ); + $minus_one_month_badge = Monthly::get_instance_from_id( Monthly::get_badge_id_from_date( $minus_one_month ) ); + if ( $minus_one_month_badge && $minus_one_month_badge->progress_callback()['progress'] < 100 ) { + $previous_incomplete_month_badges[] = $minus_one_month_badge; } - $final_tasks = []; - foreach ( $tasks as $task ) { - $task['status'] = $task['status'] ?? 'pending'; - $final_tasks[ $task['task_id'] ] = $task; + $minus_two_months = ( new \DateTime() )->modify( 'first day of previous month' )->modify( 'first day of previous month' ); + $minus_two_months_badge = Monthly::get_instance_from_id( Monthly::get_badge_id_from_date( $minus_two_months ) ); + if ( $minus_two_months_badge && $minus_two_months_badge->progress_callback()['progress'] < 100 ) { + $previous_incomplete_month_badges[] = $minus_two_months_badge; } - $final_tasks = array_values( $final_tasks ); - - // Sort the final tasks by priority. The priotity can be "high", "medium", "low", or "none". - uasort( - $final_tasks, - function ( $a, $b ) { - $priority = [ - 'high' => 0, - 'medium' => 1, - 'low' => 2, - 'none' => 3, - ]; - - $a['priority'] = ! isset( $a['priority'] ) || ! isset( $priority[ $a['priority'] ] ) ? 'none' : $a['priority']; - $b['priority'] = ! isset( $b['priority'] ) || ! isset( $priority[ $b['priority'] ] ) ? 'none' : $b['priority']; - - return $priority[ $a['priority'] ] - $priority[ $b['priority'] ]; - } - ); - - $max_items_per_category = []; - foreach ( $final_tasks as $task ) { - $max_items_per_category[ $task['category'] ] = $task['category'] === ( new Content_Review() )->get_provider_category() ? 2 : 1; - } + return $previous_incomplete_month_badges; + } - // We want to hide user tasks. - if ( isset( $max_items_per_category['user'] ) ) { - $max_items_per_category['user'] = 0; - } + /** + * Enqueue the scripts. + * + * @return void + */ + public function enqueue_scripts() { + parent::enqueue_scripts(); - // Enqueue the script. + // Enqueue the badge scroller script. \progress_planner()->get_admin__enqueue()->enqueue_script( - 'widgets/suggested-tasks', - [ - 'name' => 'prplSuggestedTasks', - 'data' => [ - 'ajaxUrl' => \admin_url( 'admin-ajax.php' ), - 'nonce' => \wp_create_nonce( 'progress_planner' ), - 'tasks' => array_values( $final_tasks ), - 'maxItemsPerCategory' => apply_filters( 'progress_planner_suggested_tasks_max_items_per_category', $max_items_per_category ), - 'delayCelebration' => $delay_celebration, - ], - ] + 'widgets/suggested-tasks-badge-scroller', ); } @@ -150,14 +92,14 @@ function ( $a, $b ) { public function get_stylesheet_dependencies() { // Register styles for the web-component. \wp_register_style( - 'progress-planner-web-components-prpl-suggested-task', - constant( 'PROGRESS_PLANNER_URL' ) . '/assets/css/web-components/prpl-suggested-task.css', + 'progress-planner-suggested-task', + \constant( 'PROGRESS_PLANNER_URL' ) . '/assets/css/suggested-task.css', [], - \progress_planner()->get_file_version( constant( 'PROGRESS_PLANNER_DIR' ) . '/assets/css/web-components/prpl-suggested-task.css' ) + \progress_planner()->get_file_version( \constant( 'PROGRESS_PLANNER_DIR' ) . '/assets/css/suggested-task.css' ) ); return [ - 'progress-planner-web-components-prpl-suggested-task', + 'progress-planner-suggested-task', ]; } } diff --git a/classes/admin/widgets/class-todo.php b/classes/admin/widgets/class-todo.php index 3b0f7ff18..8d94c84f9 100644 --- a/classes/admin/widgets/class-todo.php +++ b/classes/admin/widgets/class-todo.php @@ -36,6 +36,7 @@ public function print_content() { */ public function the_todo_list() { ?> +

    @@ -53,26 +54,6 @@ public function the_todo_list() { get_admin__enqueue()->enqueue_script( - 'widgets/todo', - [ - 'name' => 'progressPlannerTodo', - 'data' => [ - 'ajaxUrl' => \admin_url( 'admin-ajax.php' ), - 'nonce' => \wp_create_nonce( 'progress_planner' ), - 'tasks' => \progress_planner()->get_todo()->get_items(), - ], - ] - ); - } - /** * Get the stylesheet dependencies. * @@ -81,14 +62,14 @@ public function enqueue_scripts() { public function get_stylesheet_dependencies() { // Register styles for the web-component. \wp_register_style( - 'progress-planner-web-components-prpl-suggested-task', - constant( 'PROGRESS_PLANNER_URL' ) . '/assets/css/web-components/prpl-suggested-task.css', + 'progress-planner-suggested-task', + \constant( 'PROGRESS_PLANNER_URL' ) . '/assets/css/suggested-task.css', [], - \progress_planner()->get_file_version( constant( 'PROGRESS_PLANNER_DIR' ) . '/assets/css/web-components/prpl-suggested-task.css' ) + \progress_planner()->get_file_version( \constant( 'PROGRESS_PLANNER_DIR' ) . '/assets/css/suggested-task.css' ) ); return [ - 'progress-planner-web-components-prpl-suggested-task', + 'progress-planner-suggested-task', ]; } } diff --git a/classes/admin/widgets/class-whats-new.php b/classes/admin/widgets/class-whats-new.php index 2848e2c63..c343ddfd3 100644 --- a/classes/admin/widgets/class-whats-new.php +++ b/classes/admin/widgets/class-whats-new.php @@ -37,7 +37,7 @@ public function get_blog_feed() { $feed_data = \progress_planner()->get_utils__cache()->get( self::CACHE_KEY ); // Migrate old feed to new format. - if ( is_array( $feed_data ) && ! isset( $feed_data['expires'] ) && ! isset( $feed_data['feed'] ) ) { + if ( \is_array( $feed_data ) && ! isset( $feed_data['expires'] ) && ! isset( $feed_data['feed'] ) ) { $feed_data = [ 'feed' => $feed_data, 'expires' => \get_option( '_transient_timeout_' . Cache::CACHE_PREFIX . self::CACHE_KEY, 0 ), @@ -53,15 +53,15 @@ public function get_blog_feed() { } // Transient expired, fetch new feed. - if ( $feed_data['expires'] < time() ) { + if ( $feed_data['expires'] < \time() ) { // Get the feed using the REST API. $response = \wp_remote_get( \progress_planner()->get_remote_server_root_url() . '/wp-json/wp/v2/posts/?per_page=2' ); - if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { + if ( 200 !== \wp_remote_retrieve_response_code( $response ) ) { // If we cant fetch the feed, we will try again later. - $feed_data['expires'] = time() + 5 * MINUTE_IN_SECONDS; + $feed_data['expires'] = \time() + 5 * MINUTE_IN_SECONDS; } else { - $feed = json_decode( \wp_remote_retrieve_body( $response ), true ); + $feed = \json_decode( \wp_remote_retrieve_body( $response ), true ); foreach ( $feed as $key => $post ) { // Get the featured media. @@ -69,7 +69,7 @@ public function get_blog_feed() { if ( $featured_media_id ) { $response = \wp_remote_get( \progress_planner()->get_remote_server_root_url() . '/wp-json/wp/v2/media/' . $featured_media_id ); if ( ! \is_wp_error( $response ) ) { - $media = json_decode( \wp_remote_retrieve_body( $response ), true ); + $media = \json_decode( \wp_remote_retrieve_body( $response ), true ); $post['featured_media'] = $media; } @@ -78,7 +78,7 @@ public function get_blog_feed() { } $feed_data['feed'] = $feed; - $feed_data['expires'] = time() + 1 * DAY_IN_SECONDS; + $feed_data['expires'] = \time() + 1 * DAY_IN_SECONDS; } // Transient uses 'expires' key to determine if it's expired. diff --git a/classes/badges/class-badge-maintenance.php b/classes/badges/class-badge-maintenance.php index cfc1a0f4c..d1a9b2b42 100644 --- a/classes/badges/class-badge-maintenance.php +++ b/classes/badges/class-badge-maintenance.php @@ -38,7 +38,7 @@ public function get_goal() { 'status' => 'active', 'priority' => 'low', 'evaluate' => function ( $goal_object ) { - return count( + return \count( \progress_planner()->get_activities__query()->query_activities( [ 'start_date' => $goal_object->get_details()['start_date'], diff --git a/classes/badges/class-badge.php b/classes/badges/class-badge.php index 032318998..dedb3c0f1 100644 --- a/classes/badges/class-badge.php +++ b/classes/badges/class-badge.php @@ -59,9 +59,11 @@ abstract public function get_description(); /** * Progress callback. * + * @param array $args The arguments for the progress callback. + * * @return array */ - abstract public function progress_callback(); + abstract public function progress_callback( $args = [] ); /** * Get the saved progress. diff --git a/classes/badges/class-monthly.php b/classes/badges/class-monthly.php index 2804b801b..eb772e754 100644 --- a/classes/badges/class-monthly.php +++ b/classes/badges/class-monthly.php @@ -48,7 +48,6 @@ public function __construct( $id ) { * @return array */ public static function init_badges() { - if ( ! empty( self::$instances ) ) { return self::$instances; } @@ -65,10 +64,10 @@ public static function init_badges() { ? new \DateTime( 'last day of December next year' ) : new \DateTime( 'last day of December this year' ); - $dates = iterator_to_array( new \DatePeriod( $start_date, new \DateInterval( 'P1M' ), $end_date ), false ); + $dates = \iterator_to_array( new \DatePeriod( $start_date, new \DateInterval( 'P1M' ), $end_date ), false ); // To make sure keys are defined only once and consistent. - $self_months = array_keys( self::get_months() ); + $self_months = \array_keys( self::get_months() ); foreach ( $dates as $date ) { $year = (int) $date->format( 'Y' ); @@ -122,6 +121,30 @@ public static function get_badge_id_from_date( $date ) { return 'monthly-' . $date->format( 'Y' ) . '-m' . $date->format( 'n' ); } + /** + * Get the badge object from a badge ID. + * + * @param string $badge_id The badge ID. + * + * @return \Progress_Planner\Badges\Monthly|null + */ + public static function get_instance_from_id( $badge_id ) { + $year = (int) \explode( '-', \str_replace( 'monthly-', '', $badge_id ) )[0]; + $month = (int) \str_replace( 'm', '', \explode( '-', \str_replace( 'monthly-', '', $badge_id ) )[1] ); + + $instances = self::get_instances(); + + if ( isset( $instances[ $year ] ) ) { + foreach ( $instances[ $year ] as $instance ) { + if ( (int) $instance->get_month() === $month ) { + return $instance; + } + } + } + + return null; + } + /** * Get an array of months. * @@ -175,7 +198,7 @@ public function get_description() { * @return string */ public function get_year() { - return explode( '-', str_replace( 'monthly-', '', $this->id ) )[0]; + return \explode( '-', \str_replace( 'monthly-', '', $this->id ) )[0]; } /** @@ -184,19 +207,25 @@ public function get_year() { * @return string */ public function get_month() { - return str_replace( 'm', '', explode( '-', str_replace( 'monthly-', '', $this->id ) )[1] ); + return \str_replace( 'm', '', \explode( '-', \str_replace( 'monthly-', '', $this->id ) )[1] ); } /** * Progress callback. * + * @param array $args The arguments for the progress callback. + * * @return array */ - public function progress_callback() { + public function progress_callback( $args = [] ) { $saved_progress = $this->get_saved(); // If we have a saved value, return it. - if ( isset( $saved_progress['progress'] ) && isset( $saved_progress['remaining'] ) ) { + if ( isset( $saved_progress['progress'] ) + && isset( $saved_progress['remaining'] ) + && isset( $saved_progress['points'] ) + && 100 === $saved_progress['progress'] + ) { return $saved_progress; } @@ -205,7 +234,7 @@ public function progress_callback() { $month_num = (int) $this->get_month(); $start_date = \DateTime::createFromFormat( 'Y-m-d', "{$year}-{$month_num}-01" ); - $end_date = \DateTime::createFromFormat( 'Y-m-d', "{$year}-{$month_num}-" . gmdate( 't', strtotime( $month ) ) ); + $end_date = \DateTime::createFromFormat( 'Y-m-d', "{$year}-{$month_num}-" . \gmdate( 't', \strtotime( $month ) ) ); // Get the activities for the month. $activities = \progress_planner()->get_activities__query()->query_activities( @@ -221,17 +250,65 @@ public function progress_callback() { $points += $activity->get_points( $activity->date ); } - $return_progress = ( $points > self::TARGET_POINTS ) - ? [ - 'progress' => 100, - 'remaining' => 0, - ] : [ - 'progress' => (int) max( 0, min( 100, floor( 100 * $points / self::TARGET_POINTS ) ) ), - 'remaining' => self::TARGET_POINTS - $points, - ]; + $return_progress = [ + 'progress' => (int) \max( 0, \min( 100, \floor( 100 * $points / self::TARGET_POINTS ) ) ), + 'remaining' => (int) \max( 0, \min( self::TARGET_POINTS - $points, 10 ) ), + 'points' => $points, + ]; $this->save_progress( $return_progress ); - return $return_progress; + if ( $points >= self::TARGET_POINTS || ( isset( $args['no_next_badge_points'] ) && $args['no_next_badge_points'] ) ) { + return $return_progress; + } + + $points += $this->get_next_badges_excess_points(); + return [ + 'progress' => (int) \max( 0, \min( 100, \floor( 100 * $points / self::TARGET_POINTS ) ) ), + 'remaining' => (int) \max( 0, \min( self::TARGET_POINTS - $points, 10 ) ), + 'points' => $points, + ]; + } + + /** + * Get the next badge-ID. + * + * @return string + */ + public function get_next_badge_id() { + $year = $this->get_year(); + $month = $this->get_month(); + $month = $month < 10 ? "0$month" : $month; + $datetime = \DateTime::createFromFormat( 'Y-m-d', "{$year}-{$month}-10" ); + if ( ! $datetime ) { + return ''; + } + return self::get_badge_id_from_date( $datetime->modify( 'first day of next month' ) ); + } + + /** + * Get the next badge that has an excess of points, going forward up to 2 months. + * + * @return int + */ + public function get_next_badges_excess_points() { + $excess_points = 0; + $next_1_badge_points = 0; + $next_2_badge_points = 0; + // Get the next badge object. + $next_1_badge = self::get_instance_from_id( $this->get_next_badge_id() ); + if ( $next_1_badge ) { + $next_1_badge_points = $next_1_badge->progress_callback( [ 'no_next_badge_points' => true ] )['points']; + + $next_2_badge = self::get_instance_from_id( $next_1_badge->get_next_badge_id() ); + if ( $next_2_badge ) { + $next_2_badge_points = $next_2_badge->progress_callback( [ 'no_next_badge_points' => true ] )['points']; + } + } + + $excess_points = \max( 0, $next_1_badge_points - self::TARGET_POINTS ); + $excess_points += \max( 0, $next_2_badge_points - 2 * self::TARGET_POINTS ); + + return (int) $excess_points; } } diff --git a/classes/badges/content/class-content-curator.php b/classes/badges/content/class-content-curator.php index d56feee87..bb4e9a365 100644 --- a/classes/badges/content/class-content-curator.php +++ b/classes/badges/content/class-content-curator.php @@ -42,9 +42,11 @@ public function get_description() { /** * Progress callback. * + * @param array $args The arguments for the progress callback. + * * @return array */ - public function progress_callback() { + public function progress_callback( $args = [] ) { // Get the saved progress. $saved_progress = $this->get_saved(); @@ -59,7 +61,7 @@ public function progress_callback() { $total_posts_count += \wp_count_posts( $post_type )->publish; } - $remaining = 20 - min( 20, $total_posts_count ); + $remaining = 20 - \min( 20, $total_posts_count ); // If there are 20 existing posts, save the badge as complete and return. if ( 0 === $remaining ) { @@ -77,7 +79,7 @@ public function progress_callback() { } // Get the new posts count. - $new_count = count( + $new_count = \count( \progress_planner()->get_activities__query()->query_activities( [ 'category' => 'content', @@ -87,13 +89,13 @@ public function progress_callback() { ) ); - $remaining_new = 10 - min( 10, $new_count ); + $remaining_new = 10 - \min( 10, $new_count ); - $final_percent = max( - min( 100, floor( $total_posts_count / 2 ) ), - min( 100, floor( $new_count * 10 ) ) + $final_percent = \max( + \min( 100, \floor( $total_posts_count / 2 ) ), + \min( 100, \floor( $new_count * 10 ) ) ); - $final_remaining = min( $remaining, $remaining_new ); + $final_remaining = \min( $remaining, $remaining_new ); $this->save_progress( [ diff --git a/classes/badges/content/class-purposeful-publisher.php b/classes/badges/content/class-purposeful-publisher.php index 6926aa0ec..bcaf59427 100644 --- a/classes/badges/content/class-purposeful-publisher.php +++ b/classes/badges/content/class-purposeful-publisher.php @@ -37,15 +37,17 @@ public function get_name() { */ public function get_description() { /* translators: %d: The number of new posts to write. */ - return sprintf( \esc_html__( 'Write %d new posts or pages', 'progress-planner' ), 50 ); + return \sprintf( \esc_html__( 'Write %d new posts or pages', 'progress-planner' ), 50 ); } /** * Progress callback. * + * @param array $args The arguments for the progress callback. + * * @return array */ - public function progress_callback() { + public function progress_callback( $args = [] ) { $saved_progress = $this->get_saved(); // If we have a saved value, return it. @@ -54,7 +56,7 @@ public function progress_callback() { } // Get the number of new posts published. - $new_count = count( + $new_count = \count( \progress_planner()->get_activities__query()->query_activities( [ 'category' => 'content', @@ -64,8 +66,8 @@ public function progress_callback() { ) ); - $percent = min( 100, floor( 100 * $new_count / 50 ) ); - $remaining = 50 - min( 50, $new_count ); + $percent = \min( 100, \floor( 100 * $new_count / 50 ) ); + $remaining = 50 - \min( 50, $new_count ); $this->save_progress( [ diff --git a/classes/badges/content/class-revision-ranger.php b/classes/badges/content/class-revision-ranger.php index 5cde75634..f5463b8b8 100644 --- a/classes/badges/content/class-revision-ranger.php +++ b/classes/badges/content/class-revision-ranger.php @@ -37,15 +37,17 @@ public function get_name() { */ public function get_description() { /* translators: %d: The number of new posts to write. */ - return sprintf( \esc_html__( 'Write %d new posts or pages', 'progress-planner' ), 30 ); + return \sprintf( \esc_html__( 'Write %d new posts or pages', 'progress-planner' ), 30 ); } /** * Progress callback. * + * @param array $args The arguments for the progress callback. + * * @return array */ - public function progress_callback() { + public function progress_callback( $args = [] ) { $saved_progress = $this->get_saved(); // If we have a saved value, return it. @@ -54,7 +56,7 @@ public function progress_callback() { } // Get the number of new posts published. - $new_count = count( + $new_count = \count( \progress_planner()->get_activities__query()->query_activities( [ 'category' => 'content', @@ -64,8 +66,8 @@ public function progress_callback() { ) ); - $percent = min( 100, floor( 100 * $new_count / 30 ) ); - $remaining = 30 - min( 30, $new_count ); + $percent = \min( 100, \floor( 100 * $new_count / 30 ) ); + $remaining = 30 - \min( 30, $new_count ); $this->save_progress( [ diff --git a/classes/badges/maintenance/class-maintenance-maniac.php b/classes/badges/maintenance/class-maintenance-maniac.php index 076cd9fa7..fbcabe55f 100644 --- a/classes/badges/maintenance/class-maintenance-maniac.php +++ b/classes/badges/maintenance/class-maintenance-maniac.php @@ -37,15 +37,17 @@ public function get_name() { */ public function get_description() { /* translators: %d: The number of weeks. */ - return sprintf( \esc_html__( '%d weeks streak', 'progress-planner' ), 26 ); + return \sprintf( \esc_html__( '%d weeks streak', 'progress-planner' ), 26 ); } /** * Progress callback. * + * @param array $args The arguments for the progress callback. + * * @return array */ - public function progress_callback() { + public function progress_callback( $args = [] ) { $saved_progress = $this->get_saved(); // If we have a saved value, return it. @@ -54,8 +56,8 @@ public function progress_callback() { } $max_streak = $this->get_goal()->get_streak()['max_streak']; - $percent = min( 100, floor( 100 * $max_streak / 26 ) ); - $remaining = 26 - min( 26, $max_streak ); + $percent = \min( 100, \floor( 100 * $max_streak / 26 ) ); + $remaining = 26 - \min( 26, $max_streak ); $this->save_progress( [ diff --git a/classes/badges/maintenance/class-progress-padawan.php b/classes/badges/maintenance/class-progress-padawan.php index 2f0c4b673..f42b2ff39 100644 --- a/classes/badges/maintenance/class-progress-padawan.php +++ b/classes/badges/maintenance/class-progress-padawan.php @@ -37,15 +37,17 @@ public function get_name() { */ public function get_description() { /* translators: %d: The number of weeks. */ - return sprintf( \esc_html__( '%d weeks streak', 'progress-planner' ), 6 ); + return \sprintf( \esc_html__( '%d weeks streak', 'progress-planner' ), 6 ); } /** * Progress callback. * + * @param array $args The arguments for the progress callback. + * * @return array */ - public function progress_callback() { + public function progress_callback( $args = [] ) { $saved_progress = $this->get_saved(); // If we have a saved value, return it. @@ -54,8 +56,8 @@ public function progress_callback() { } $max_streak = $this->get_goal()->get_streak()['max_streak']; - $percent = min( 100, floor( 100 * $max_streak / 6 ) ); - $remaining = 6 - min( 6, $max_streak ); + $percent = \min( 100, \floor( 100 * $max_streak / 6 ) ); + $remaining = 6 - \min( 6, $max_streak ); $this->save_progress( [ diff --git a/classes/badges/maintenance/class-super-site-specialist.php b/classes/badges/maintenance/class-super-site-specialist.php index 090d6fd05..1af22f3d8 100644 --- a/classes/badges/maintenance/class-super-site-specialist.php +++ b/classes/badges/maintenance/class-super-site-specialist.php @@ -37,15 +37,17 @@ public function get_name() { */ public function get_description() { /* translators: %d: The number of weeks. */ - return sprintf( \esc_html__( '%d weeks streak', 'progress-planner' ), 52 ); + return \sprintf( \esc_html__( '%d weeks streak', 'progress-planner' ), 52 ); } /** * Progress callback. * + * @param array $args The arguments for the progress callback. + * * @return array */ - public function progress_callback() { + public function progress_callback( $args = [] ) { $saved_progress = $this->get_saved(); // If we have a saved value, return it. @@ -54,8 +56,8 @@ public function progress_callback() { } $max_streak = $this->get_goal()->get_streak()['max_streak']; - $percent = min( 100, floor( 100 * $max_streak / 52 ) ); - $remaining = 52 - min( 52, $max_streak ); + $percent = \min( 100, \floor( 100 * $max_streak / 52 ) ); + $remaining = 52 - \min( 52, $max_streak ); $this->save_progress( [ diff --git a/classes/class-badges.php b/classes/class-badges.php index 9aea331c6..2cfa867da 100644 --- a/classes/class-badges.php +++ b/classes/class-badges.php @@ -17,28 +17,28 @@ class Badges { /** * Content badges. * - * @var array<\Progress_Planner\Badges\Badge> + * @var \Progress_Planner\Badges\Badge[] */ private $content = []; /** * Maintenance badges. * - * @var array<\Progress_Planner\Badges\Badge> + * @var \Progress_Planner\Badges\Badge[] */ private $maintenance = []; /** * Monthly badges. * - * @var array<\Progress_Planner\Badges\Badge> + * @var \Progress_Planner\Badges\Badge[] */ private $monthly = []; /** * Monthly badges flat. * - * @var array<\Progress_Planner\Badges\Badge> + * @var \Progress_Planner\Badges\Badge[] */ private $monthly_flat = []; @@ -68,7 +68,7 @@ public function __construct() { // Init monthly badges. $this->monthly = Monthly::get_instances(); foreach ( $this->monthly as $monthly_year_badges ) { - $this->monthly_flat = array_merge( $this->monthly_flat, $monthly_year_badges ); + $this->monthly_flat = \array_merge( $this->monthly_flat, $monthly_year_badges ); } \add_action( 'progress_planner_suggested_task_completed', [ $this, 'clear_monthly_progress' ] ); @@ -80,7 +80,7 @@ public function __construct() { * * @param string $context The badges context (content|maintenance|monthly). * - * @return array<\Progress_Planner\Badges\Badge> + * @return \Progress_Planner\Badges\Badge[] */ public function get_badges( $context ) { return isset( $this->$context ) ? $this->$context : []; @@ -112,7 +112,6 @@ public function get_badge( $badge_id ) { * @return void */ public function clear_monthly_progress( $activity_id ) { - $activities = \progress_planner()->get_activities__query()->query_activities( [ 'category' => 'suggested_task', @@ -139,17 +138,14 @@ public function clear_monthly_progress( $activity_id ) { } } - /** * Clear the progress of all badges. * * @return void */ public function clear_content_progress() { - // Clear content saved progress. foreach ( $this->content as $badge ) { - // If the badge is already complete, skip it. if ( 100 <= $badge->progress_callback()['progress'] ) { continue; @@ -160,7 +156,6 @@ public function clear_content_progress() { } } - /** * Get the latest completed badge. * diff --git a/classes/class-base.php b/classes/class-base.php index df59fcd7a..8fa344544 100644 --- a/classes/class-base.php +++ b/classes/class-base.php @@ -7,10 +7,11 @@ namespace Progress_Planner; +use Progress_Planner\Utils\Deprecations; + /** * Main plugin class. * - * @method \Progress_Planner\Suggested_Tasks get_suggested_tasks() * @method \Progress_Planner\Settings get_settings() * @method \Progress_Planner\Activities\Query get_activities__query() * @method \Progress_Planner\Utils\Cache get_utils__cache() @@ -36,6 +37,9 @@ * @method \Progress_Planner\Utils\Debug_Tools get_utils__debug_tools() * @method \Progress_Planner\Badges get_badges() * @method \Progress_Planner\Plugin_Migrations get_plugin_migrations() + * @method \Progress_Planner\Suggested_Tasks get_suggested_tasks() + * @method \Progress_Planner\Suggested_Tasks_DB get_suggested_tasks_db() + * @method \Progress_Planner\Utils\Deprecations get_utils__deprecations() */ class Base { @@ -66,14 +70,14 @@ class Base { * @return void */ public function init() { - if ( ! function_exists( 'current_user_can' ) ) { + if ( ! \function_exists( 'current_user_can' ) ) { require_once ABSPATH . 'wp-includes/capabilities.php'; // @phpstan-ignore requireOnce.fileNotFound } - if ( ! function_exists( 'wp_get_current_user' ) ) { + if ( ! \function_exists( 'wp_get_current_user' ) ) { require_once ABSPATH . 'wp-includes/pluggable.php'; // @phpstan-ignore requireOnce.fileNotFound } - if ( defined( '\IS_PLAYGROUND_PREVIEW' ) && constant( '\IS_PLAYGROUND_PREVIEW' ) === true ) { + if ( \defined( '\IS_PLAYGROUND_PREVIEW' ) && \constant( '\IS_PLAYGROUND_PREVIEW' ) === true ) { $this->get_utils__playground(); } @@ -88,6 +92,9 @@ public function init() { $this->get_admin__dashboard_widget_todo(); } } + + $this->get_suggested_tasks(); + $this->get_admin__editor(); $this->get_actions__content(); @@ -109,12 +116,11 @@ public function init() { $this->get_page_todos(); } - \add_filter( 'plugin_action_links_' . plugin_basename( PROGRESS_PLANNER_FILE ), [ $this, 'add_action_links' ] ); + \add_filter( 'plugin_action_links_' . \plugin_basename( PROGRESS_PLANNER_FILE ), [ $this, 'add_action_links' ] ); // We need to initialize some classes early. $this->get_page_types(); $this->get_settings(); - $this->get_suggested_tasks(); $this->get_badges(); if ( true === $this->is_privacy_policy_accepted() ) { @@ -129,7 +135,7 @@ public function init() { $this->get_suggested_tasks__data_collector__data_collector_manager(); // Debug tools. - if ( ( defined( 'PRPL_DEBUG' ) && PRPL_DEBUG ) || \get_option( 'prpl_debug' ) ) { + if ( $this->is_debug_mode_enabled() ) { $this->get_utils__debug_tools(); } @@ -140,6 +146,11 @@ public function init() { * Redirect on login. */ \add_action( 'wp_login', [ $this, 'redirect_on_login' ], 10, 2 ); + + if ( \defined( 'WP_CLI' ) && \WP_CLI ) { + $this->get_wp_cli__get_stats_command(); + $this->get_wp_cli__task_command(); + } } /** @@ -157,50 +168,30 @@ public function init() { * @return mixed */ public function __call( $name, $arguments ) { - if ( 0 !== strpos( $name, 'get_' ) ) { + if ( 0 !== \strpos( $name, 'get_' ) ) { return; } - $cache_name = substr( $name, 4 ); + $cache_name = \substr( $name, 4 ); if ( isset( $this->cached[ $cache_name ] ) ) { return $this->cached[ $cache_name ]; } - $class_name = implode( '\\', explode( '__', $cache_name ) ); - $class_name = 'Progress_Planner\\' . implode( '_', array_map( 'ucfirst', explode( '_', $class_name ) ) ); - if ( class_exists( $class_name ) ) { + $class_name = \implode( '\\', \explode( '__', $cache_name ) ); + $class_name = 'Progress_Planner\\' . \implode( '_', \array_map( 'ucfirst', \explode( '_', $class_name ) ) ); + if ( \class_exists( $class_name ) ) { $this->cached[ $cache_name ] = new $class_name( $arguments ); return $this->cached[ $cache_name ]; } // Backwards-compatibility. - $deprecated = [ - 'get_query' => [ 'get_activities__query', '1.1.1' ], - 'get_date' => [ 'get_utils__date', '1.1.1' ], - 'get_widgets__suggested_tasks' => [ 'get_admin__widgets__suggested_tasks', '1.1.1' ], - 'get_widgets__activity_scores' => [ 'get_admin__widgets__activity_scores', '1.1.1' ], - 'get_widgets__todo' => [ 'get_admin__widgets__todo', '1.1.1' ], - 'get_widgets__challenge' => [ 'get_admin__widgets__challenge', '1.1.1' ], - 'get_widgets__latest_badge' => [ 'get_admin__widgets__latest_badge', '1.1.1' ], - 'get_widgets__badge_streak' => [ 'get_admin__widgets__badge_streak', '1.1.1' ], - 'get_widgets__published_content' => [ 'get_admin__widgets__published_content', '1.1.1' ], - 'get_widgets__whats_new' => [ 'get_admin__widgets__whats_new', '1.1.1' ], - 'get_onboard' => [ 'get_utils__onboard', '1.1.1' ], - 'get_cache' => [ 'get_utils__cache', '1.1.1' ], - 'get_rest_api_stats' => [ 'get_rest__stats', '1.1.1' ], - 'get_rest_api_tasks' => [ 'get_rest__tasks', '1.1.1' ], - 'get_data_collector__data_collector_manager' => [ 'get_suggested_tasks__data_collector__data_collector_manager', '1.1.1' ], - 'get_debug_tools' => [ 'get_utils__debug_tools', '1.1.1' ], - 'get_playground' => [ 'get_utils__playground', '1.1.1' ], - 'get_chart' => [ 'get_ui__chart', '1.1.1' ], - 'get_popover' => [ 'get_ui__popover', '1.1.1' ], - - 'get_admin__widgets__published_content' => [ 'get_admin__widgets__content_activity', '1.3.0' ], - ]; - - if ( isset( $deprecated[ $name ] ) ) { + if ( isset( Deprecations::BASE_METHODS[ $name ] ) ) { // Deprecated method. - \_deprecated_function( \esc_html( $name ), \esc_html( $deprecated[ $name ][1] ), \esc_html( $deprecated[ $name ][0] ) ); - return $this->{$deprecated[ $name ][0]}(); + \_deprecated_function( + \esc_html( $name ), + \esc_html( Deprecations::BASE_METHODS[ $name ][1] ), + \esc_html( Deprecations::BASE_METHODS[ $name ][0] ) + ); + return $this->{Deprecations::BASE_METHODS[ $name ][0]}(); } } @@ -210,7 +201,7 @@ public function __call( $name, $arguments ) { * @return string */ public function get_remote_server_root_url() { - return defined( 'PROGRESS_PLANNER_REMOTE_SERVER_ROOT_URL' ) + return \defined( 'PROGRESS_PLANNER_REMOTE_SERVER_ROOT_URL' ) ? \constant( 'PROGRESS_PLANNER_REMOTE_SERVER_ROOT_URL' ) : 'https://progressplanner.com'; } @@ -224,7 +215,7 @@ public function get_remote_server_root_url() { * @return string */ public function get_placeholder_svg( $width = 1200, $height = 675 ) { - return 'data:image/svg+xml;base64,' . base64_encode( sprintf( 'progressplanner.com', $width, $height, ( $width - 4 ), ( $height - 4 ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + return 'data:image/svg+xml;base64,' . \base64_encode( \sprintf( 'progressplanner.com', $width, $height, ( $width - 4 ), ( $height - 4 ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } /** @@ -259,12 +250,12 @@ public function is_privacy_policy_accepted() { * @return array */ public function add_action_links( $actions ) { - return array_merge( + return \array_merge( [ - sprintf( + \sprintf( '%2$s', \admin_url( 'admin.php?page=progress-planner' ), - __( 'Dashboard', 'progress-planner' ) + \__( 'Dashboard', 'progress-planner' ) ), ], $actions @@ -280,7 +271,7 @@ public function add_action_links( $actions ) { * @return void */ public function the_view( $template, $args = [] ) { - $templates = ( is_string( $template ) ) + $templates = ( \is_string( $template ) ) ? [ $template, "/views/{$template}" ] : $template; $this->the_file( $templates, $args ); @@ -296,7 +287,7 @@ public function the_view( $template, $args = [] ) { * @return void */ public function the_asset( $asset, $args = [] ) { - $assets = ( is_string( $asset ) ) + $assets = ( \is_string( $asset ) ) ? [ $asset, "/assets/{$asset}" ] : $asset; $this->the_file( $assets, $args ); @@ -312,12 +303,12 @@ public function the_asset( $asset, $args = [] ) { * @return string|false */ public function get_asset( $asset, $args = [] ) { - ob_start(); - $assets = ( is_string( $asset ) ) + \ob_start(); + $assets = ( \is_string( $asset ) ) ? [ $asset, "/assets/{$asset}" ] : $asset; $this->the_file( $assets, $args ); - return ob_get_clean(); + return \ob_get_clean(); } /** @@ -341,7 +332,7 @@ public function the_file( $files, $args = [] ) { $path = \PROGRESS_PLANNER_DIR . "/{$file}"; } if ( \file_exists( $path ) ) { - extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract + \extract( $args ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract include $path; // phpcs:ignore PEAR.Files.IncludingFile.UseRequire break; } @@ -356,12 +347,12 @@ public function the_file( $files, $args = [] ) { */ public function get_file_version( $file ) { // If we're in debug mode, use filemtime. - if ( defined( 'WP_SCRIPT_DEBUG' ) && constant( 'WP_SCRIPT_DEBUG' ) ) { - return (string) filemtime( $file ); + if ( \defined( 'WP_SCRIPT_DEBUG' ) && \constant( 'WP_SCRIPT_DEBUG' ) ) { + return (string) \filemtime( $file ); } // Otherwise, use the plugin header. - if ( ! function_exists( 'get_file_data' ) ) { + if ( ! \function_exists( 'get_file_data' ) ) { require_once ABSPATH . 'wp-includes/functions.php'; // @phpstan-ignore requireOnce.fileNotFound } @@ -386,8 +377,7 @@ public function is_local_site() { $host = ! empty( $url_parts['host'] ) ? $url_parts['host'] : false; if ( ! empty( $url ) && ! empty( $host ) ) { - if ( - 'localhost' === $host + if ( 'localhost' === $host || ( false !== \ip2long( $host ) && ! \filter_var( $host, \FILTER_VALIDATE_IP, \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE ) @@ -483,5 +473,14 @@ public function is_on_progress_planner_dashboard_page() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We're not processing any data. return \is_admin() && isset( $_GET['page'] ) && $_GET['page'] === 'progress-planner'; } + + /** + * Check whether debug mode is enabled. + * + * @return bool + */ + public function is_debug_mode_enabled() { + return ( \defined( 'PRPL_DEBUG' ) && PRPL_DEBUG ) || \get_option( 'prpl_debug' ); + } } // phpcs:enable Generic.Commenting.Todo diff --git a/classes/class-lessons.php b/classes/class-lessons.php index f9602d8c6..0e91a860a 100644 --- a/classes/class-lessons.php +++ b/classes/class-lessons.php @@ -47,10 +47,10 @@ public function get_remote_api_items() { ) : \add_query_arg( [ 'site' => \get_site_url() ], $url ); - $cache_key = md5( $url ); + $cache_key = \md5( $url ); $cached = \progress_planner()->get_utils__cache()->get( $cache_key ); - if ( is_array( $cached ) ) { + if ( \is_array( $cached ) ) { return $cached; } @@ -61,13 +61,13 @@ public function get_remote_api_items() { return []; } - if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { + if ( 200 !== (int) \wp_remote_retrieve_response_code( $response ) ) { \progress_planner()->get_utils__cache()->set( $cache_key, [], 5 * MINUTE_IN_SECONDS ); return []; } - $json = json_decode( \wp_remote_retrieve_body( $response ), true ); - if ( ! is_array( $json ) ) { + $json = \json_decode( \wp_remote_retrieve_body( $response ), true ); + if ( ! \is_array( $json ) ) { \progress_planner()->get_utils__cache()->set( $cache_key, [], 5 * MINUTE_IN_SECONDS ); return []; } diff --git a/classes/class-page-todos.php b/classes/class-page-todos.php index 764dacedc..787c0d357 100644 --- a/classes/class-page-todos.php +++ b/classes/class-page-todos.php @@ -52,15 +52,15 @@ public function init() { * @return string */ public function sanitize_post_meta_progress_planner_page_todos( $value ) { - $values = explode( ',', $value ); + $values = \explode( ',', $value ); // Remove any empty values. - $values = array_filter( $values ); + $values = \array_filter( $values ); // Remove any duplicates. - $values = array_unique( $values ); + $values = \array_unique( $values ); // Trim all values. - $values = array_map( 'trim', $values ); + $values = \array_map( 'trim', $values ); // Return the sanitized value. - return \sanitize_text_field( implode( ',', $values ) ); + return \sanitize_text_field( \implode( ',', $values ) ); } } diff --git a/classes/class-page-types.php b/classes/class-page-types.php index 5a7b71a91..ce2cec6bc 100644 --- a/classes/class-page-types.php +++ b/classes/class-page-types.php @@ -257,7 +257,6 @@ public function set_page_type_by_id( $post_id, $page_type_id ) { public function get_default_page_type( $post_type, $post_id ) { // Post-type checks. switch ( $post_type ) { - // Products from WooCommerce & EDD. case 'product': case 'download': @@ -291,7 +290,6 @@ public function get_default_page_type( $post_type, $post_id ) { * @return int */ public function get_default_page_id_by_type( $page_type ) { - $homepage_id = \get_option( 'page_on_front' ) ?? 0; // Early return for the homepage. @@ -301,18 +299,18 @@ public function get_default_page_id_by_type( $page_type ) { $types_pages = [ 'homepage' => [ $homepage_id ], - 'contact' => $this->get_posts_by_title( __( 'Contact', 'progress-planner' ) ), - 'about' => $this->get_posts_by_title( __( 'About', 'progress-planner' ) ), - 'faq' => array_merge( - $this->get_posts_by_title( __( 'FAQ', 'progress-planner' ) ), - $this->get_posts_by_title( __( 'Frequently Asked Questions', 'progress-planner' ) ), + 'contact' => $this->get_posts_by_title( \__( 'Contact', 'progress-planner' ) ), + 'about' => $this->get_posts_by_title( \__( 'About', 'progress-planner' ) ), + 'faq' => \array_merge( + $this->get_posts_by_title( \__( 'FAQ', 'progress-planner' ) ), + $this->get_posts_by_title( \__( 'Frequently Asked Questions', 'progress-planner' ) ), ), ]; - $defined_page_types = array_keys( $types_pages ); + $defined_page_types = \array_keys( $types_pages ); // If the page type is not among defined page types, return 0. - if ( ! in_array( $page_type, $defined_page_types, true ) ) { + if ( ! \in_array( $page_type, $defined_page_types, true ) ) { return 0; } @@ -326,7 +324,6 @@ public function get_default_page_id_by_type( $page_type ) { // Exclude the homepage and any pages that are already assigned to another page-type. foreach ( $defined_page_types as $defined_page_type ) { - // Skip the current page-type. if ( $page_type === $defined_page_type ) { continue; @@ -473,7 +470,7 @@ public function is_page_needed( $type ) { if ( ! $term ) { return false; } - return '' !== get_term_meta( $term->term_id, '_progress_planner_no_page', true ) ? false : true; + return '' !== \get_term_meta( $term->term_id, '_progress_planner_no_page', true ) ? false : true; } /** @@ -507,7 +504,7 @@ public function set_no_page_needed( $type, $value ) { private function get_posts_by_title( $title ) { global $wpdb; // Check if we have a cached result. - $cache_key = 'pp_posts_by_title_' . sanitize_title( $title ); + $cache_key = 'pp_posts_by_title_' . \sanitize_title( $title ); $cache_group = \Progress_Planner\Activities\Query::CACHE_GROUP; $posts_ids = \wp_cache_get( $cache_key, $cache_group ); if ( false !== $posts_ids ) { diff --git a/classes/class-plugin-deactivation.php b/classes/class-plugin-deactivation.php index 928c9933a..6b05b85bd 100644 --- a/classes/class-plugin-deactivation.php +++ b/classes/class-plugin-deactivation.php @@ -216,7 +216,7 @@ protected function the_inline_script() { const requestData = { action: 'plugin_deactivation', plugin: '', - site: '', + site: '', }; deactivatePluginFeedbackAjaxRequest( { // Get a nonce from the remote server. diff --git a/classes/class-plugin-migrations.php b/classes/class-plugin-migrations.php index eb43e0015..b2d4130a9 100644 --- a/classes/class-plugin-migrations.php +++ b/classes/class-plugin-migrations.php @@ -66,24 +66,24 @@ private function get_db_version() { */ public function maybe_upgrade() { // If the current version is the same as the plugin version, do nothing. - if ( version_compare( $this->db_version, $this->version, '=' ) && + if ( \version_compare( $this->db_version, $this->version, '=' ) && ! \get_option( 'prpl_debug_migrations' ) ) { return; } // Get all available updates, as an array of integers. - $updates_files = glob( PROGRESS_PLANNER_DIR . '/classes/update/*.php' ); - if ( ! is_array( $updates_files ) ) { + $updates_files = \glob( PROGRESS_PLANNER_DIR . '/classes/update/*.php' ); + if ( ! \is_array( $updates_files ) ) { return; } - $updates = array_map( + $updates = \array_map( function ( $file ) { - return str_replace( 'class-update-', '', basename( $file, '.php' ) ); + return \str_replace( 'class-update-', '', \basename( $file, '.php' ) ); }, $updates_files ); - sort( $updates ); + \sort( $updates ); // Run the upgrades. foreach ( $updates as $version_int ) { @@ -91,10 +91,10 @@ function ( $file ) { $version = $upgrade_class::VERSION; if ( \get_option( 'prpl_debug_migrations' ) || - version_compare( $version, $this->db_version, '>' ) + \version_compare( $version, $this->db_version, '>' ) ) { $upgrade_class = new $upgrade_class(); - if ( method_exists( $upgrade_class, 'run' ) ) { + if ( \method_exists( $upgrade_class, 'run' ) ) { $upgrade_class->run(); } } @@ -111,6 +111,6 @@ function ( $file ) { * @param string $version The new version of the plugin. * @param string $db_version The old version of the plugin. */ - do_action( 'progress_planner_plugin_updated', $this->version, $this->db_version ); + \do_action( 'progress_planner_plugin_updated', $this->version, $this->db_version ); } } diff --git a/classes/class-plugin-upgrade-tasks.php b/classes/class-plugin-upgrade-tasks.php index 87b619659..fdd449f46 100644 --- a/classes/class-plugin-upgrade-tasks.php +++ b/classes/class-plugin-upgrade-tasks.php @@ -16,7 +16,6 @@ class Plugin_Upgrade_Tasks { * Constructor. */ public function __construct() { - // Plugin (possibly 3rd party) activated. \add_action( 'activated_plugin', [ $this, 'plugin_activated_or_updated' ], 10 ); @@ -36,7 +35,7 @@ public function __construct() { * @return void */ public function plugin_activated_or_updated() { - update_option( 'progress_planner_plugin_was_activated', true ); + \update_option( 'progress_planner_plugin_was_activated', true ); } /** @@ -82,13 +81,13 @@ public function maybe_add_onboarding_tasks() { $newly_added_task_provider_ids = \get_option( 'progress_planner_upgrade_popover_task_provider_ids', [] ); foreach ( $onboard_task_provider_ids as $task_provider_id ) { - if ( ! empty( $task_provider_id ) && ! in_array( $task_provider_id, $old_task_providers, true ) && ! in_array( $task_provider_id, $newly_added_task_provider_ids, true ) ) { + if ( ! empty( $task_provider_id ) && ! \in_array( $task_provider_id, $old_task_providers, true ) && ! \in_array( $task_provider_id, $newly_added_task_provider_ids, true ) ) { $newly_added_task_provider_ids[] = $task_provider_id; } } // Update 'progress_planner_previous_version_task_providers' option. - \update_option( 'progress_planner_previous_version_task_providers', array_unique( array_merge( $old_task_providers, $onboard_task_provider_ids ), SORT_REGULAR ) ); + \update_option( 'progress_planner_previous_version_task_providers', \array_unique( \array_merge( $old_task_providers, $onboard_task_provider_ids ), SORT_REGULAR ) ); // Update 'progress_planner_upgrade_popover_task_providers' option. \update_option( 'progress_planner_upgrade_popover_task_provider_ids', $newly_added_task_provider_ids ); diff --git a/classes/class-settings.php b/classes/class-settings.php index 963c8cdfe..79cb2c701 100644 --- a/classes/class-settings.php +++ b/classes/class-settings.php @@ -40,7 +40,7 @@ class Settings { public function get( $setting, $default_value = null ) { $this->load_settings(); - if ( is_array( $setting ) ) { + if ( \is_array( $setting ) ) { return \_wp_array_get( self::$settings, $setting, $default_value ); } return self::$settings[ $setting ] ?? $default_value; @@ -59,7 +59,7 @@ public function get( $setting, $default_value = null ) { */ public function set( $setting, $value ) { $this->load_settings(); - if ( is_array( $setting ) ) { + if ( \is_array( $setting ) ) { \_wp_array_set( self::$settings, $setting, $value ); } else { self::$settings[ $setting ] = $value; @@ -119,11 +119,11 @@ public function delete_all() { public function get_post_types_names() { static $include_post_types; - if ( ! doing_action( 'init' ) && ! did_action( 'init' ) ) { + if ( ! \doing_action( 'init' ) && ! \did_action( 'init' ) ) { \trigger_error( // phpcs:ignore - sprintf( + \sprintf( '%1$s was called too early. Wait for init hook to be called to have access to the post types.', - \esc_html( get_class() . '::' . __FUNCTION__ ) + \esc_html( \get_class() . '::' . __FUNCTION__ ) ), E_USER_WARNING ); @@ -137,10 +137,10 @@ public function get_post_types_names() { $public_post_types = $this->get_public_post_types(); // Post or pages can be deregistered. - $default = array_intersect( [ 'post', 'page' ], $public_post_types ); + $default = \array_intersect( [ 'post', 'page' ], $public_post_types ); // Filter the saved post types. - $include_post_types = array_intersect( $this->get( [ 'include_post_types' ], $default ), $public_post_types ); + $include_post_types = \array_intersect( $this->get( [ 'include_post_types' ], $default ), $public_post_types ); return empty( $include_post_types ) ? $default : \array_values( $include_post_types ); } @@ -155,6 +155,7 @@ public function get_public_post_types() { unset( $public_post_types['attachment'] ); unset( $public_post_types['elementor_library'] ); // Elementor templates are not a post type we want to track. + unset( $public_post_types['prpl_recommendations'] ); /** * Filter the public post types. diff --git a/classes/class-suggested-tasks-db.php b/classes/class-suggested-tasks-db.php new file mode 100644 index 000000000..3aa81db20 --- /dev/null +++ b/classes/class-suggested-tasks-db.php @@ -0,0 +1,384 @@ +get_tasks_by( + [ + 'post_status' => [ 'publish', 'trash', 'draft', 'future', 'pending' ], // 'any' doesn't include statuses which have 'exclude_from_search' set to true (trash and pending). + 'numberposts' => 1, + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + [ + 'key' => 'prpl_task_id', + 'value' => $data['task_id'], + 'compare' => '=', + ], + ], + ] + ); + + // If we have an existing task, skip. + if ( ! empty( $posts ) ) { + \delete_transient( $transient_key ); + return $posts[0]->ID; + } + + if ( ! isset( $data['order'] ) && isset( $data['priority'] ) ) { + $data['order'] = $data['priority']; + } + + $data['post_status'] = $data['post_status'] ?? 'publish'; + + $args = [ + 'post_type' => 'prpl_recommendations', + 'post_title' => $data['post_title'], + 'post_content' => $data['description'] ?? '', + 'menu_order' => $data['order'] ?? 0, + ]; + switch ( $data['post_status'] ) { + case 'pending': + $args['post_status'] = 'pending'; + break; + + case 'completed': + case 'trash': + $args['post_status'] = 'trash'; + break; + + case 'snoozed': + $args['post_status'] = 'future'; + $args['post_date'] = \DateTime::createFromFormat( 'U', $data['time'] )->format( 'Y-m-d H:i:s' ); + break; + + default: + $args['post_status'] = 'publish'; + break; + } + + $post_id = \wp_insert_post( $args ); + + // Add terms if they don't exist. + foreach ( [ 'category', 'provider_id' ] as $context ) { + $taxonomy_name = \str_replace( '_id', '', $context ); + $term = \get_term_by( 'name', $data[ $context ], "prpl_recommendations_$taxonomy_name" ); + if ( ! $term ) { + \wp_insert_term( $data[ $context ], "prpl_recommendations_$taxonomy_name" ); + } + } + + // Set the task category. + \wp_set_post_terms( $post_id, $data['category'], 'prpl_recommendations_category' ); + + // Set the task provider. + \wp_set_post_terms( $post_id, $data['provider_id'], 'prpl_recommendations_provider' ); + + // Set the task parent. + if ( ! empty( $data['parent'] ) ) { + $parent = \get_post( $data['parent'] ); + if ( $parent ) { + \wp_update_post( + [ + 'ID' => $post_id, + 'post_parent' => $parent->ID, + ] + ); + } + } + + // Set other meta. + $default_keys = [ + 'title', + 'description', + 'status', + 'category', + 'provider_id', + 'parent', + 'order', + 'post_status', + ]; + foreach ( $data as $key => $value ) { + if ( \in_array( $key, $default_keys, true ) ) { + continue; + } + + \update_post_meta( $post_id, "prpl_$key", $value ); + } + + \delete_transient( $transient_key ); + + return $post_id; + } + + /** + * Update a recommendation. + * + * @param int $id The recommendation ID. + * @param array $data The data to update. + * + * @return bool + */ + public function update_recommendation( $id, $data ) { + if ( ! $id ) { + return false; + } + + $update_data = [ 'ID' => $id ]; + $update_meta = []; + $update_terms = []; + $update_results = []; + foreach ( $data as $key => $value ) { + switch ( $key ) { + case 'points': + case 'prpl_points': + $update_meta[ 'prpl_' . \str_replace( 'prpl_', '', (string) $key ) ] = $value; + break; + + case 'category': + case 'provider': + $update_terms[ "prpl_recommendations_$key" ] = $value; + break; + + default: + $update_data[ $key ] = $value; + break; + } + } + + if ( 1 < \count( $update_data ) ) { + $update_results[] = (bool) \wp_update_post( $update_data ); + } + + if ( ! empty( $update_meta ) ) { + foreach ( $update_meta as $key => $value ) { + $update_results[] = (bool) \update_post_meta( $id, $key, $value ); + } + } + + if ( ! empty( $update_terms ) ) { + foreach ( $update_terms as $taxonomy => $term ) { + $update_results[] = (bool) \wp_set_object_terms( $id, $term->slug, $taxonomy ); + } + } + + return ! \in_array( false, $update_results, true ); + } + + /** + * Delete all recommendations. + * + * @return void + */ + public function delete_all_recommendations() { + // Get all recommendations. + $recommendations = $this->get(); + + // Delete each recommendation. + foreach ( $recommendations as $recommendation ) { + $this->delete_recommendation( $recommendation->ID ); + } + } + + /** + * Delete a recommendation. + * + * @param int $id The recommendation ID. + * + * @return bool + */ + public function delete_recommendation( int $id ) { + $result = (bool) \wp_delete_post( $id, true ); + \wp_cache_flush_group( static::GET_TASKS_CACHE_GROUP ); + return $result; + } + + /** + * Format recommendations results. + * + * @param array $recommendations The recommendations. + * + * @return \Progress_Planner\Suggested_Tasks\Task[] + */ + public function format_recommendations( $recommendations ) { + $result = []; + foreach ( $recommendations as $recommendation ) { + $result[] = $this->format_recommendation( $recommendation ); + } + + return $result; + } + + /** + * Format a recommendation. + * + * @param \WP_Post $post The recommendation post. + * + * @return \Progress_Planner\Suggested_Tasks\Task + */ + public function format_recommendation( $post ) { + static $cached = []; + if ( isset( $cached[ $post->ID ] ) ) { + return $cached[ $post->ID ]; + } + + $post_data = (array) $post; + + // Format the post meta. + $post_meta = \get_post_meta( $post_data['ID'] ); + foreach ( $post_meta as $key => $value ) { + $post_data[ \str_replace( 'prpl_', '', (string) $key ) ] = + \is_array( $value ) && isset( $value[0] ) && 1 === \count( $value ) + ? $value[0] + : $value; + } + + foreach ( [ 'category', 'provider' ] as $context ) { + $terms = \wp_get_post_terms( $post_data['ID'], "prpl_recommendations_$context" ); + $post_data[ $context ] = \is_array( $terms ) && isset( $terms[0] ) ? $terms[0] : null; + } + + $cached[ $post_data['ID'] ] = new Task( $post_data ); + return $cached[ $post_data['ID'] ]; + } + + /** + * Get the post-ID of a recommendation. + * + * @param string|int $id The recommendation ID. Can be a task-ID or a post-ID. + * + * @return \Progress_Planner\Suggested_Tasks\Task|false The recommendation post or false if not found. + */ + public function get_post( $id ) { + $posts = $this->get_tasks_by( + \is_numeric( $id ) + ? [ 'p' => $id ] + : [ 'task_id' => $id ] + ); + + return isset( $posts[0] ) ? $posts[0] : false; + } + + /** + * Get recommendations, filtered by a parameter. + * + * @param array $params The parameters to filter by ([ 'provider' => 'provider_id' ] etc). + * + * @return \Progress_Planner\Suggested_Tasks\Task[] + */ + public function get_tasks_by( $params ) { + $args = []; + + foreach ( $params as $param => $value ) { + switch ( $param ) { + case 'provider': + case 'provider_id': + case 'category': + $args['tax_query'] = isset( $args['tax_query'] ) ? $args['tax_query'] : []; // phpcs:ignore WordPress.DB.SlowDBQuery + $args['tax_query'][] = [ + 'taxonomy' => 'category' === $param + ? 'prpl_recommendations_category' + : 'prpl_recommendations_provider', + 'field' => 'slug', + 'terms' => (array) $value, + ]; + + unset( $params[ $param ] ); + break; + + case 'task_id': + $args['meta_query'] = isset( $args['meta_query'] ) ? $args['meta_query'] : []; // phpcs:ignore WordPress.DB.SlowDBQuery + $args['meta_query'][] = [ + 'key' => 'prpl_task_id', + 'value' => $value, + ]; + + unset( $params[ $param ] ); + break; + + default: + $args[ $param ] = $value; + break; + } + } + + return $this->get( $args ); + } + + /** + * Get recommendations. + * + * @param array $args The arguments. + * + * @return \Progress_Planner\Suggested_Tasks\Task[] + */ + public function get( $args = [] ) { + $args = \wp_parse_args( + $args, + [ + 'post_type' => 'prpl_recommendations', + 'post_status' => [ 'publish', 'trash', 'draft', 'future', 'pending' ], // 'any' doesn't include statuses which have 'exclude_from_search' set to true (trash and pending). + 'numberposts' => -1, + 'orderby' => 'menu_order', + 'order' => 'ASC', + ] + ); + + $cache_key = 'progress-planner-get-tasks-' . \md5( (string) \wp_json_encode( $args ) ); + $results = \wp_cache_get( $cache_key, static::GET_TASKS_CACHE_GROUP ); + if ( $results ) { + return $results; + } + + $results = $this->format_recommendations( + \get_posts( $args ) + ); + + \wp_cache_set( $cache_key, $results, static::GET_TASKS_CACHE_GROUP ); + + return $results; + } +} diff --git a/classes/class-suggested-tasks.php b/classes/class-suggested-tasks.php index c8555a11e..a45d0285b 100644 --- a/classes/class-suggested-tasks.php +++ b/classes/class-suggested-tasks.php @@ -1,47 +1,73 @@ + */ + const STATUS_MAP = [ + 'completed' => 'trash', + 'pending_celebration' => 'pending', + 'pending' => 'publish', + 'snoozed' => 'future', + ]; + /** * An object containing tasks. * - * @var \Progress_Planner\Suggested_Tasks\Tasks_Manager|null + * @var \Progress_Planner\Suggested_Tasks\Tasks_Manager */ - private $tasks_manager; + private Tasks_Manager $tasks_manager; /** * Constructor. - * - * @return void */ public function __construct() { $this->tasks_manager = new Tasks_Manager(); - \add_action( 'wp_ajax_progress_planner_suggested_task_action', [ $this, 'suggested_task_action' ] ); - if ( \is_admin() ) { \add_action( 'init', [ $this, 'init' ], 100 ); // Wait for the post types to be initialized. - // Check GET parameter and maybe set task as pending celebration. + // Check GET parameter and maybe set task as pending. \add_action( 'init', [ $this, 'maybe_complete_task' ] ); } + \add_action( 'wp_ajax_progress_planner_suggested_task_action', [ $this, 'suggested_task_action' ] ); // Add the automatic updates complete action. \add_action( 'automatic_updates_complete', [ $this, 'on_automatic_updates_complete' ] ); + + // Register the custom post type. + \add_action( 'init', [ $this, 'register_post_type' ], 0 ); + + // Register the custom taxonomies. + \add_action( 'init', [ $this, 'register_taxonomy' ], 0 ); + + // Filter the REST API tax query. + \add_filter( 'rest_prpl_recommendations_query', [ $this, 'rest_api_tax_query' ], 10, 2 ); + + // Filter the REST API response. + \add_filter( 'rest_prepare_prpl_recommendations', [ $this, 'rest_prepare_recommendation' ], 10, 2 ); + + \add_filter( 'wp_trash_post_days', [ $this, 'change_trashed_posts_lifetime' ], 10, 2 ); } /** @@ -49,26 +75,20 @@ public function __construct() { * * @return void */ - public function init() { - // Unsnooze tasks. - $this->maybe_unsnooze_tasks(); - + public function init(): void { // Check for completed tasks. - $completed_tasks = $this->tasks_manager->evaluate_tasks(); // @phpstan-ignore-line method.nonObject + $completed_tasks = $this->tasks_manager->evaluate_tasks(); foreach ( $completed_tasks as $task ) { + if ( ! $task->task_id && $task->ID ) { + continue; + } - // Get the task data. - $task_data = $task->get_data(); - - // Update the task data. - $this->update_pending_task( $task_data['task_id'], $task_data ); - - // Change the task status to pending celebration. - $this->mark_task_as( 'pending_celebration', $task_data['task_id'] ); + // Change the task status to pending. + $task->celebrate(); // Insert an activity. - $this->insert_activity( $task_data['task_id'] ); + $this->insert_activity( $task->task_id ); } } @@ -79,7 +99,7 @@ public function init() { * * @return void */ - public function insert_activity( $task_id ) { + public function insert_activity( string $task_id ): void { // Insert an activity. $activity = new Suggested_Task_Activity(); $activity->type = 'completed'; @@ -89,7 +109,7 @@ public function insert_activity( $task_id ) { $activity->save(); // Allow other classes to react to the completion of a suggested task. - do_action( 'progress_planner_suggested_task_completed', $task_id ); + \do_action( 'progress_planner_suggested_task_completed', $task_id ); } /** @@ -99,7 +119,7 @@ public function insert_activity( $task_id ) { * * @return void */ - public function delete_activity( $task_id ) { + public function delete_activity( string $task_id ): void { $activity = \progress_planner()->get_activities__query()->query_activities( [ 'data_id' => $task_id, @@ -115,510 +135,409 @@ public function delete_activity( $task_id ) { } /** - * If done via automatic updates, the "core update" task should be marked as "completed" (and skip "pending celebration" status). + * If done via automatic updates, the "core update" task should be marked as "trashed" (and skip "pending" status). * * @return void */ - public function on_automatic_updates_complete() { - - $pending_tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); // @phpstan-ignore-line method.nonObject + public function on_automatic_updates_complete(): void { + $pending_tasks = \progress_planner()->get_suggested_tasks_db()->get( + [ + 'numberposts' => 1, + 'post_status' => 'publish', + 'provider_id' => 'update-core', + 'date_query' => [ [ 'after' => 'this Monday' ] ], + ] + ); if ( empty( $pending_tasks ) ) { return; } - foreach ( $pending_tasks as $task_data ) { - $task_id = $task_data['task_id']; + \progress_planner()->get_suggested_tasks_db()->update_recommendation( $pending_tasks[0]->ID, [ 'post_status' => 'trash' ] ); - if ( $task_data['provider_id'] === ( new Core_Update() )->get_provider_id() && - \gmdate( 'YW' ) === $task_data['date'] - ) { - // Change the task status to completed. - $this->mark_task_as( 'completed', $task_id ); - - // Insert an activity. - $this->insert_activity( $task_id ); - break; - } - } + // Insert an activity. + $this->insert_activity( $pending_tasks[0]->task_id ); } /** - * Get the tasks manager object. + * Get the tasks manager. * * @return \Progress_Planner\Suggested_Tasks\Tasks_Manager */ - public function get_tasks_manager() { - return $this->tasks_manager; // @phpstan-ignore-line return.type + public function get_tasks_manager(): Tasks_Manager { + return $this->tasks_manager; } /** - * Return filtered items. + * Check if a task was completed. Task is considered completed if it was trashed or pending. * - * @return array + * @param string|int $task_id The task ID. + * + * @return bool */ - public function get_tasks() { - $tasks = []; - /** - * Filter the suggested tasks. - * - * @param array $tasks The suggested tasks. - * @return array - */ - $tasks = \apply_filters( 'progress_planner_suggested_tasks_items', $tasks ); - $db_tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); - foreach ( $tasks as $key => $task ) { - if ( isset( $task['status'] ) && ! empty( $task['status'] ) ) { - continue; - } - - foreach ( $db_tasks as $db_task_key => $db_task ) { - if ( $db_task['task_id'] === $task['task_id'] ) { - $tasks[ $key ]['status'] = $db_task['status']; - unset( $db_tasks[ $db_task_key ] ); - break; - } - } - } - - return $tasks; + public function was_task_completed( $task_id ): bool { + $task = \progress_planner()->get_suggested_tasks_db()->get_post( $task_id ); + return $task && $task->is_completed(); } /** - * Get pending tasks with details. + * Maybe complete a task. + * Primarly this is used for deeplinking, ie user is testing if the emails are working + * He gets an email with a link which automatically completes the task. * - * @return array + * @return void */ - public function get_pending_tasks_with_details() { - $tasks = $this->get_tasks(); - $tasks_with_details = []; - - foreach ( $tasks as $task ) { - $task_details = Task_Factory::create_task_from( 'id', $task['task_id'] )->get_task_details(); - - if ( $task_details ) { - $tasks_with_details[] = $task_details; - } + public function maybe_complete_task() { + if ( ! \progress_planner()->is_on_progress_planner_dashboard_page() || ! isset( $_GET['prpl_complete_task'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return; } - return $tasks_with_details; - } + $task_id = \sanitize_text_field( \wp_unslash( $_GET['prpl_complete_task'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( ! $task_id ) { + return; + } - /** - * Get tasks by. - * - * @param string $param The parameter. - * @param string $value The value. - * - * @return array - */ - public function get_tasks_by( $param, $value ) { - $tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); - $tasks = array_filter( - $tasks, - function ( $task ) use ( $param, $value ) { - return isset( $task[ $param ] ) && $task[ $param ] === $value; - } - ); + if ( ! $this->was_task_completed( $task_id ) ) { + $task = \progress_planner()->get_suggested_tasks_db()->get_post( $task_id ); - return array_values( $tasks ); - } + if ( $task ) { + \progress_planner()->get_suggested_tasks_db()->update_recommendation( $task->ID, [ 'post_status' => 'pending' ] ); - /** - * Delete a task. - * - * @param string $task_id The task ID. - * - * @return bool - */ - public function delete_task( $task_id ) { - $tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); - $modified = false; - foreach ( $tasks as $key => $task ) { - if ( $task['task_id'] === $task_id ) { - unset( $tasks[ $key ] ); - $modified = true; - break; + // Insert an activity. + $this->insert_activity( $task_id ); } } - - return $modified - ? \progress_planner()->get_settings()->set( 'tasks', $tasks ) - : false; } /** - * Mark a task as a given status. - * - * @param string $status The status. - * @param string $task_id The task ID. - * @param array $data The data. + * Handle the suggested task action. * - * @return bool + * @return void */ - public function mark_task_as( $status, $task_id, $data = [] ) { - $tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); - $tasks_changed = false; - foreach ( $tasks as $key => $task ) { - if ( $task['task_id'] !== $task_id ) { - continue; - } - - if ( 'completed' === $task['status'] && 'pending_celebration' === $status ) { - break; - } - - $tasks[ $key ]['status'] = $status; - $tasks_changed = true; - - if ( 'snoozed' === $status ) { - $tasks[ $key ]['time'] = \time() + $data['time']; - } - - break; + public function suggested_task_action() { + // Check the nonce. + if ( ! \check_ajax_referer( 'progress_planner', 'nonce', false ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid nonce.', 'progress-planner' ) ] ); } - if ( ! $tasks_changed ) { - return false; + if ( ! isset( $_POST['post_id'] ) || ! isset( $_POST['action_type'] ) ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Missing data.', 'progress-planner' ) ] ); } - $result = \progress_planner()->get_settings()->set( 'tasks', $tasks ); + $action = \sanitize_text_field( \wp_unslash( $_POST['action_type'] ) ); + $post_id = (string) \sanitize_text_field( \wp_unslash( $_POST['post_id'] ) ); + $task = \progress_planner()->get_suggested_tasks_db()->get_post( $post_id ); - // Fire an action when the task status is changed. - if ( true === $result ) { - do_action( 'progress_planner_task_status_changed', $task_id, $status ); + if ( ! $task ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Task not found.', 'progress-planner' ) ] ); } - return $result; - } + $updated = false; - /** - * Remove a task from a given status (sets it as `pending`). - * - * @param string $status The status. - * @param string $task_id The task ID. - * - * @return bool - */ - public function remove_task_from( $status, $task_id ) { - $tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); - $tasks_changed = false; - - foreach ( $tasks as $key => $task ) { - if ( $task['task_id'] !== $task_id ) { - continue; - } - - if ( ! isset( $task['status'] ) || $task['status'] !== $status ) { - return false; - } + switch ( $action ) { + case 'complete': + // Insert an activity. + $this->insert_activity( $task->task_id ); + $updated = true; + break; - $tasks[ $key ]['status'] = 'pending'; - $tasks_changed = true; + case 'delete': + $this->delete_activity( $task->task_id ); + $updated = true; + break; } - if ( ! $tasks_changed ) { - return false; + /** + * Allow other classes to react to the completion of a suggested task. + * + * @param string $post_id The post ID. + * @param bool $updated Whether the action was successful. + */ + \do_action( "progress_planner_ajax_task_{$action}", $post_id, $updated ); + + if ( ! $updated ) { + \wp_send_json_error( [ 'message' => \esc_html__( 'Not saved.', 'progress-planner' ) ] ); } - return \progress_planner()->get_settings()->set( 'tasks', $tasks ); + \wp_send_json_success( [ 'message' => \esc_html__( 'Saved.', 'progress-planner' ) ] ); } /** - * Transition a task from one status to another. + * Register a custom post type for suggested tasks. * - * @param string $task_id The task ID. - * @param string $old_status The old status. - * @param string $new_status The new status. - * @param array $data The data. - * - * @return bool + * @return void */ - public function transition_task_status( $task_id, $old_status, $new_status, $data = [] ) { - - $return_old_status = false; - $return_new_status = false; - - if ( $old_status ) { - $return_old_status = $this->remove_task_from( $old_status, $task_id ); - } + public function register_post_type() { + \register_post_type( + 'prpl_recommendations', + [ + 'label' => \__( 'Recommendations', 'progress-planner' ), + 'public' => false, + 'show_ui' => \apply_filters( 'progress_planner_tasks_show_ui', false ), + 'show_in_admin_bar' => \apply_filters( 'progress_planner_tasks_show_ui', false ), + 'show_in_rest' => true, + 'rest_controller_class' => \Progress_Planner\Rest\Recommendations_Controller::class, + 'supports' => [ 'title', 'editor', 'author', 'custom-fields', 'page-attributes' ], + 'rewrite' => false, + 'menu_icon' => 'dashicons-admin-tools', + 'menu_position' => 5, + 'hierarchical' => true, + 'exclude_from_search' => true, + 'publicly_queryable' => true, + ] + ); - if ( $new_status ) { - $return_new_status = $this->mark_task_as( $new_status, $task_id, $data ); + $rest_meta_fields = [ + 'prpl_points' => [ + 'type' => 'number', + 'single' => true, + 'show_in_rest' => true, + ], + 'prpl_task_id' => [ + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + ], + 'prpl_url' => [ + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + ], + 'prpl_url_target' => [ + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + ], + 'prpl_dismissable' => [ + 'type' => 'boolean', + 'single' => true, + 'show_in_rest' => true, + ], + 'prpl_snoozable' => [ + 'type' => 'boolean', + 'single' => true, + 'show_in_rest' => true, + ], + 'menu_order' => [ + 'type' => 'number', + 'single' => true, + 'show_in_rest' => true, + 'default' => 0, + ], + 'prpl_popover_id' => [ + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + ], + ]; + + foreach ( $rest_meta_fields as $key => $field ) { + \register_post_meta( + 'prpl_recommendations', + $key, + $field + ); } - - return $return_old_status && $return_new_status; } /** - * Mark a task as snoozed. + * Custom trash lifetime by post type. * - * @param string $task_id The task ID. - * @param string $duration The duration. + * @param int $days The number of days to keep in trash. + * @param \WP_Post $post The post. * - * @return bool + * @return int */ - public function snooze_task( $task_id, $duration ) { - - switch ( $duration ) { - case '1-month': - $time = \MONTH_IN_SECONDS; - break; - - case '3-months': - $time = 3 * \MONTH_IN_SECONDS; - break; - - case '6-months': - $time = 6 * \MONTH_IN_SECONDS; - break; - - case '1-year': - $time = \YEAR_IN_SECONDS; - break; - - case 'forever': - $time = \PHP_INT_MAX; - break; - - default: - $time = \WEEK_IN_SECONDS; - break; - } - - return $this->mark_task_as( 'snoozed', $task_id, [ 'time' => $time ] ); + public function change_trashed_posts_lifetime( $days, $post ) { + return 'prpl_recommendations' === $post->post_type ? 60 : $days; } /** - * Maybe unsnooze tasks. + * Register a custom taxonomies for suggested tasks. * * @return void */ - private function maybe_unsnooze_tasks() { - $tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); - $tasks_changed = false; - foreach ( $tasks as $key => $task ) { - if ( $task['status'] !== 'snoozed' ) { - continue; - } - - if ( isset( $task['time'] ) && $task['time'] < \time() ) { - if ( isset( $task['provider_id'] ) && 'user' === $task['provider_id'] ) { - $tasks[ $key ]['status'] = 'pending'; - unset( $tasks[ $key ]['time'] ); - } else { - unset( $tasks[ $key ] ); - } - $tasks_changed = true; - } - } - - if ( $tasks_changed ) { - \progress_planner()->get_settings()->set( 'tasks', $tasks ); + public function register_taxonomy() { + foreach ( [ + 'prpl_recommendations_category' => \__( 'Categories', 'progress-planner' ), + 'prpl_recommendations_provider' => \__( 'Providers', 'progress-planner' ), + ] as $taxonomy => $label ) { + \register_taxonomy( + $taxonomy, + [ 'prpl_recommendations' ], + [ + 'public' => false, + 'hierarchical' => false, + 'labels' => [ + 'name' => $label, + ], + 'show_ui' => \apply_filters( 'progress_planner_tasks_show_ui', false ), + 'show_admin_column' => false, + 'query_var' => true, + 'rewrite' => [ 'slug' => $taxonomy ], + 'show_in_rest' => true, + 'show_in_menu' => \apply_filters( 'progress_planner_tasks_show_ui', false ), + ] + ); } } /** - * Check if a task meets a condition. + * Filter the REST API tax query. * - * @param array $condition The condition. - * [ - * string 'type' The condition type. - * string 'task_id' The task id (optional, used for completed and snoozed conditions). - * array 'post_lengths' The post lengths (optional, used for snoozed-post-length condition). - * ]. + * @param array $args The arguments. + * @param \WP_REST_Request $request The request. * - * @return bool + * @return array */ - public function check_task_condition( $condition ) { - $parsed_condition = \wp_parse_args( - $condition, - [ - 'status' => '', - 'task_id' => '', - 'post_lengths' => [], - ] - ); - - if ( 'snoozed-post-length' === $parsed_condition['status'] ) { - if ( isset( $parsed_condition['post_lengths'] ) ) { - if ( ! \is_array( $parsed_condition['post_lengths'] ) ) { - $parsed_condition['post_lengths'] = [ $parsed_condition['post_lengths'] ]; - } - - $snoozed_tasks = $this->get_tasks_by( 'status', 'snoozed' ); - $snoozed_post_lengths = []; - - // Get the post lengths of the snoozed tasks. - foreach ( $snoozed_tasks as $task ) { - $data = $this->tasks_manager->get_data_from_task_id( $task['task_id'] ); // @phpstan-ignore-line method.nonObject - if ( isset( $data['category'] ) && 'create-post' === $data['category'] ) { - $key = true === $data['long'] ? 'long' : 'short'; - if ( ! isset( $snoozed_post_lengths[ $key ] ) ) { - $snoozed_post_lengths[ $key ] = true; - } - } - } + public function rest_api_tax_query( $args, $request ) { + $tax_query = []; + + // Include terms (matches any term in list). + if ( isset( $request['provider'] ) ) { + $tax_query[] = [ + 'taxonomy' => 'prpl_recommendations_provider', + 'field' => 'slug', + 'terms' => \explode( ',', $request['provider'] ), + 'operator' => 'IN', + ]; + } - // Check if the snoozed post lengths match the condition. - foreach ( $parsed_condition['post_lengths'] as $post_length ) { - if ( ! isset( $snoozed_post_lengths[ $post_length ] ) ) { - return false; - } - } + // Exclude terms. + if ( isset( $request['exclude_provider'] ) ) { + $tax_query[] = [ + 'taxonomy' => 'prpl_recommendations_provider', + 'field' => 'slug', + 'terms' => \explode( ',', $request['exclude_provider'] ), + 'operator' => 'NOT IN', + ]; + } - return true; - } + if ( ! empty( $tax_query ) ) { + $args['tax_query'] = $tax_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query } - foreach ( $this->get_tasks_by( 'status', $parsed_condition['status'] ) as $task ) { - if ( $task['task_id'] === $parsed_condition['task_id'] ) { - return true; - } + // Handle sorting parameters. + if ( isset( $request['filter']['orderby'] ) ) { + $args['orderby'] = \sanitize_sql_orderby( $request['filter']['orderby'] ); + } + if ( isset( $request['filter']['order'] ) ) { + $args['order'] = \in_array( \strtoupper( $request['filter']['order'] ), [ 'ASC', 'DESC' ], true ) + ? \strtoupper( $request['filter']['order'] ) + : 'ASC'; } - return false; + return $args; } /** - * Check if a task was completed. Task is considered completed if it was completed or pending celebration. + * Filter the REST API response. * - * @param string $task_id The task ID. + * @param \WP_REST_Response $response The response. + * @param \WP_Post $post The post. * - * @return bool + * @return \WP_REST_Response */ - public function was_task_completed( $task_id ) { - foreach ( \progress_planner()->get_settings()->get( 'tasks', [] ) as $task ) { - if ( ! isset( $task['task_id'] ) || $task['task_id'] !== $task_id ) { - continue; + public function rest_prepare_recommendation( $response, $post ) { + $provider_term = \wp_get_object_terms( $post->ID, 'prpl_recommendations_provider' ); + if ( $provider_term && ! \is_wp_error( $provider_term ) ) { + $provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( $provider_term[0]->slug ); + + if ( $provider ) { + // Link should be added during run time, since it is not added for users without required capability. + $response->data['meta']['prpl_url'] = $response->data['meta']['prpl_url'] && $provider->capability_required() + ? \esc_url( (string) $response->data['meta']['prpl_url'] ) + : ''; } - - return isset( $task['status'] ) && in_array( $task['status'], [ 'completed', 'pending_celebration' ], true ); } - return false; + return $response; } /** - * Update a task. + * Get the pending tasks in REST format. * - * @param string $task_id The task ID. - * @param array $data The data. + * @param array $args The arguments. * - * @return bool + * @return array */ - public function update_pending_task( $task_id, $data ) { - $tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); - $tasks_changed = false; - foreach ( $tasks as $key => $task ) { - if ( 'pending' !== $task['status'] || $task['task_id'] !== $task_id ) { - continue; - } + public function get_tasks_in_rest_format( array $args = [] ) { + $args = \wp_parse_args( + $args, + [ + 'post_status' => 'publish', + 'exclude_provider' => [], + 'include_provider' => [], + 'posts_per_page' => 0, + ] + ); - // Don't update the task_id. - if ( isset( $data['task_id'] ) ) { - unset( $data['task_id'] ); - } + // Get the max items per category. + $max_items_per_category = $this->get_max_items_per_category(); - // Update the task data except the 'task_id' key. - $tasks[ $key ] = array_merge( $tasks[ $key ], $data ); - $tasks_changed = true; + // Initialize the tasks array. + $tasks = []; - break; - } + // Get the tasks for each category. + foreach ( $max_items_per_category as $category_slug => $max_items ) { - if ( ! $tasks_changed ) { - return false; - } - return \progress_planner()->get_settings()->set( 'tasks', $tasks ); - } + // Skip excluded providers. + if ( ! empty( $args['exclude_provider'] ) && \in_array( $category_slug, $args['exclude_provider'], true ) ) { + continue; + } - /** - * Maybe complete a task. - * Primarly this is used for deeplinking, ie user is testing if the emails are working - * He gets an email with a link which automatically completes the task. - * - * @return void - */ - public function maybe_complete_task() { - if ( ! \progress_planner()->is_on_progress_planner_dashboard_page() || ! isset( $_GET['prpl_complete_task'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - return; - } + // Skip not included providers. + if ( ! empty( $args['include_provider'] ) && ! \in_array( $category_slug, $args['include_provider'], true ) ) { + continue; + } - $task_id = \sanitize_text_field( \wp_unslash( $_GET['prpl_complete_task'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - if ( ! $task_id ) { - return; - } + $category_tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( + [ + 'category' => $category_slug, + 'posts_per_page' => 0 < $args['posts_per_page'] ? $args['posts_per_page'] : $max_items, + 'post_status' => $args['post_status'], + ] + ); - if ( ! $this->was_task_completed( $task_id ) ) { - $this->mark_task_as( 'pending_celebration', $task_id ); + if ( ! empty( $category_tasks ) ) { + $tasks[ $category_slug ] = []; - // Insert an activity. - $this->insert_activity( $task_id ); + foreach ( $category_tasks as $task ) { + $tasks[ $category_slug ][] = $task->get_rest_formatted_data(); + } + } } + + return $tasks; } /** - * Handle the suggested task action. + * Get the max items per category. * - * @return void + * @return array */ - public function suggested_task_action() { - // Check the nonce. - if ( ! \check_ajax_referer( 'progress_planner', 'nonce', false ) ) { - \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid nonce.', 'progress-planner' ) ] ); - } - - if ( ! isset( $_POST['task_id'] ) || ! isset( $_POST['action_type'] ) ) { - \wp_send_json_error( [ 'message' => \esc_html__( 'Missing data.', 'progress-planner' ) ] ); - } - - $action = \sanitize_text_field( \wp_unslash( $_POST['action_type'] ) ); - $task_id = (string) \sanitize_text_field( \wp_unslash( $_POST['task_id'] ) ); - - switch ( $action ) { - case 'complete': - // Mark the task as completed. - $this->mark_task_as( 'completed', $task_id ); - - // Insert an activity. - $this->insert_activity( $task_id ); - - $updated = true; - break; - - case 'pending': - $this->mark_task_as( 'pending', $task_id ); - $updated = true; - $this->delete_activity( $task_id ); - break; - - case 'snooze': - $duration = isset( $_POST['duration'] ) ? \sanitize_text_field( \wp_unslash( $_POST['duration'] ) ) : ''; - $updated = $this->snooze_task( $task_id, $duration ); - break; - - case 'delete': - $updated = $this->delete_task( $task_id ); - $this->delete_activity( $task_id ); - break; + public function get_max_items_per_category() { + // Set max items per category. + $max_items_per_category = []; + $provider_categories = \get_terms( + [ + 'taxonomy' => 'prpl_recommendations_category', + 'hide_empty' => false, + ] + ); - default: - \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid action.', 'progress-planner' ) ] ); + if ( ! empty( $provider_categories ) && ! \is_wp_error( $provider_categories ) ) { + $content_review_category = ( new Content_Review() )->get_provider_category(); + foreach ( $provider_categories as $provider_category ) { + $max_items_per_category[ $provider_category->slug ] = $provider_category->slug === $content_review_category ? 2 : 1; + } } - /** - * Allow other classes to react to the completion of a suggested task. - * - * @param string $task_id The task ID. - * @param bool $updated Whether the action was successful. - */ - \do_action( "progress_planner_ajax_task_{$action}", $task_id, $updated ); - - if ( ! $updated ) { - \wp_send_json_error( [ 'message' => \esc_html__( 'Failed to save.', 'progress-planner' ) ] ); + // This should never happen, but just in case - user tasks are displayed in different widget. + if ( isset( $max_items_per_category['user'] ) ) { + $max_items_per_category['user'] = 100; } - \wp_send_json_success( [ 'message' => \esc_html__( 'Saved.', 'progress-planner' ) ] ); + return \apply_filters( 'progress_planner_suggested_tasks_max_items_per_category', $max_items_per_category ); } } diff --git a/classes/class-todo.php b/classes/class-todo.php index b847a3ddf..9f716c0d4 100644 --- a/classes/class-todo.php +++ b/classes/class-todo.php @@ -7,8 +7,6 @@ namespace Progress_Planner; -use Progress_Planner\Suggested_Tasks\Task_Factory; - /** * Todo class. */ @@ -20,299 +18,84 @@ class Todo { * @return void */ public function __construct() { - \add_action( 'wp_ajax_progress_planner_save_user_suggested_task', [ $this, 'save_user_suggested_task' ] ); - \add_action( 'wp_ajax_progress_planner_save_suggested_user_tasks_order', [ $this, 'save_suggested_user_tasks_order' ] ); - - \add_action( 'progress_planner_task_status_changed', [ $this, 'remove_order_from_completed_user_task' ], 10, 2 ); + // Wait for the CPT to be registered. + \add_action( 'init', [ $this, 'maybe_change_first_item_points_on_monday' ] ); - $this->maybe_change_first_item_points_on_monday(); + // Handle user tasks creation. + \add_action( 'rest_after_insert_prpl_recommendations', [ $this, 'handle_creating_user_task' ], 10, 3 ); } /** - * Remove the order from a completed user task. - * - * @param string $task_id The task ID. - * @param string $status The status. + * Maybe change the points of the first item in the todo list on Monday. * * @return void */ - public function remove_order_from_completed_user_task( $task_id, $status ) { + public function maybe_change_first_item_points_on_monday() { + // Ordered by menu_order ASC, by default. + $pending_items = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( + [ + 'provider_id' => 'user', + 'post_status' => 'publish', + ] + ); - // Bail if the task is not completed. - if ( 'completed' !== $status ) { + // Bail if there are no items. + if ( ! \count( $pending_items ) ) { return; } - $task = Task_Factory::create_task_from( 'id', $task_id ); + $transient_name = 'todo_points_change_on_monday'; + $next_update = \progress_planner()->get_utils__cache()->get( $transient_name ); - // Bail if the task is not a user task. - if ( 'user' !== $task->get_provider_id() ) { + if ( false !== $next_update && $next_update > \time() ) { return; } - $task_changed = false; - $tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); - foreach ( $tasks as $key => $task ) { - if ( $task['task_id'] === $task_id ) { - unset( $tasks[ $key ]['order'] ); - $task_changed = true; - break; - } - } - - if ( $task_changed ) { - \progress_planner()->get_settings()->set( 'tasks', $tasks ); - } - } - - /** - * Get the pending todo list items. - * - * @return array - */ - public function get_items() { - return array_merge( $this->get_pending_items(), $this->get_completed_items() ); - } - - /** - * Get the completed todo list items. - * - * @return array - */ - public function get_completed_items() { - $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'provider_id', 'user' ); - - $items = []; - foreach ( $tasks as $task ) { - if ( 'completed' === $task['status'] ) { - $items[] = array_merge( - $task, - [ - 'dismissable' => true, - 'snoozable' => false, - ] - ); - } - } - - return $items; - } - - /** - * Get the pending todo list items. - * - * @return array - */ - public function get_pending_items() { - $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'provider_id', 'user' ); - $items = []; - $max_order = 0; - - // Get the maximum order value from the $tasks array. - foreach ( $tasks as $task ) { - if ( 'pending' === $task['status'] && isset( $task['order'] ) && $task['order'] > $max_order ) { - $max_order = $task['order']; - } - } - - foreach ( $tasks as $task ) { - // Skip non-pending tasks. - if ( 'pending' !== $task['status'] ) { - continue; - } + $next_monday = new \DateTime( 'monday next week' ); - if ( ! isset( $task['order'] ) ) { - $task['order'] = $max_order + 1; - ++$max_order; - } - $items[] = array_merge( - $task, - [ - 'dismissable' => true, - 'snoozable' => false, - ] + // Reset the points of all the tasks, except for the first one in the todo list. + foreach ( $pending_items as $task ) { + \progress_planner()->get_suggested_tasks_db()->update_recommendation( + $task->ID, + [ 'points' => $task->ID === $pending_items[0]->ID ? 1 : 0 ] ); } - // Order the items by the order value. - usort( - $items, - function ( $a, $b ) { - return $a['order'] - $b['order']; - } - ); - - return $items; + \progress_planner()->get_utils__cache()->set( $transient_name, $next_monday->getTimestamp(), WEEK_IN_SECONDS ); } /** - * Save a user suggested task. + * Handle the creation of the first user task. + * We need separate hook, since at the time 'maybe_change_first_item_points_on_monday' is called there might not be any tasks yet. + * TODO: Revisit when we see how we handle completed user tasks. + * + * @param \WP_Post $post Inserted or updated post object. + * @param \WP_REST_Request $request Request object. + * @param bool $creating True when creating a post, false when updating. * * @return void */ - public function save_user_suggested_task() { - // Check the nonce. - if ( ! \check_ajax_referer( 'progress_planner', 'nonce', false ) ) { - \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid nonce.', 'progress-planner' ) ] ); - } - - $task_id = isset( $_POST['task']['task_id'] ) ? \sanitize_text_field( \wp_unslash( $_POST['task']['task_id'] ) ) : ''; - if ( ! $task_id ) { - \wp_send_json_error( [ 'message' => \esc_html__( 'Missing task ID.', 'progress-planner' ) ] ); - } - - $tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); - $title = isset( $_POST['task']['title'] ) ? \sanitize_text_field( \wp_unslash( $_POST['task']['title'] ) ) : ''; + public function handle_creating_user_task( $post, $request, $creating ) { - // Check if the task already exists (this is the update case). - $task_index = false; - foreach ( $tasks as $key => $task ) { - if ( $task['task_id'] === $task_id ) { - $task_index = $key; - break; - } + if ( ! $creating || ! \has_term( 'user', 'prpl_recommendations_provider', $post->ID ) ) { + return; } - // Default value. - $task_points = 0; - - // We're creating a new task. - if ( false === $task_index ) { - $task_points = $this->calc_points_for_new_task(); - $tasks[] = [ - 'task_id' => $task_id, - 'provider_id' => 'user', - 'category' => 'user', - 'status' => 'pending', - 'title' => $title, - 'points' => $task_points, - ]; - } else { - $tasks[ $task_index ]['title'] = $title; - } + // Add task_id to the post. + \update_post_meta( $post->ID, 'prpl_task_id', 'user-' . $post->ID ); - \progress_planner()->get_settings()->set( 'tasks', $tasks ); - \wp_send_json_success( + // If it is first task ever created, it should be golden. + $pending_items = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ - 'message' => \esc_html__( 'Saved.', 'progress-planner' ), - 'points' => $task_points, // We're using it when adding the new task to the todo list. + 'provider_id' => 'user', ] ); - } - - /** - * Save the order of suggested user tasks. - * - * @return void - */ - public function save_suggested_user_tasks_order() { - // Check the nonce. - if ( ! \check_ajax_referer( 'progress_planner', 'nonce', false ) ) { - \wp_send_json_error( [ 'message' => \esc_html__( 'Invalid nonce.', 'progress-planner' ) ] ); - } - - $tasks = isset( $_POST['tasks'] ) ? \sanitize_text_field( \wp_unslash( $_POST['tasks'] ) ) : ''; - if ( ! $tasks ) { - \wp_send_json_error( [ 'message' => \esc_html__( 'Missing tasks.', 'progress-planner' ) ] ); - } - - $tasks = \explode( ',', $tasks ); - $saved_tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); - - foreach ( $saved_tasks as $key => $task ) { - if ( in_array( $task['task_id'], $tasks, true ) ) { - $saved_tasks[ $key ]['order'] = array_search( $task['task_id'], $tasks, true ); - } - } - - \progress_planner()->get_settings()->set( 'tasks', $saved_tasks ); - } - - /** - * Get the points for a new task. - * - * @return int - */ - public function calc_points_for_new_task() { - $items = $this->get_items(); - - // If this is the first user task ever, return 1. - if ( ! count( $items ) ) { - return 1; - } - - // Get the task IDs from the todos. - $task_ids = array_column( $items, 'task_id' ); - - // Get the completed activities for this week that are in the todos. - $activities = array_filter( - \progress_planner()->get_activities__query()->query_activities( - [ - 'start_date' => new \DateTime( 'monday this week' ), - 'end_date' => new \DateTime( 'sunday this week' ), - 'category' => 'suggested_task', - 'type' => 'completed', - ] - ), - function ( $activity ) use ( $task_ids ) { - return in_array( $activity->data_id, $task_ids, true ); - } - ); - - // If there are completed todos this week, we already have set the golden task and it was completed. - if ( count( $activities ) ) { - return 0; - } - - // Check if there are already pending user tasks with a points value other than 0. - foreach ( $items as $item ) { - if ( 'pending' === $item['status'] && isset( $item['points'] ) && $item['points'] !== 0 ) { - return 0; - } - } - return 1; - } - - /** - * Maybe change the points of the first item in the todo list on Monday. - * - * @return void - */ - public function maybe_change_first_item_points_on_monday() { - $pending_items = $this->get_pending_items(); - - // Bail if there are no items. - if ( ! count( $pending_items ) ) { - return; - } - - $transient_name = 'todo_points_change_on_monday'; - $next_update = \progress_planner()->get_utils__cache()->get( $transient_name ); - - if ( false !== $next_update && $next_update > time() ) { + // If this is the first task created, it should be golden. + if ( 1 === \count( $pending_items ) && $pending_items[0]->ID === $post->ID ) { + $this->maybe_change_first_item_points_on_monday(); return; } - - $next_monday = new \DateTime( 'monday next week' ); - - // Get the task IDs from the todos. - $task_ids = array_column( $pending_items, 'task_id' ); - - // Get the tasks. - $tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); - - // Reset the points of all the tasks, except for the first one in the todo list. - foreach ( $tasks as $key => $task ) { - if ( 'user' === $task['provider_id'] && 'pending' === $task['status'] ) { - $tasks[ $key ]['points'] = $tasks[ $key ]['task_id'] === $task_ids[0] ? 1 : 0; - } - } - - // Save the tasks. - \progress_planner()->get_settings()->set( 'tasks', $tasks ); - - \progress_planner()->get_utils__cache()->set( $transient_name, $next_monday->getTimestamp(), WEEK_IN_SECONDS ); } } // phpcs:enable Generic.Commenting.Todo diff --git a/classes/goals/class-goal-recurring.php b/classes/goals/class-goal-recurring.php index 961758738..d70fc2de8 100644 --- a/classes/goals/class-goal-recurring.php +++ b/classes/goals/class-goal-recurring.php @@ -125,9 +125,9 @@ public function get_occurences() { } // If the last range ends before today, add a new range. - if ( (int) gmdate( 'Ymd' ) > (int) end( $ranges )['end_date']->format( 'Ymd' ) ) { + if ( (int) \gmdate( 'Ymd' ) > (int) \end( $ranges )['end_date']->format( 'Ymd' ) ) { $ranges[] = \progress_planner()->get_utils__date()->get_range( - end( $ranges )['end_date'], + \end( $ranges )['end_date'], new \DateTime( 'tomorrow' ) ); } @@ -163,7 +163,7 @@ public function get_streak() { $evaluation = $occurence->evaluate(); if ( $evaluation ) { ++$streak_nr; - $max_streak = max( $max_streak, $streak_nr ); + $max_streak = \max( $max_streak, $streak_nr ); continue; } diff --git a/classes/rest/class-recommendations-controller.php b/classes/rest/class-recommendations-controller.php new file mode 100644 index 000000000..c8028faed --- /dev/null +++ b/classes/rest/class-recommendations-controller.php @@ -0,0 +1,46 @@ +get_json_params(); - - $data = []; - - // Get the number of pending updates. - $data['pending_updates'] = \wp_get_update_data()['counts']['total']; - - // Get number of content from any public post-type, published in the past week. - $data['weekly_posts'] = count( - \get_posts( - [ - 'post_status' => 'publish', - 'post_type' => 'post', - 'date_query' => [ [ 'after' => '1 week ago' ] ], - 'posts_per_page' => 10, - ] - ) - ); - - // Get the number of activities in the past week. - $data['activities'] = count( - \progress_planner()->get_activities__query()->query_activities( - [ - 'start_date' => new \DateTime( '-7 days' ), - ] - ) - ); - - // Get the website activity score. - $activity_score = new Activity_Scores(); - $data['website_activity'] = [ - 'score' => $activity_score->get_score(), - 'checklist' => $activity_score->get_checklist_results(), - ]; - - // Get the badges. - $badges = array_merge( - \progress_planner()->get_badges()->get_badges( 'content' ), - \progress_planner()->get_badges()->get_badges( 'maintenance' ), - \progress_planner()->get_badges()->get_badges( 'monthly_flat' ) - ); - - $data['badges'] = []; - foreach ( $badges as $badge ) { - $data['badges'][ $badge->get_id() ] = array_merge( - [ - 'id' => $badge->get_id(), - 'name' => $badge->get_name(), - ], - $badge->progress_callback() - ); - } - - $data['latest_badge'] = \progress_planner()->get_badges()->get_latest_completed_badge(); - - $scores = \progress_planner()->get_ui__chart()->get_chart_data( - [ - 'items_callback' => function ( $start_date, $end_date ) { - return \progress_planner()->get_activities__query()->query_activities( - [ - 'start_date' => $start_date, - 'end_date' => $end_date, - ] - ); - }, - 'dates_params' => [ - 'start_date' => \DateTime::createFromFormat( 'Y-m-d', \gmdate( 'Y-m-01' ) )->modify( '-6 months' ), - 'end_date' => new \DateTime(), - 'frequency' => 'monthly', - 'format' => 'M', - ], - 'count_callback' => function ( $activities, $date ) { - $score = 0; - foreach ( $activities as $activity ) { - $score += $activity->get_points( $date ); - } - return $score * 100 / Base::SCORE_TARGET; - }, - 'normalized' => true, - 'max' => 100, - ] - ); - - $data['scores'] = []; - foreach ( $scores as $item ) { - $data['scores'][] = [ - 'label' => $item['label'], - 'value' => $item['score'], - ]; - } - - // The website URL. - $data['website'] = \home_url(); - - // Timezone offset. - $data['timezone_offset'] = \wp_timezone()->getOffset( new \DateTime( 'midnight' ) ) / 3600; - $ravis_recommendations = \progress_planner()->get_suggested_tasks()->get_pending_tasks_with_details(); - $data['recommendations'] = []; - foreach ( $ravis_recommendations as $recommendation ) { - $r = [ - 'id' => $recommendation['task_id'], - 'title' => $recommendation['title'], - 'url' => isset( $recommendation['url'] ) ? $recommendation['url'] : '', - 'provider_id' => $recommendation['provider_id'], - ]; - - if ( 'user' === $recommendation['provider_id'] ) { - $r['points'] = isset( $recommendation['points'] ) ? $recommendation['points'] : 0; - } - $data['recommendations'][] = $r; - } - - $data['plugin_url'] = \esc_url( \get_admin_url( null, 'admin.php?page=progress-planner' ) ); + public function get_stats() { + $system_status = new \Progress_Planner\Utils\System_Status(); - return new \WP_REST_Response( $data ); + return new \WP_REST_Response( $system_status->get_system_status() ); } /** @@ -186,7 +72,7 @@ public function get_stats( \WP_REST_Request $request ) { * @return bool */ public function validate_token( $token ) { - $token = str_replace( 'token/', '', $token ); + $token = \str_replace( 'token/', '', $token ); if ( \progress_planner()->is_pro_site() && $token === \get_option( 'progress_planner_pro_license_key' ) ) { return true; } diff --git a/classes/rest/class-tasks.php b/classes/rest/class-tasks.php index 99da1589d..ba0bdb22b 100644 --- a/classes/rest/class-tasks.php +++ b/classes/rest/class-tasks.php @@ -54,7 +54,7 @@ public function register_rest_endpoint() { * @return bool */ public function validate_token( $token ) { - $token = str_replace( 'token/', '', $token ); + $token = \str_replace( 'token/', '', $token ); if ( $token === \get_option( 'progress_planner_test_token', '' ) ) { return true; @@ -78,8 +78,13 @@ public function validate_token( $token ) { */ public function get_tasks() { - $tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); + // Collection of task objects. + $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'post_status' => [ 'publish', 'trash', 'draft', 'future', 'pending' ] ] ); + $tasks_to_return = []; - return new \WP_REST_Response( $tasks ); + foreach ( $tasks as $task ) { + $tasks_to_return[] = $task->get_data(); + } + return new \WP_REST_Response( $tasks_to_return ); } } diff --git a/classes/suggested-tasks/class-task-factory.php b/classes/suggested-tasks/class-task-factory.php index c70ef810b..f5672bcc3 100644 --- a/classes/suggested-tasks/class-task-factory.php +++ b/classes/suggested-tasks/class-task-factory.php @@ -17,117 +17,14 @@ class Task_Factory { /** * Get the task. * - * @param string $param The parameter, 'id' or 'data'. - * @param mixed $value The task ID or task data. + * @param mixed $value The task ID or task data. * * @return \Progress_Planner\Suggested_Tasks\Task */ - public static function create_task_from( $param, $value = null ): Task { + public static function create_task_from_id( $value = null ): Task { + $task = \progress_planner()->get_suggested_tasks_db()->get_post( $value ); - // If we have task data, return it. - if ( 'data' === $param && is_array( $value ) ) { - return new Task( $value ); - } - - if ( 'id' === $param && is_string( $value ) ) { - // We should have all the data saved in the database. - $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $value ); - - // If we have the task data, return it. - if ( isset( $tasks[0] ) ) { - return new Task( $tasks[0] ); - } - - /* - We're here in following cases: - * - Legacy tasks, happens during v1.1.1 update, where we parsed task data from the task_id. - */ - return self::parse_task_data_from_task_id( $value ); - } - - return new Task( [] ); - } - - /** - * Legacy function for parsing task data from task ID. - * - * @param string $task_id The task ID. - * - * @return \Progress_Planner\Suggested_Tasks\Task - */ - public static function parse_task_data_from_task_id( $task_id ) { - $data = []; - - // Parse simple format, e.g. 'update-core-202449' or "hello-world". - if ( ! str_contains( $task_id, '|' ) ) { - - $last_pos = strrpos( $task_id, '-' ); - - // Check if the task ID ends with a '-12345' or not, if not that would be mostly one time tasks. - if ( $last_pos === false || ! preg_match( '/-\d+$/', $task_id ) ) { - - $task_provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( $task_id ); - - return new Task( - [ - 'task_id' => $task_id, - 'category' => $task_provider ? $task_provider->get_provider_category() : '', - 'provider_id' => $task_provider ? $task_provider->get_provider_id() : '', - ] - ); - } - - // Repetitive tasks (update-core-202449). - $task_provider_id = substr( $task_id, 0, $last_pos ); - - // Check for legacy create-post task_id, old task_ids were migrated to create-post-short' or 'create-post-long' (since we had 2 such tasks per week). - if ( 'create-post-short' === $task_provider_id || 'create-post-long' === $task_provider_id ) { - $task_provider_id = 'create-post'; - } - - $task_provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( $task_provider_id ); - - return new Task( - [ - 'task_id' => $task_id, - 'category' => $task_provider ? $task_provider->get_provider_category() : '', - 'provider_id' => $task_provider ? $task_provider->get_provider_id() : '', - 'date' => substr( $task_id, $last_pos + 1 ), - ] - ); - } - - // Legacy piped format. - $data = [ 'task_id' => $task_id ]; - - // Parse detailed (piped) format (date/202510|long/1|provider_id/create-post). - $parts = \explode( '|', $task_id ); - foreach ( $parts as $part ) { - $part = \explode( '/', $part ); - if ( 2 !== \count( $part ) ) { - continue; - } - // Date should be a string, not a number. - $data[ $part[0] ] = ( 'date' !== $part[0] && \is_numeric( $part[1] ) ) - ? (int) $part[1] - : $part[1]; - } - \ksort( $data ); - - // Convert (int) 1 and (int) 0 to (bool) true and (bool) false. - if ( isset( $data['long'] ) ) { - $data['long'] = (bool) $data['long']; - } - if ( isset( $data['type'] ) && ! isset( $data['provider_id'] ) ) { - $data['provider_id'] = $data['type']; - unset( $data['type'] ); - } - - if ( isset( $data['provider_id'] ) ) { - $task_provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( $data['provider_id'] ); // @phpstan-ignore-line - $data['category'] = $task_provider ? $task_provider->get_provider_category() : ''; - } - - return new Task( $data ); + // If we have the task data, return it. + return $task ? $task : new Task( [] ); } } diff --git a/classes/suggested-tasks/class-task.php b/classes/suggested-tasks/class-task.php index 04fd2ed13..d1cfe4a19 100644 --- a/classes/suggested-tasks/class-task.php +++ b/classes/suggested-tasks/class-task.php @@ -9,19 +9,40 @@ /** * Task abstract class. + * + * @property int $ID The task ID + * @property string $post_status The task status + * @property string $post_title The task title + * @property string $post_date The task date + * @property \stdClass|null $provider The task provider object with slug property + * @property string $task_id The task identifier + * @property string $provider_id The provider identifier + * @property string $category The task category + * @property int $priority The task priority (0-100, 0 being highest and 100 being lowest). + * @property int $points The task points + * @property bool $dismissable Whether the task is dismissable + * @property string $url The task URL + * @property string $url_target The task URL target + * @property string $description The task description + * @property array $data The task data array + * @property int|null $target_post_id The target post ID for the task + * @property int|null $target_term_id The target term ID for the task + * @property string|null $target_taxonomy The target taxonomy for the task + * @property string|null $target_term_name The target term name for the task + * @property string|null $date The task date in YW format (year-week) */ class Task { /** * The task data. * - * @var array + * @var array */ protected array $data; /** * Constructor. * - * @param array $data The task data. + * @param array $data The task data. */ public function __construct( array $data = [] ) { $this->data = $data; @@ -30,21 +51,90 @@ public function __construct( array $data = [] ) { /** * Get the task data. * - * @return array + * @return array */ - public function get_data() { + public function get_data(): array { return $this->data; } /** * Set the task data. * - * @param array $data The task data. + * @param array $data The task data. + * + * @return void + */ + public function set_data( array $data ): void { + $this->data = $data; + } + + /** + * Update the task data. + * + * @param array $data The task data. * * @return void */ - public function set_data( array $data ) { + public function update( array $data ): void { $this->data = $data; + + // Update only if the task is already saved in the database. + if ( $this->ID ) { + \progress_planner()->get_suggested_tasks_db()->update_recommendation( $this->ID, $this->data ); + } + } + + /** + * Delete the task. + * + * @return void + */ + public function delete(): void { + $this->data = []; + // Delete only if the task is already saved in the database. + if ( $this->ID ) { + \progress_planner()->get_suggested_tasks_db()->delete_recommendation( $this->ID ); + } + } + + /** + * Check if the task is snoozed. + * + * @return bool + */ + public function is_snoozed(): bool { + return isset( $this->data['post_status'] ) && 'future' === $this->data['post_status']; + } + + /** + * Get the snoozed until date. + * + * @return \DateTime|null|false + */ + public function snoozed_until() { + return isset( $this->data['post_date'] ) ? \DateTime::createFromFormat( 'Y-m-d H:i:s', $this->data['post_date'] ) : null; + } + + /** + * Check if the task is completed. + * + * @return bool + */ + public function is_completed(): bool { + return isset( $this->data['post_status'] ) && \in_array( $this->data['post_status'], [ 'trash', 'pending' ], true ); + } + + /** + * Set the task status to pending. + * + * @return bool + */ + public function celebrate(): bool { + if ( ! $this->ID ) { + return false; + } + + return \progress_planner()->get_suggested_tasks_db()->update_recommendation( $this->ID, [ 'post_status' => 'pending' ] ); } /** @@ -52,33 +142,76 @@ public function set_data( array $data ) { * * @return string */ - public function get_provider_id() { - return $this->data['provider_id'] ?? ''; + public function get_provider_id(): string { + return $this->data['provider']->slug ?? ''; } /** - * Get the provider ID. + * Get the category. + * + * @return string + */ + public function get_category(): string { + return $this->data['category']->slug ?? ''; + } + + /** + * Get the task ID. * * @return string */ - public function get_task_id() { + public function get_task_id(): string { return $this->data['task_id'] ?? ''; } /** - * Get the provider ID. + * Magic getter. + * + * @param string $key The key. + * + * @return mixed + */ + public function __get( string $key ) { + return $this->data[ $key ] ?? null; + } + + /** + * Get the REST formatted data. + * + * @param int|null $post_id The post ID. * * @return array */ - public function get_task_details() { - $task_provider_id = $this->get_provider_id(); - $task_id = $this->get_task_id(); + public function get_rest_formatted_data( $post_id = null ): array { + if ( ! $post_id ) { + $post_id = $this->ID; + } - $task_provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( $task_provider_id ); - if ( ! $task_provider ) { + $post = \get_post( $post_id ); + if ( ! $post ) { return []; } - return $task_provider->get_task_details( $task_id ); + // Make sure WP_REST_Posts_Controller is loaded. + if ( ! \class_exists( 'WP_REST_Posts_Controller' ) ) { + require_once ABSPATH . 'wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php'; // @phpstan-ignore requireOnce.fileNotFound + } + + // Make sure WP_REST_Request is loaded. + if ( ! \class_exists( 'WP_REST_Request' ) ) { + require_once ABSPATH . 'wp-includes/rest-api/class-wp-rest-request.php'; // @phpstan-ignore requireOnce.fileNotFound + } + + // Use the appropriate controller for the post type. + $controller = new \WP_REST_Posts_Controller( $post->post_type ); + + // Build dummy request object. + $request = new \WP_REST_Request(); + $request->set_param( 'context', 'view' ); + + // Get formatted response. + $response = $controller->prepare_item_for_response( $post, $request ); + + return $response->get_data(); } } diff --git a/classes/suggested-tasks/class-tasks-interface.php b/classes/suggested-tasks/class-tasks-interface.php index 88e94e83d..53ddf060c 100644 --- a/classes/suggested-tasks/class-tasks-interface.php +++ b/classes/suggested-tasks/class-tasks-interface.php @@ -36,29 +36,27 @@ public function get_tasks_to_inject(); /** * Evaluate a task. * - * @param string $task The task ID. + * @param string $task_id The task id. * - * @return bool + * @return \Progress_Planner\Suggested_Tasks\Task|false */ - public function evaluate_task( $task ); + public function evaluate_task( $task_id ); /** * Get the task details. * - * @param string $task_id The task ID. + * @param array $task_data Optional data to include in the task. * * @return array */ - public function get_task_details( $task_id = '' ); + public function get_task_details( $task_data = [] ); /** - * Get the task details. - * - * @param string $task_id The task ID. + * Get the task link setting. * * @return array */ - public function get_data_from_task_id( $task_id ); + public function get_link_setting(); /** * Get the provider category. @@ -89,4 +87,11 @@ public function capability_required(); * @return bool */ public function is_task_relevant(); + + /** + * Check if the task is a repetitive task. + * + * @return bool + */ + public function is_repetitive(); } diff --git a/classes/suggested-tasks/class-tasks-manager.php b/classes/suggested-tasks/class-tasks-manager.php index 21d958022..5c5bae0b2 100644 --- a/classes/suggested-tasks/class-tasks-manager.php +++ b/classes/suggested-tasks/class-tasks-manager.php @@ -7,8 +7,6 @@ namespace Progress_Planner\Suggested_Tasks; -use Progress_Planner\Suggested_Tasks\Task_Factory; - use Progress_Planner\Suggested_Tasks\Providers\Core_Update; use Progress_Planner\Suggested_Tasks\Providers\Content_Create; use Progress_Planner\Suggested_Tasks\Providers\Content_Review; @@ -32,6 +30,7 @@ use Progress_Planner\Suggested_Tasks\Providers\Fewer_Tags; use Progress_Planner\Suggested_Tasks\Providers\Remove_Terms_Without_Posts; use Progress_Planner\Suggested_Tasks\Providers\Update_Term_Description; +use Progress_Planner\Suggested_Tasks\Providers\Collaborator; /** * Tasks_Manager class. @@ -49,7 +48,6 @@ class Tasks_Manager { * Constructor. */ public function __construct() { - // Instantiate task providers. $this->task_providers = [ new Content_Create(), @@ -73,6 +71,7 @@ public function __construct() { new Remove_Terms_Without_Posts(), new Fewer_Tags(), new Update_Term_Description(), + new Collaborator(), ]; // Add the plugin integration. @@ -91,7 +90,6 @@ public function __construct() { * @return void */ public function add_plugin_integration() { - // Yoast SEO integration. new Add_Yoast_Providers(); } @@ -102,7 +100,6 @@ public function add_plugin_integration() { * @return void */ public function init() { - /** * Filter the task providers, 3rd party providers are added here as well. * @@ -113,8 +110,8 @@ public function init() { // Now when all are instantiated, initialize them. foreach ( $this->task_providers as $key => $task_provider ) { if ( ! $task_provider instanceof Tasks_Interface ) { - error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log - sprintf( + \error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + \sprintf( 'Task provider %1$s is not an instance of %2$s', $task_provider->get_provider_id(), Tasks_Interface::class @@ -129,8 +126,7 @@ public function init() { $task_provider->init(); } - // Inject tasks. - \add_filter( 'progress_planner_suggested_tasks_items', [ $this, 'inject_tasks' ] ); + $this->inject_tasks(); // Add the onboarding task providers. \add_filter( 'prpl_onboarding_task_providers', [ $this, 'add_onboarding_task_providers' ] ); @@ -144,9 +140,7 @@ public function init() { * @return array */ public function add_onboarding_task_providers( $task_providers ) { - foreach ( $this->task_providers as $task_provider ) { - if ( $task_provider->is_onboarding_task() ) { $task_providers[] = $task_provider->get_provider_id(); } @@ -164,9 +158,9 @@ public function add_onboarding_task_providers( $task_providers ) { * @return \Progress_Planner\Suggested_Tasks\Tasks_Interface|null */ public function __call( $name, $arguments ) { - if ( 0 === strpos( $name, 'get_' ) ) { - $provider_type = substr( $name, 4 ); // Remove 'get_' prefix. - $provider_type = str_replace( '_', '-', strtolower( $provider_type ) ); // Transform 'update_core' to 'update-core'. + if ( 0 === \strpos( $name, 'get_' ) ) { + $provider_type = \substr( $name, 4 ); // Remove 'get_' prefix. + $provider_type = \str_replace( '_', '-', \strtolower( $provider_type ) ); // Transform 'update_core' to 'update-core'. return $this->get_task_provider( $provider_type ); } @@ -203,59 +197,27 @@ public function get_task_provider( $provider_id ) { /** * Inject tasks. * - * @param array $tasks The tasks. - * - * @return array + * @return void */ - public function inject_tasks( $tasks ) { - $provider_tasks = []; - $tasks_to_inject = []; - + public function inject_tasks() { // Loop through all registered task providers and inject their tasks. foreach ( $this->task_providers as $provider_instance ) { - $provider_tasks = \array_merge( $provider_tasks, $provider_instance->get_tasks_to_inject() ); - } - - // Add the tasks to the pending tasks option, it will not add duplicates. - foreach ( $provider_tasks as $task ) { - - // Skip the task if it was completed. - if ( true === \progress_planner()->get_suggested_tasks()->was_task_completed( $task['task_id'] ) ) { - continue; - } - - $tasks_to_inject[] = $task; - $this->add_pending_task( $task ); + // WIP, get_tasks_to_inject() is injecting tasks. + $provider_instance->get_tasks_to_inject(); } - - return \array_merge( $tasks, $tasks_to_inject ); } /** * Evaluate tasks stored in the option. * - * @return array + * @return \Progress_Planner\Suggested_Tasks\Task[] */ - public function evaluate_tasks() { - $tasks = (array) \progress_planner()->get_suggested_tasks()->get_tasks_by( 'status', 'pending' ); + public function evaluate_tasks(): array { + $tasks = (array) \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'post_status' => 'publish' ] ); $completed_tasks = []; - foreach ( $tasks as $task_data ) { - if ( ! isset( $task_data['task_id'] ) ) { - continue; - } - - $task_id = $task_data['task_id']; - - // Check if the task is no longer relevant. - $task_object = Task_Factory::create_task_from( 'id', $task_id ); - $task_provider = $this->get_task_provider( $task_object->get_provider_id() ); - if ( $task_provider && ! $task_provider->is_task_relevant() ) { - // Remove the task from the pending tasks. - \progress_planner()->get_suggested_tasks()->delete_task( $task_id ); - } - - $task_result = $this->evaluate_task( $task_id ); + foreach ( $tasks as $task ) { + $task_result = $this->evaluate_task( $task ); if ( false !== $task_result ) { $completed_tasks[] = $task_result; } @@ -265,83 +227,33 @@ public function evaluate_tasks() { } /** - * Wrapper function for evaluating tasks. + * Evaluate a task. * - * @param string $task_id The task ID. + * @param \Progress_Planner\Suggested_Tasks\Task $task The task to evaluate. * - * @return bool|\Progress_Planner\Suggested_Tasks\Task + * @return \Progress_Planner\Suggested_Tasks\Task|false */ - public function evaluate_task( $task_id ) { - $task_object = Task_Factory::create_task_from( 'id', $task_id ); - $task_provider = $this->get_task_provider( $task_object->get_provider_id() ); - - if ( ! $task_provider ) { + public function evaluate_task( Task $task ) { + // User tasks are not evaluated. + if ( \has_term( 'user', 'prpl_recommendations_provider', $task->ID ) ) { return false; } - return $task_provider->evaluate_task( $task_id ); - } - - /** - * Wrapper function for getting task details. - * - * @param string $task_id The task ID. - * - * @return array|false - */ - public function get_task_details( $task_id ) { - $task_object = Task_Factory::create_task_from( 'id', $task_id ); - $task_provider = $this->get_task_provider( $task_object->get_provider_id() ); - - if ( ! $task_provider ) { + if ( ! $task->provider ) { return false; } - - return $task_provider->get_task_details( $task_id ); - } - - /** - * Wrapper function for getting task details. - * - * @param string $task_id The task ID. - * - * @return array - */ - public function get_data_from_task_id( $task_id ) { - $task_object = Task_Factory::create_task_from( 'id', $task_id ); - - return $task_object->get_data(); - } - - /** - * Add a pending task. - * - * @param array $task The task data. - * - * @return bool - */ - public function add_pending_task( $task ) { - $tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); - - $task_index = false; - - foreach ( $tasks as $key => $_task ) { - if ( ! isset( $_task['task_id'] ) || $task['task_id'] !== $_task['task_id'] ) { - continue; - } - $task_index = $key; - break; + $task_provider = $this->get_task_provider( $task->provider->slug ); + if ( ! $task_provider ) { + return false; } - $task['status'] = 'pending'; - - if ( false !== $task_index ) { - $tasks[ $task_index ] = array_merge( $task, $tasks[ $task_index ] ); - } else { - $tasks[] = $task; + // Check if the task is no longer relevant. + if ( ! $task_provider->is_task_relevant() ) { + // Remove the task from the published tasks. + \progress_planner()->get_suggested_tasks_db()->delete_recommendation( $task->ID ); } - return \progress_planner()->get_settings()->set( 'tasks', $tasks ); + return $task_provider->evaluate_task( $task->task_id ); } /** @@ -351,40 +263,24 @@ public function add_pending_task( $task ) { * @return void */ public function cleanup_pending_tasks() { - - $cleanup_recently_performed = \progress_planner()->get_utils__cache()->get( 'cleanup_pending_tasks' ); - - if ( $cleanup_recently_performed ) { + if ( \progress_planner()->get_utils__cache()->get( 'cleanup_pending_tasks' ) ) { return; } - $tasks = (array) \progress_planner()->get_settings()->get( 'tasks', [] ); + $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'post_status' => 'publish' ] ); - if ( empty( $tasks ) ) { - return; - } - - $task_count = count( $tasks ); - - $tasks = \array_filter( - $tasks, - function ( $task ) { - - if ( 'pending' === $task['status'] && isset( $task['date'] ) ) { - return (string) \gmdate( 'YW' ) === (string) $task['date']; - } + foreach ( $tasks as $task ) { + // Skip user tasks. + if ( 'user' === $task->get_provider_id() ) { + continue; + } - // We have changed provider_id name, so we need to remove all tasks of the old provider_id. - if ( isset( $task['provider_id'] ) && 'update-post' === $task['provider_id'] ) { - return false; - } + $task_provider = $this->get_task_provider( $task->get_provider_id() ); - return true; + // Should we delete the task? Delete tasks which don't have a task provider or repetitive tasks which were created in the previous week. + if ( ! $task_provider || ( $task_provider->is_repetitive() && ( ! $task->date || \gmdate( 'YW' ) !== (string) $task->date ) ) ) { + \progress_planner()->get_suggested_tasks_db()->delete_recommendation( $task->ID ); } - ); - - if ( count( $tasks ) !== $task_count ) { - \progress_planner()->get_settings()->set( 'tasks', array_values( $tasks ) ); } \progress_planner()->get_utils__cache()->set( 'cleanup_pending_tasks', true, DAY_IN_SECONDS ); diff --git a/classes/suggested-tasks/data-collector/class-archive-format.php b/classes/suggested-tasks/data-collector/class-archive-format.php index 59a7404c6..5afd106c4 100644 --- a/classes/suggested-tasks/data-collector/class-archive-format.php +++ b/classes/suggested-tasks/data-collector/class-archive-format.php @@ -51,18 +51,20 @@ public function update_archive_format_cache( $new_status, $old_status, $post ) { protected function calculate_data() { // Check if there are any posts that use a post format using get_posts and get only the IDs. // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_tax_query - $args = [ - 'posts_per_page' => 10, - 'fields' => 'ids', - 'tax_query' => [ + return \count( + \get_posts( [ - 'taxonomy' => 'post_format', - 'operator' => 'EXISTS', - ], - ], - ]; + 'posts_per_page' => 10, + 'fields' => 'ids', + 'tax_query' => [ + [ + 'taxonomy' => 'post_format', + 'operator' => 'EXISTS', + ], + ], + ] + ) + ); // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_tax_query - - return count( get_posts( $args ) ); } } diff --git a/classes/suggested-tasks/data-collector/class-base-data-collector.php b/classes/suggested-tasks/data-collector/class-base-data-collector.php index 20e2d6db9..bf6af2d17 100644 --- a/classes/suggested-tasks/data-collector/class-base-data-collector.php +++ b/classes/suggested-tasks/data-collector/class-base-data-collector.php @@ -87,8 +87,7 @@ public function update_cache() { * @return mixed */ protected function get_cached_data( string $key ) { - $settings = \progress_planner()->get_settings(); - $data = $settings->get( static::CACHE_KEY, [] ); + $data = \progress_planner()->get_settings()->get( static::CACHE_KEY, [] ); return $data[ $key ] ?? null; } @@ -101,9 +100,8 @@ protected function get_cached_data( string $key ) { * @return void */ protected function set_cached_data( string $key, $value ) { - $settings = \progress_planner()->get_settings(); - $data = $settings->get( static::CACHE_KEY, [] ); + $data = \progress_planner()->get_settings()->get( static::CACHE_KEY, [] ); $data[ $key ] = $value; - $settings->set( static::CACHE_KEY, $data ); + \progress_planner()->get_settings()->set( static::CACHE_KEY, $data ); } } diff --git a/classes/suggested-tasks/data-collector/class-data-collector-manager.php b/classes/suggested-tasks/data-collector/class-data-collector-manager.php index 576c3afa8..0911474c9 100644 --- a/classes/suggested-tasks/data-collector/class-data-collector-manager.php +++ b/classes/suggested-tasks/data-collector/class-data-collector-manager.php @@ -68,9 +68,8 @@ public function __construct() { * @return void */ public function add_plugin_integration() { - // Yoast SEO integration. - if ( function_exists( 'YoastSEO' ) ) { + if ( \function_exists( 'YoastSEO' ) ) { $this->data_collectors[] = new Yoast_Orphaned_Content(); } } @@ -81,7 +80,6 @@ public function add_plugin_integration() { * @return void */ public function init() { - /** * Filter the data collectors. * @@ -101,10 +99,7 @@ public function init() { * @return void */ public function update_data_collectors_cache() { - - $update_recently_performed = \progress_planner()->get_utils__cache()->get( 'update_data_collectors_cache' ); - - if ( $update_recently_performed ) { + if ( \progress_planner()->get_utils__cache()->get( 'update_data_collectors_cache' ) ) { return; } diff --git a/classes/suggested-tasks/data-collector/class-hello-world.php b/classes/suggested-tasks/data-collector/class-hello-world.php index 91006c1e2..73c27a743 100644 --- a/classes/suggested-tasks/data-collector/class-hello-world.php +++ b/classes/suggested-tasks/data-collector/class-hello-world.php @@ -40,7 +40,6 @@ public function init() { * @return void */ public function update_hello_world_post_cache( $new_status, $old_status, $post ) { - // If the status is the same, do nothing. if ( $old_status === $new_status ) { return; @@ -57,12 +56,12 @@ public function update_hello_world_post_cache( $new_status, $old_status, $post ) * @return int */ protected function calculate_data() { - $sample_post = get_page_by_path( __( 'hello-world' ), OBJECT, 'post' ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain + $sample_post = \get_page_by_path( \__( 'hello-world' ), OBJECT, 'post' ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain if ( null === $sample_post ) { $query = new \WP_Query( [ 'post_type' => 'post', - 'title' => __( 'Hello world!' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain + 'title' => \__( 'Hello world!' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain 'post_status' => 'publish', 'posts_per_page' => 1, ] @@ -71,6 +70,6 @@ protected function calculate_data() { $sample_post = ! empty( $query->post ) ? $query->post : 0; } - return ( is_object( $sample_post ) && is_a( $sample_post, \WP_Post::class ) ) ? $sample_post->ID : 0; + return ( \is_object( $sample_post ) && \is_a( $sample_post, \WP_Post::class ) ) ? $sample_post->ID : 0; } } diff --git a/classes/suggested-tasks/data-collector/class-inactive-plugins.php b/classes/suggested-tasks/data-collector/class-inactive-plugins.php index 3a6a66da3..b4c82fb17 100644 --- a/classes/suggested-tasks/data-collector/class-inactive-plugins.php +++ b/classes/suggested-tasks/data-collector/class-inactive-plugins.php @@ -46,28 +46,28 @@ public function update_inactive_plugins_cache() { * @return int */ protected function calculate_data() { - if ( ! function_exists( 'get_plugins' ) ) { + if ( ! \function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; // @phpstan-ignore requireOnce.fileNotFound } // Clear the plugins cache, so get_plugins() returns the latest plugins. - wp_cache_delete( 'plugins', 'plugins' ); + \wp_cache_delete( 'plugins', 'plugins' ); - $plugins = get_plugins(); + $plugins = \get_plugins(); $plugins_active = 0; $plugins_total = 0; // Loop over the available plugins and check their versions and active state. - foreach ( array_keys( $plugins ) as $plugin_path ) { + foreach ( \array_keys( $plugins ) as $plugin_path ) { ++$plugins_total; - if ( is_plugin_active( $plugin_path ) ) { + if ( \is_plugin_active( $plugin_path ) ) { ++$plugins_active; } } $unused_plugins = 0; - if ( ! is_multisite() && $plugins_total > $plugins_active ) { + if ( ! \is_multisite() && $plugins_total > $plugins_active ) { $unused_plugins = $plugins_total - $plugins_active; } diff --git a/classes/suggested-tasks/data-collector/class-last-published-post.php b/classes/suggested-tasks/data-collector/class-last-published-post.php index f56679800..5155400a2 100644 --- a/classes/suggested-tasks/data-collector/class-last-published-post.php +++ b/classes/suggested-tasks/data-collector/class-last-published-post.php @@ -57,7 +57,9 @@ public function set_include_post_types() { * @return void */ public function update_last_published_post_cache( $new_status, $old_status, $post ) { - if ( true === \in_array( get_post_type( $post ), $this->include_post_types, true ) && ( $new_status === 'publish' || $old_status === 'publish' ) ) { + if ( true === \in_array( \get_post_type( $post ), $this->include_post_types, true ) && + ( $new_status === 'publish' || $old_status === 'publish' ) + ) { $this->update_cache(); } } @@ -68,7 +70,6 @@ public function update_last_published_post_cache( $new_status, $old_status, $pos * @return array */ protected function calculate_data() { - // Default data. $data = [ 'post_id' => 0, diff --git a/classes/suggested-tasks/data-collector/class-post-author.php b/classes/suggested-tasks/data-collector/class-post-author.php index 9a5bf5669..48e2f1fe4 100644 --- a/classes/suggested-tasks/data-collector/class-post-author.php +++ b/classes/suggested-tasks/data-collector/class-post-author.php @@ -78,6 +78,6 @@ protected function calculate_data() { " ); - return count( $author_ids ); + return \count( $author_ids ); } } diff --git a/classes/suggested-tasks/data-collector/class-sample-page.php b/classes/suggested-tasks/data-collector/class-sample-page.php index c7759bf5c..1ae1d5e40 100644 --- a/classes/suggested-tasks/data-collector/class-sample-page.php +++ b/classes/suggested-tasks/data-collector/class-sample-page.php @@ -40,7 +40,6 @@ public function init() { * @return void */ public function update_sample_page_cache( $new_status, $old_status, $post ) { - // If the status is the same, do nothing. if ( $old_status === $new_status ) { return; @@ -57,12 +56,12 @@ public function update_sample_page_cache( $new_status, $old_status, $post ) { * @return \WP_Post|int */ protected function calculate_data() { - $sample_page = get_page_by_path( __( 'sample-page' ) ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain + $sample_page = \get_page_by_path( \__( 'sample-page' ) ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain if ( null === $sample_page ) { $query = new \WP_Query( [ 'post_type' => 'page', - 'title' => __( 'Sample Page' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain + 'title' => \__( 'Sample Page' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain 'post_status' => 'publish', 'posts_per_page' => 1, ] @@ -71,6 +70,6 @@ protected function calculate_data() { $sample_page = ! empty( $query->post ) ? $query->post : 0; } - return ( is_object( $sample_page ) && is_a( $sample_page, \WP_Post::class ) ) ? $sample_page->ID : 0; + return ( \is_object( $sample_page ) && \is_a( $sample_page, \WP_Post::class ) ) ? $sample_page->ID : 0; } } diff --git a/classes/suggested-tasks/data-collector/class-terms-without-description.php b/classes/suggested-tasks/data-collector/class-terms-without-description.php index af1da309a..1767a4b3a 100644 --- a/classes/suggested-tasks/data-collector/class-terms-without-description.php +++ b/classes/suggested-tasks/data-collector/class-terms-without-description.php @@ -50,10 +50,9 @@ public function init() { * @return void */ public function on_term_edited( $term_id, $tt_id, $taxonomy, $args ) { - // Check if the taxonomy is public and that description is not empty. $taxonomy_object = \get_taxonomy( $taxonomy ); - if ( ! $taxonomy_object || ! $taxonomy_object->public || ! isset( $args['description'] ) || '' === trim( $args['description'] ) ) { + if ( ! $taxonomy_object || ! $taxonomy_object->public || ! isset( $args['description'] ) || '' === \trim( $args['description'] ) ) { return; } @@ -83,14 +82,27 @@ protected function calculate_data() { * * @var array $public_taxonomies */ - $public_taxonomies = get_taxonomies( [ 'public' => true ], 'names' ); - - if ( isset( $public_taxonomies['post_format'] ) ) { - unset( $public_taxonomies['post_format'] ); - } + $public_taxonomies = \get_taxonomies( [ 'public' => true ], 'names' ); - if ( isset( $public_taxonomies['product_shipping_class'] ) ) { - unset( $public_taxonomies['product_shipping_class'] ); + /** + * Array of public taxonomies to exclude from the terms without description query. + * + * @var array $exclude_public_taxonomies + */ + $exclude_public_taxonomies = \apply_filters( + 'progress_planner_exclude_public_taxonomies', + [ + 'post_format', + 'product_shipping_class', + 'prpl_recommendations_category', + 'prpl_recommendations_provider', + ] + ); + + foreach ( $exclude_public_taxonomies as $taxonomy ) { + if ( isset( $public_taxonomies[ $taxonomy ] ) ) { + unset( $public_taxonomies[ $taxonomy ] ); + } } // Exclude the Uncategorized category. @@ -108,7 +120,6 @@ protected function calculate_data() { $result = []; foreach ( $public_taxonomies as $taxonomy ) { - $query = " SELECT t.term_id, t.name, tt.count, tt.taxonomy FROM {$wpdb->terms} AS t @@ -117,7 +128,7 @@ protected function calculate_data() { AND (tt.description = '' OR tt.description IS NULL OR tt.description = ' ') AND tt.count >= %d"; if ( ! empty( $exclude_term_ids ) ) { - $query .= ' AND t.term_id NOT IN (' . implode( ',', array_map( 'intval', $exclude_term_ids ) ) . ')'; + $query .= ' AND t.term_id NOT IN (' . \implode( ',', \array_map( 'intval', $exclude_term_ids ) ) . ')'; } $query .= ' ORDER BY tt.count DESC LIMIT 1'; diff --git a/classes/suggested-tasks/data-collector/class-terms-without-posts.php b/classes/suggested-tasks/data-collector/class-terms-without-posts.php index fc00d4dc3..f39925b98 100644 --- a/classes/suggested-tasks/data-collector/class-terms-without-posts.php +++ b/classes/suggested-tasks/data-collector/class-terms-without-posts.php @@ -52,7 +52,6 @@ public function init() { * @return void */ public function on_terms_changed( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) { - // Check if the taxonomy is public. $taxonomy_object = \get_taxonomy( $taxonomy ); if ( ! $taxonomy_object || ! $taxonomy_object->public ) { @@ -92,14 +91,27 @@ protected function calculate_data() { * * @var array $public_taxonomies */ - $public_taxonomies = get_taxonomies( [ 'public' => true ], 'names' ); - - if ( isset( $public_taxonomies['post_format'] ) ) { - unset( $public_taxonomies['post_format'] ); - } + $public_taxonomies = \get_taxonomies( [ 'public' => true ], 'names' ); - if ( isset( $public_taxonomies['product_shipping_class'] ) ) { - unset( $public_taxonomies['product_shipping_class'] ); + /** + * Array of public taxonomies to exclude from the terms without posts query. + * + * @var array $exclude_public_taxonomies + */ + $exclude_public_taxonomies = \apply_filters( + 'progress_planner_exclude_public_taxonomies', + [ + 'post_format', + 'product_shipping_class', + 'prpl_recommendations_category', + 'prpl_recommendations_provider', + ] + ); + + foreach ( $exclude_public_taxonomies as $taxonomy ) { + if ( isset( $public_taxonomies[ $taxonomy ] ) ) { + unset( $public_taxonomies[ $taxonomy ] ); + } } /** @@ -127,7 +139,7 @@ protected function calculate_data() { "; if ( ! empty( $exclude_term_ids ) ) { - $query .= ' AND t.term_id NOT IN (' . implode( ',', array_map( 'intval', $exclude_term_ids ) ) . ')'; + $query .= ' AND t.term_id NOT IN (' . \implode( ',', \array_map( 'intval', $exclude_term_ids ) ) . ')'; } $query .= ' LIMIT %d'; diff --git a/classes/suggested-tasks/data-collector/class-uncategorized-category.php b/classes/suggested-tasks/data-collector/class-uncategorized-category.php index aab4df56d..c50e0d689 100644 --- a/classes/suggested-tasks/data-collector/class-uncategorized-category.php +++ b/classes/suggested-tasks/data-collector/class-uncategorized-category.php @@ -47,8 +47,8 @@ public function update_uncategorized_category_cache() { */ protected function calculate_data() { global $wpdb; - $default_category_name = __( 'Uncategorized' ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain - $default_category_slug = sanitize_title( _x( 'Uncategorized', 'Default category slug' ) ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain + $default_category_name = \__( 'Uncategorized' ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain + $default_category_slug = \sanitize_title( \_x( 'Uncategorized', 'Default category slug' ) ); // phpcs:ignore WordPress.WP.I18n.MissingArgDomain // Get the Uncategorized category by name or slug. $uncategorized_category = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching diff --git a/classes/suggested-tasks/data-collector/class-yoast-orphaned-content.php b/classes/suggested-tasks/data-collector/class-yoast-orphaned-content.php index 0e925fb90..09f7dc01b 100644 --- a/classes/suggested-tasks/data-collector/class-yoast-orphaned-content.php +++ b/classes/suggested-tasks/data-collector/class-yoast-orphaned-content.php @@ -29,7 +29,7 @@ class Yoast_Orphaned_Content extends Base_Data_Collector { * @return void */ public function init() { - if ( ! function_exists( 'YoastSEO' ) ) { + if ( ! \function_exists( 'YoastSEO' ) ) { return; } @@ -57,7 +57,7 @@ public function update_orphaned_content_cache( $new_status, $old_status, $post ) * @return array */ protected function calculate_data() { - if ( ! function_exists( 'YoastSEO' ) ) { + if ( ! \function_exists( 'YoastSEO' ) ) { return []; } @@ -69,19 +69,19 @@ protected function calculate_data() { $post_types_in = ''; if ( ! empty( $public_post_types ) ) { - $post_types_in = array_map( + $post_types_in = \array_map( function ( $type ) { - return (string) esc_sql( $type ); + return (string) \esc_sql( $type ); }, - array_values( $public_post_types ) + \array_values( $public_post_types ) ); - $post_types_in = "p.post_type IN ('" . implode( "','", $post_types_in ) . "')"; + $post_types_in = "p.post_type IN ('" . \implode( "','", $post_types_in ) . "')"; $where_clause .= " AND $post_types_in"; } // Exclude "Hello World" and "Sample Page" posts, use array_filter() to remove empty values. - $exclude_post_ids = array_filter( + $exclude_post_ids = \array_filter( [ ( new Hello_World() )->collect(), ( new Sample_Page() )->collect(), @@ -96,18 +96,18 @@ function ( $type ) { $exclude_post_ids = \apply_filters( 'progress_planner_yoast_orphaned_content_exclude_post_ids', $exclude_post_ids ); if ( ! empty( $exclude_post_ids ) ) { - $exclude_post_ids = array_map( 'intval', $exclude_post_ids ); - $where_clause .= ' AND p.ID NOT IN (' . implode( ',', $exclude_post_ids ) . ')'; + $exclude_post_ids = \array_map( 'intval', $exclude_post_ids ); + $where_clause .= ' AND p.ID NOT IN (' . \implode( ',', $exclude_post_ids ) . ')'; } $query = " SELECT p.ID AS post_id, p.post_title AS post_title FROM {$wpdb->posts} p LEFT JOIN ( - SELECT DISTINCT target_post_id - FROM {$wpdb->prefix}yoast_seo_links - WHERE type = 'internal' - AND target_post_id IS NOT NULL + SELECT DISTINCT l.target_post_id + FROM {$wpdb->prefix}yoast_seo_links l + WHERE l.type = 'internal' + AND l.target_post_id IS NOT NULL ) l ON p.ID = l.target_post_id WHERE {$where_clause} AND l.target_post_id IS NULL diff --git a/classes/suggested-tasks/providers/class-blog-description.php b/classes/suggested-tasks/providers/class-blog-description.php index 2c11ecab4..cb1cc28d2 100644 --- a/classes/suggested-tasks/providers/class-blog-description.php +++ b/classes/suggested-tasks/providers/class-blog-description.php @@ -27,38 +27,48 @@ class Blog_Description extends Tasks { protected const PROVIDER_ID = 'core-blogdescription'; /** - * Constructor. - */ - public function __construct() { - $this->url = \admin_url( 'options-general.php?pp-focus-el=' . $this->get_task_id() ); - $this->link_setting = [ - 'hook' => 'options-general.php', - 'iconEl' => 'th:has(+td #tagline-description)', - ]; - } - - /** - * Get the title. + * Get the task title. * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Set tagline', 'progress-planner' ); } /** - * Get the description. + * Get the task description. * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %s:tagline link */ \esc_html__( 'Set the %s to make your website look more professional.', 'progress-planner' ), '' . \esc_html__( 'tagline', 'progress-planner' ) . '' ); } + /** + * Get the task URL. + * + * @return string + */ + protected function get_url() { + return \admin_url( 'options-general.php?pp-focus-el=' . $this->get_task_id() ); + } + + /** + * Get the link setting. + * + * @return array + */ + public function get_link_setting() { + return [ + 'hook' => 'options-general.php', + 'iconEl' => 'th:has(+td #tagline-description)', + ]; + } + /** * Check if the task should be added. * diff --git a/classes/suggested-tasks/providers/class-collaborator.php b/classes/suggested-tasks/providers/class-collaborator.php new file mode 100644 index 000000000..556ddfca0 --- /dev/null +++ b/classes/suggested-tasks/providers/class-collaborator.php @@ -0,0 +1,112 @@ +get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $task_id ] ); + if ( empty( $tasks ) ) { + return false; + } + + $task_data = $tasks[0]->get_data(); + + return isset( $task_data['is_completed_callback'] ) && \is_callable( $task_data['is_completed_callback'] ) + ? \call_user_func( $task_data['is_completed_callback'], $task_id ) + : false; + } + + /** + * Get the task details. + * + * @param array $task_data The task data. + * + * @return array + */ + public function get_task_details( $task_data = [] ) { + $tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); + + foreach ( $tasks as $task ) { + if ( $task['task_id'] !== $task_data['task_id'] ) { + continue; + } + + return \wp_parse_args( + $task, + [ + 'task_id' => '', + 'title' => '', + 'parent' => 0, + 'provider_id' => $this->get_provider_id(), + 'category' => $this->get_provider_category(), + 'priority' => 'medium', + 'points' => 0, + 'url' => '', + 'url_target' => '_self', + 'description' => '', + 'link_setting' => [], + 'dismissable' => true, + 'snoozable' => false, + ] + ); + } + + return []; + } +} diff --git a/classes/suggested-tasks/providers/class-content-create.php b/classes/suggested-tasks/providers/class-content-create.php index 3c220cca0..d45c22c2e 100644 --- a/classes/suggested-tasks/providers/class-content-create.php +++ b/classes/suggested-tasks/providers/class-content-create.php @@ -35,6 +35,13 @@ class Content_Create extends Tasks { */ protected const CAPABILITY = 'edit_others_posts'; + /** + * The data collector class name. + * + * @var string + */ + protected const DATA_COLLECTOR_CLASS = Last_Published_Post_Data_Collector::class; + /** * Whether the task is repetitive. * @@ -50,18 +57,12 @@ class Content_Create extends Tasks { protected $url_target = '_blank'; /** - * The data collector. + * Get the task URL. * - * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Last_Published_Post - */ - protected $data_collector; - - /** - * Constructor. + * @return string */ - public function __construct() { - $this->data_collector = new Last_Published_Post_Data_Collector(); - $this->url = 'https://prpl.fyi/valuable-content'; + protected function get_url() { + return 'https://prpl.fyi/valuable-content'; } /** @@ -69,8 +70,8 @@ public function __construct() { * * @return string */ - public function get_title() { - return esc_html__( 'Create valuable content', 'progress-planner' ); + protected function get_title() { + return \esc_html__( 'Create valuable content', 'progress-planner' ); } /** @@ -78,8 +79,8 @@ public function get_title() { * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %s: "Read more" link. */ \esc_html__( 'Time to add more valuable content to your site! Check our blog for inspiration. %s.', 'progress-planner' ), '' . \esc_html__( 'Read more', 'progress-planner' ) . '' @@ -94,14 +95,14 @@ public function get_description() { * @return array */ public function modify_evaluated_task_data( $task_data ) { - $last_published_post_data = $this->data_collector->collect(); + $last_published_post_data = $this->get_data_collector()->collect(); if ( ! $last_published_post_data || empty( $last_published_post_data['post_id'] ) ) { return $task_data; } - // Add the post ID and post length to the task data. - $task_data['post_id'] = $last_published_post_data['post_id']; + // Add the post ID to the task data. + $task_data['target_post_id'] = $last_published_post_data['post_id']; return $task_data; } @@ -112,9 +113,8 @@ public function modify_evaluated_task_data( $task_data ) { * @return bool */ public function should_add_task() { - // Get the post that was created last. - $last_published_post_data = $this->data_collector->collect(); + $last_published_post_data = $this->get_data_collector()->collect(); // There are no published posts, add task. if ( ! $last_published_post_data || empty( $last_published_post_data['post_id'] ) ) { @@ -122,61 +122,6 @@ public function should_add_task() { } // Add tasks if there are no posts published this week. - return \gmdate( 'YW' ) !== \gmdate( 'YW', strtotime( $last_published_post_data['post_date'] ) ); - } - - /** - * Get the task details. - * - * @param string $task_id The task ID. - * - * @return array - */ - public function get_task_details( $task_id = '' ) { - - if ( ! $task_id ) { - return []; - } - - $task_details = [ - 'task_id' => $task_id, - 'provider_id' => $this->get_provider_id(), - 'title' => $this->get_title(), - 'parent' => $this->get_parent(), - 'priority' => $this->get_priority(), - 'category' => $this->get_provider_category(), - 'points' => $this->get_points(), - 'dismissable' => $this->is_dismissable(), - 'url' => $this->get_url(), - 'url_target' => $this->get_url_target(), - 'description' => $this->get_description(), - ]; - - return $task_details; - } - - /** - * Get the number of points for the task. - * This is used to calculate points in the RR widget, so user can see if he earned 1 or 2 points when celebrating. - * - * @param string $task_id The task ID. - * - * @return int - */ - public function get_points( $task_id = '' ) { - - if ( ! $task_id ) { - return $this->points; - } - - $post_data = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $task_id ); - $post_data = $post_data[0] ?? false; - - // Backwards compatibility. - if ( $post_data && isset( $post_data['long'] ) ) { - return true === $post_data['long'] ? 2 : 1; - } - - return $this->points; + return \gmdate( 'YW' ) !== \gmdate( 'YW', \strtotime( $last_published_post_data['post_date'] ) ); } } diff --git a/classes/suggested-tasks/providers/class-content-review.php b/classes/suggested-tasks/providers/class-content-review.php index 3d30f51a1..e20253331 100644 --- a/classes/suggested-tasks/providers/class-content-review.php +++ b/classes/suggested-tasks/providers/class-content-review.php @@ -7,7 +7,6 @@ namespace Progress_Planner\Suggested_Tasks\Providers; -use Progress_Planner\Suggested_Tasks\Task_Factory; use Progress_Planner\Suggested_Tasks\Providers\Traits\Dismissable_Task; use Progress_Planner\Page_Types; @@ -46,11 +45,18 @@ class Content_Review extends Tasks { protected $is_repetitive = true; /** - * The task priority. + * The task URL target. * * @var string */ - protected $priority = 'high'; + protected $url_target = '_blank'; + + /** + * The task priority. + * + * @var int + */ + protected $priority = 30; /** * Whether the task is dismissable. @@ -105,7 +111,7 @@ public function init() { \add_filter( 'progress_planner_update_posts_tasks_args', [ $this, 'filter_update_posts_args' ] ); // Add the Yoast cornerstone pages to the important page IDs. - if ( function_exists( 'YoastSEO' ) ) { + if ( \function_exists( 'YoastSEO' ) ) { \add_filter( 'progress_planner_update_posts_important_page_ids', [ $this, 'add_yoast_cornerstone_pages' ] ); } @@ -115,60 +121,86 @@ public function init() { /** * Get the task title. * - * @param string $task_id The task ID. + * @param array $task_data The task data. * * @return string */ - public function get_title( $task_id = '' ) { - $post = $this->get_post_from_task_id( $task_id ); + protected function get_title_with_data( $task_data = [] ) { + if ( ! isset( $task_data['target_post_id'] ) ) { + return ''; + } + + $post = \get_post( $task_data['target_post_id'] ); - return $post - ? sprintf( + if ( ! $post ) { + return ''; + } + + return \sprintf( // translators: %1$s: The post type, %2$s: The post title. - \esc_html__( 'Review %1$s "%2$s"', 'progress-planner' ), - strtolower( \get_post_type_object( \esc_html( $post->post_type ) )->labels->singular_name ), // @phpstan-ignore-line property.nonObject - \esc_html( $post->post_title ) // @phpstan-ignore-line property.nonObject - ) : ''; + \esc_html__( 'Review %1$s "%2$s"', 'progress-planner' ), + \strtolower( \get_post_type_object( \esc_html( $post->post_type ) )->labels->singular_name ), // @phpstan-ignore-line property.nonObject + \esc_html( $post->post_title ) // @phpstan-ignore-line property.nonObject + ); } /** * Get the task description. * - * @param string $task_id The task ID. + * @param array $task_data The task data. * * @return string */ - public function get_description( $task_id = '' ) { - $post = $this->get_post_from_task_id( $task_id ); + protected function get_description_with_data( $task_data = [] ) { + if ( ! isset( $task_data['target_post_id'] ) ) { + return ''; + } + + $post = \get_post( $task_data['target_post_id'] ); if ( ! $post ) { return ''; } - $months = in_array( (int) $post->ID, $this->get_saved_page_types(), true ) ? '12' : '6'; + $months = \in_array( (int) $post->ID, $this->get_saved_page_types(), true ) ? '12' : '6'; - return '

    ' . sprintf( + return '

    ' . \sprintf( /* translators: %1$s Review link, %2$s: The post title, %3$s: The number of months. */ \esc_html__( '%1$s the post "%2$s" as it was last updated more than %3$s months ago.', 'progress-planner' ), '' . \esc_html__( 'Review', 'progress-planner' ) . '', \esc_html( $post->post_title ), // @phpstan-ignore-line property.nonObject \esc_html( $months ) - ) . '

    ' . ( $this->capability_required() ? '

    ' . \esc_html__( 'Edit the post', 'progress-planner' ) . '.

    ' : '' ); // @phpstan-ignore-line property.nonObject + ) . '

    '; } /** * Get the task URL. * - * @param string $task_id The task ID. + * @param array $task_data The task data. * * @return string */ - public function get_url( $task_id = '' ) { - $post = $this->get_post_from_task_id( $task_id ); + protected function get_url_with_data( $task_data = [] ) { + if ( ! isset( $task_data['target_post_id'] ) ) { + return ''; + } + + $post = \get_post( $task_data['target_post_id'] ); - return $post && $this->capability_required() - ? \esc_url( (string) \get_edit_post_link( $post->ID ) ) - : ''; + if ( ! $post ) { + return ''; + } + + // We don't use the edit_post_link() function because we need to bypass it's current_user_can() check. + return \esc_url( + \add_query_arg( + [ + 'post' => $post->ID, + 'action' => 'edit', + ], + \admin_url( 'post.php' ) + ) + ); } /** @@ -177,108 +209,106 @@ public function get_url( $task_id = '' ) { * @return bool */ public function should_add_task() { - if ( null === $this->task_post_mappings ) { - $this->task_post_mappings = []; + if ( null !== $this->task_post_mappings ) { + return 0 < \count( $this->task_post_mappings ); + } - $number_of_posts_to_inject = static::ITEMS_TO_INJECT; - $last_updated_posts = []; + $this->task_post_mappings = []; - // Check if there are any important pages to update. - $important_page_ids = []; - foreach ( \progress_planner()->get_admin__page_settings()->get_settings() as $important_page ) { - if ( 0 !== (int) $important_page['value'] ) { - $important_page_ids[] = (int) $important_page['value']; - } - } + $number_of_posts_to_inject = static::ITEMS_TO_INJECT; + $last_updated_posts = []; - // Add the privacy policy page ID if it exists. Not 'publish' page will not be fetched by get_posts(). - $privacy_policy_page_id = \get_option( 'wp_page_for_privacy_policy' ); - if ( $privacy_policy_page_id ) { - $important_page_ids[] = (int) $privacy_policy_page_id; + // Check if there are any important pages to update. + $important_page_ids = []; + foreach ( \progress_planner()->get_admin__page_settings()->get_settings() as $important_page ) { + if ( 0 !== (int) $important_page['value'] ) { + $important_page_ids[] = (int) $important_page['value']; } + } - /** - * Filters the pages we deem more important for content updates. - * - * @param int[] $important_page_ids Post & page IDs of the important pages. - */ - $important_page_ids = \apply_filters( 'progress_planner_update_posts_important_page_ids', $important_page_ids ); - - if ( ! empty( $important_page_ids ) ) { - $last_updated_posts = $this->get_old_posts( - [ - 'post__in' => $important_page_ids, - 'post_type' => 'any', - 'date_query' => [ - [ - 'column' => 'post_modified', - 'before' => '-6 months', // Important pages are updated more often. - ], - ], - ] - ); - } + // Add the privacy policy page ID if it exists. Not 'publish' page will not be fetched by get_posts(). + $privacy_policy_page_id = \get_option( 'wp_page_for_privacy_policy' ); + if ( $privacy_policy_page_id ) { + $important_page_ids[] = (int) $privacy_policy_page_id; + } - // Lets check for other posts to update. - $number_of_posts_to_inject = $number_of_posts_to_inject - count( $last_updated_posts ); + /** + * Filters the pages we deem more important for content updates. + * + * @param int[] $important_page_ids Post & page IDs of the important pages. + */ + $important_page_ids = \apply_filters( 'progress_planner_update_posts_important_page_ids', $important_page_ids ); - if ( 0 < $number_of_posts_to_inject ) { - // Get the post that was updated last. - $last_updated_posts = array_merge( - $last_updated_posts, - $this->get_old_posts( + if ( ! empty( $important_page_ids ) ) { + $last_updated_posts = $this->get_old_posts( + [ + 'post__in' => $important_page_ids, + 'post_type' => 'any', + 'date_query' => [ [ - 'post__not_in' => $important_page_ids, // This can be an empty array. - 'post_type' => $this->include_post_types, - ] - ) - ); - } + 'column' => 'post_modified', + 'before' => '-6 months', // Important pages are updated more often. + ], + ], + ] + ); + } - if ( ! $last_updated_posts ) { - return false; - } + // Lets check for other posts to update. + $number_of_posts_to_inject = $number_of_posts_to_inject - \count( $last_updated_posts ); - foreach ( $last_updated_posts as $post ) { - $task_data = [ - 'post_id' => $post->ID, - 'provider_id' => $this->get_provider_id(), - ]; + if ( 0 < $number_of_posts_to_inject ) { + // Get the post that was updated last. + $last_updated_posts = \array_merge( + $last_updated_posts, + $this->get_old_posts( + [ + 'post__not_in' => $important_page_ids, // This can be an empty array. + 'post_type' => $this->include_post_types, + ] + ) + ); + } - // Skip if the task has been dismissed. - if ( $this->is_task_dismissed( $task_data ) ) { - continue; - } + if ( ! $last_updated_posts ) { + return false; + } - $task_id = $this->get_task_id( [ 'post_id' => $post->ID ] ); + foreach ( $last_updated_posts as $post ) { + // Skip if the task has been dismissed. + if ( $this->is_task_dismissed( + [ + 'target_post_id' => $post->ID, + 'provider_id' => $this->get_provider_id(), + ] + ) ) { + continue; + } - // Don't add the task if it was completed. - if ( true === \progress_planner()->get_suggested_tasks()->was_task_completed( $task_id ) ) { - continue; - } + $task_id = $this->get_task_id( [ 'target_post_id' => $post->ID ] ); - $this->task_post_mappings[ $task_id ] = [ - 'task_id' => $task_id, - 'post_id' => $post->ID, - 'post_type' => $post->post_type, - ]; + // Don't add the task if it was completed. + if ( true === \progress_planner()->get_suggested_tasks()->was_task_completed( $task_id ) ) { + continue; } + + $this->task_post_mappings[ $task_id ] = [ + 'task_id' => $task_id, + 'target_post_id' => $post->ID, + 'target_post_type' => $post->post_type, + ]; } - return 0 < count( $this->task_post_mappings ); + return 0 < \count( $this->task_post_mappings ); } - /** * Get an array of tasks to inject. * * @return array */ public function get_tasks_to_inject() { - - if ( - ! $this->should_add_task() // No need to add the task. - ) { + if ( ! $this->should_add_task() ) { return []; } @@ -290,47 +320,35 @@ public function get_tasks_to_inject() { } $task_to_inject[] = [ - 'task_id' => $this->get_task_id( [ 'post_id' => $task_data['post_id'] ] ), - 'provider_id' => $this->get_provider_id(), - 'category' => $this->get_provider_category(), - 'post_id' => $task_data['post_id'], - 'post_type' => $task_data['post_type'], - 'date' => \gmdate( 'YW' ), + 'task_id' => $this->get_task_id( [ 'target_post_id' => $task_data['target_post_id'] ] ), + 'provider_id' => $this->get_provider_id(), + 'category' => $this->get_provider_category(), + 'target_post_id' => $task_data['target_post_id'], + 'target_post_type' => $task_data['target_post_type'], + 'date' => \gmdate( 'YW' ), + 'post_title' => $this->get_title_with_data( $task_data ), + 'description' => $this->get_description_with_data( $task_data ), + 'url' => $this->get_url_with_data( $task_data ), + 'url_target' => $this->get_url_target(), + 'dismissable' => $this->is_dismissable(), + 'snoozable' => $this->is_snoozable, + 'points' => $this->get_points(), ]; } } - return $task_to_inject; - } + $added_tasks = []; - /** - * Get the task details. - * - * @param string $task_id The task ID. - * - * @return array - */ - public function get_task_details( $task_id = '' ) { + foreach ( $task_to_inject as $task_data ) { + // Skip the task if it was already injected. + if ( \progress_planner()->get_suggested_tasks_db()->get_post( $task_data['task_id'] ) ) { + continue; + } - if ( ! $task_id ) { - return []; + $added_tasks[] = \progress_planner()->get_suggested_tasks_db()->add( $task_data ); } - $task_details = [ - 'task_id' => $task_id, - 'provider_id' => $this->get_provider_id(), - 'title' => $this->get_title( $task_id ), - 'parent' => $this->get_parent(), - 'priority' => $this->get_priority(), - 'category' => $this->get_provider_category(), - 'points' => $this->get_points(), - 'dismissable' => $this->is_dismissable(), - 'url' => $this->get_url( $task_id ), - 'url_target' => $this->get_url_target(), - 'description' => $this->get_description( $task_id ), - ]; - - return $task_details; + return $added_tasks; } /** @@ -341,15 +359,15 @@ public function get_task_details( $task_id = '' ) { * @return \WP_Post|null */ public function get_post_from_task_id( $task_id ) { - $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $task_id ); + $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $task_id ] ); if ( empty( $tasks ) ) { return null; } - $data = $tasks[0]; - - return isset( $data['post_id'] ) && $data['post_id'] ? \get_post( $data['post_id'] ) : null; + return isset( $tasks[0]->target_post_id ) && $tasks[0]->target_post_id + ? \get_post( $tasks[0]->target_post_id ) + : null; } /** @@ -374,7 +392,7 @@ public function get_old_posts( $args = [] ) { $posts = []; // Parse default args. - $args = wp_parse_args( + $args = \wp_parse_args( $args, [ 'posts_per_page' => static::ITEMS_TO_INJECT, @@ -396,7 +414,7 @@ public function get_old_posts( $args = [] ) { * * @param array $args The get_posts args. */ - $args = apply_filters( 'progress_planner_update_posts_tasks_args', $args ); + $args = \apply_filters( 'progress_planner_update_posts_tasks_args', $args ); // Get the post that was updated last. $posts = \get_posts( $args ); @@ -416,7 +434,7 @@ public function filter_update_posts_args( $args ) { ? $args['post__not_in'] : []; - $args['post__not_in'] = array_merge( + $args['post__not_in'] = \array_merge( $args['post__not_in'], // Add the snoozed post IDs to the post__not_in array. $this->get_snoozed_post_ids(), @@ -425,10 +443,10 @@ public function filter_update_posts_args( $args ) { $dismissed_post_ids = $this->get_dismissed_post_ids(); if ( ! empty( $dismissed_post_ids ) ) { - $args['post__not_in'] = array_merge( $args['post__not_in'], $dismissed_post_ids ); + $args['post__not_in'] = \array_merge( $args['post__not_in'], $dismissed_post_ids ); } - if ( function_exists( 'YoastSEO' ) ) { + if ( \function_exists( 'YoastSEO' ) ) { // Handle the case when the meta key doesn't exist. $args['meta_query'] = [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'relation' => 'OR', @@ -455,18 +473,22 @@ public function filter_update_posts_args( $args ) { * @return array */ protected function get_snoozed_post_ids() { - if ( null !== $this->snoozed_post_ids ) { return $this->snoozed_post_ids; } $this->snoozed_post_ids = []; - $snoozed = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'status', 'snoozed' ); + $snoozed = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'post_status' => 'future' ] ); if ( ! empty( $snoozed ) ) { foreach ( $snoozed as $task ) { - if ( isset( $task['provider_id'] ) && 'review-post' === $task['provider_id'] ) { - $this->snoozed_post_ids[] = $task['post_id']; + /** + * The task object. + * + * @var \Progress_Planner\Suggested_Tasks\Task $task + */ + if ( isset( $task->provider->slug ) && 'review-post' === $task->provider->slug ) { + $this->snoozed_post_ids[] = $task->target_post_id; } } } @@ -480,7 +502,6 @@ protected function get_snoozed_post_ids() { * @return array */ protected function get_dismissed_post_ids() { - if ( null !== $this->dismissed_post_ids ) { return $this->dismissed_post_ids; } @@ -489,7 +510,7 @@ protected function get_dismissed_post_ids() { $dismissed = $this->get_dismissed_tasks(); if ( ! empty( $dismissed ) ) { - $this->dismissed_post_ids = array_values( wp_list_pluck( $dismissed, 'post_id' ) ); + $this->dismissed_post_ids = \array_values( \wp_list_pluck( $dismissed, 'post_id' ) ); } return $this->dismissed_post_ids; @@ -504,7 +525,7 @@ protected function get_dismissed_post_ids() { * @return string|false The task identifier or false if not applicable. */ protected function get_task_identifier( $task_data ) { - return $this->get_provider_id() . '-' . $task_data['post_id']; + return $this->get_provider_id() . '-' . $task_data['target_post_id']; } /** @@ -531,15 +552,16 @@ protected function get_saved_page_types() { * @return bool */ protected function is_specific_task_completed( $task_id ) { + $task = \progress_planner()->get_suggested_tasks_db()->get_post( $task_id ); - $task = Task_Factory::create_task_from( 'id', $task_id ); - $data = $task->get_data(); - - if ( isset( $data['post_id'] ) && (int) \get_post_modified_time( 'U', false, (int) $data['post_id'] ) > strtotime( '-12 months' ) ) { - return true; + if ( ! $task ) { + return false; } - return false; + $data = $task->get_data(); + + return $data && isset( $data['target_post_id'] ) + && (int) \get_post_modified_time( 'U', false, (int) $data['target_post_id'] ) > \strtotime( '-12 months' ); } /** @@ -549,18 +571,19 @@ protected function is_specific_task_completed( $task_id ) { * @return int[] */ public function add_yoast_cornerstone_pages( $important_page_ids ) { - if ( function_exists( 'YoastSEO' ) ) { - $cornerstone_page_ids = \get_posts( - [ - 'post_type' => 'any', - 'meta_key' => '_yoast_wpseo_is_cornerstone', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key - 'meta_value' => '1', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value - 'fields' => 'ids', - ] - ); - if ( ! empty( $cornerstone_page_ids ) ) { - $important_page_ids = array_merge( $important_page_ids, $cornerstone_page_ids ); - } + if ( ! \function_exists( 'YoastSEO' ) ) { + return $important_page_ids; + } + $cornerstone_page_ids = \get_posts( + [ + 'post_type' => 'any', + 'meta_key' => '_yoast_wpseo_is_cornerstone', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + 'meta_value' => '1', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + 'fields' => 'ids', + ] + ); + if ( ! empty( $cornerstone_page_ids ) ) { + $important_page_ids = \array_merge( $important_page_ids, $cornerstone_page_ids ); } return $important_page_ids; } @@ -584,7 +607,7 @@ protected function get_expiration_period( $dismissal_data ) { } // Check if it his cornerstone content. - if ( function_exists( 'YoastSEO' ) ) { + if ( \function_exists( 'YoastSEO' ) ) { $is_cornerstone = \get_post_meta( $dismissal_data['post_id'], '_yoast_wpseo_is_cornerstone', true ); if ( '1' === $is_cornerstone ) { return 6 * MONTH_IN_SECONDS; diff --git a/classes/suggested-tasks/providers/class-core-update.php b/classes/suggested-tasks/providers/class-core-update.php index 8bf12ad5b..475646ffc 100644 --- a/classes/suggested-tasks/providers/class-core-update.php +++ b/classes/suggested-tasks/providers/class-core-update.php @@ -43,18 +43,17 @@ class Core_Update extends Tasks { /** * The task priority. * - * @var string + * @var int */ - protected $priority = 'high'; - + protected $priority = 0; /** - * Constructor. + * Get the task URL. * - * @return void + * @return string */ - public function __construct() { - $this->url = \admin_url( 'update-core.php' ); + protected function get_url() { + return \admin_url( 'update-core.php' ); } /** @@ -73,7 +72,7 @@ public function init() { * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Perform all updates', 'progress-planner' ); } @@ -82,8 +81,8 @@ public function get_title() { * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %s:See why we recommend this link */ \esc_html__( 'Regular updates improve security and performance. %s.', 'progress-planner' ), '' . \esc_html__( 'See why we recommend this', 'progress-planner' ) . '' @@ -98,14 +97,14 @@ public function get_description() { * @return array */ public function add_core_update_link( $update_actions ) { - $pending_tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'status', 'pending' ); + $pending_tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'post_status' => 'publish' ] ); - // All updates are completed and there is a 'update-core' task in the pending tasks. + // All updates are completed and there is a 'update-core' task in the published tasks. if ( $pending_tasks && $this->is_task_completed() ) { foreach ( $pending_tasks as $task ) { - if ( $this->get_task_id() === $task['task_id'] ) { + if ( $this->get_task_id() === $task->task_id ) { $update_actions['prpl_core_update'] = - 'Progress Planner' . + 'Progress Planner' . '' . \esc_html__( 'Click here to celebrate your completed task!', 'progress-planner' ) . ''; break; } @@ -114,6 +113,7 @@ public function add_core_update_link( $update_actions ) { return $update_actions; } + /** * Check if the task should be added. * @@ -121,37 +121,9 @@ public function add_core_update_link( $update_actions ) { */ public function should_add_task() { // Without this \wp_get_update_data() might not return correct data for the core updates (depending on the timing). - if ( ! function_exists( 'get_core_updates' ) ) { + if ( ! \function_exists( 'get_core_updates' ) ) { require_once ABSPATH . 'wp-admin/includes/update.php'; // @phpstan-ignore requireOnce.fileNotFound } return 0 < \wp_get_update_data()['counts']['total']; } - - /** - * Get the task details. - * - * @param string $task_id The task ID. - * - * @return array - */ - public function get_task_details( $task_id = '' ) { - - if ( ! $task_id ) { - $task_id = $this->get_task_id(); - } - - return [ - 'task_id' => $task_id, - 'title' => $this->get_title(), - 'parent' => $this->get_parent(), - 'priority' => $this->get_priority(), - 'category' => $this->get_provider_category(), - 'provider_id' => $this->get_provider_id(), - 'points' => $this->get_points(), - 'dismissable' => $this->is_dismissable(), - 'url' => $this->get_url(), - 'url_target' => $this->get_url_target(), - 'description' => $this->get_description(), - ]; - } } diff --git a/classes/suggested-tasks/providers/class-debug-display.php b/classes/suggested-tasks/providers/class-debug-display.php index 19be08443..c0789c055 100644 --- a/classes/suggested-tasks/providers/class-debug-display.php +++ b/classes/suggested-tasks/providers/class-debug-display.php @@ -27,21 +27,22 @@ class Debug_Display extends Tasks { protected const PROVIDER_ID = 'wp-debug-display'; /** - * Get the title. + * Get the task title. * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Disable public display of PHP errors', 'progress-planner' ); } /** - * Get the description. + * Get the task description. * + * @param array $task_data Optional data to include in the task. * @return string */ - public function get_description() { - return sprintf( + protected function get_description( $task_data = [] ) { + return \sprintf( // translators: %1$s is the name of the WP_DEBUG_DISPLAY constant, %2$s We recommend link. \esc_html__( '%1$s is enabled. This means that errors are shown to users. %2$s disabling it.', 'progress-planner' ), 'WP_DEBUG_DISPLAY', @@ -55,6 +56,6 @@ public function get_description() { * @return bool */ public function should_add_task() { - return defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY; + return \defined( 'WP_DEBUG' ) && WP_DEBUG && \defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY; } } diff --git a/classes/suggested-tasks/providers/class-disable-comments.php b/classes/suggested-tasks/providers/class-disable-comments.php index eedfd7476..715f8d90e 100644 --- a/classes/suggested-tasks/providers/class-disable-comments.php +++ b/classes/suggested-tasks/providers/class-disable-comments.php @@ -27,32 +27,42 @@ class Disable_Comments extends Tasks { protected const PROVIDER_ID = 'disable-comments'; /** - * Constructor. + * Get the task URL. + * + * @return string + */ + protected function get_url() { + return \admin_url( 'options-discussion.php' ); + } + + /** + * Get the link setting. + * + * @return array */ - public function __construct() { - $this->url = \admin_url( 'options-discussion.php' ); - $this->link_setting = [ + public function get_link_setting() { + return [ 'hook' => 'options-discussion.php', 'iconEl' => 'label[for="default_comment_status"]', ]; } /** - * Get the title. + * Get the task title. * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Disable comments', 'progress-planner' ); } /** - * Get the title. + * Get the task description. * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( \esc_html( // translators: %d is the number of approved comments, %s is the disabling them link. \_n( diff --git a/classes/suggested-tasks/providers/class-fewer-tags.php b/classes/suggested-tasks/providers/class-fewer-tags.php index fe479ffbc..0bdf17cf2 100644 --- a/classes/suggested-tasks/providers/class-fewer-tags.php +++ b/classes/suggested-tasks/providers/class-fewer-tags.php @@ -40,9 +40,9 @@ class Fewer_Tags extends Tasks { /** * The task priority. * - * @var string + * @var int */ - protected $priority = 'high'; + protected $priority = 10; /** * The plugin active state. @@ -72,6 +72,13 @@ class Fewer_Tags extends Tasks { */ private $plugin_path = 'fewer-tags/fewer-tags.php'; + /** + * Whether the task is dismissable. + * + * @var bool + */ + protected $is_dismissable = true; + /** * Constructor. */ @@ -79,9 +86,15 @@ public function __construct() { // Data collectors. $this->post_tag_count_data_collector = new Post_Tag_Count(); $this->published_post_count_data_collector = new Published_Post_Count(); + } - $this->url = \admin_url( '/plugin-install.php?tab=search&s=fewer+tags' ); - $this->is_dismissable = true; + /** + * Get the task URL. + * + * @return string + */ + protected function get_url() { + return \admin_url( '/plugin-install.php?tab=search&s=fewer+tags' ); } /** @@ -89,7 +102,7 @@ public function __construct() { * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Install Fewer Tags and clean up your tags', 'progress-planner' ); } @@ -98,8 +111,8 @@ public function get_title() { * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( // translators: %1$s is the number of tags, %2$s is the number of published posts, %3$s Read more link. \esc_html__( 'We detected that you have %1$s tags and %2$s published posts. Consider installing the "Fewer Tags" plugin. %3$s', 'progress-planner' ), $this->post_tag_count_data_collector->collect(), @@ -116,11 +129,7 @@ public function get_description() { */ public function should_add_task() { // If the plugin is active, we don't need to add the task. - if ( $this->is_plugin_active() ) { - return false; - } - - return $this->is_task_relevant(); + return $this->is_plugin_active() ? false : $this->is_task_relevant(); } /** @@ -149,14 +158,13 @@ public function is_task_completed( $task_id = '' ) { * @return bool */ protected function is_plugin_active() { - if ( null === $this->is_plugin_active ) { - if ( ! function_exists( 'get_plugins' ) ) { + if ( ! \function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; // @phpstan-ignore requireOnce.fileNotFound } - $plugins = get_plugins(); - $this->is_plugin_active = isset( $plugins[ $this->plugin_path ] ) && is_plugin_active( $this->plugin_path ); + $plugins = \get_plugins(); + $this->is_plugin_active = isset( $plugins[ $this->plugin_path ] ) && \is_plugin_active( $this->plugin_path ); } return $this->is_plugin_active; diff --git a/classes/suggested-tasks/providers/class-hello-world.php b/classes/suggested-tasks/providers/class-hello-world.php index 772cb7d67..8f2a46180 100644 --- a/classes/suggested-tasks/providers/class-hello-world.php +++ b/classes/suggested-tasks/providers/class-hello-world.php @@ -36,23 +36,35 @@ class Hello_World extends Tasks { protected const CAPABILITY = 'edit_posts'; /** - * The data collector. + * The data collector class name. * - * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Hello_World + * @var string */ - protected $data_collector; + protected const DATA_COLLECTOR_CLASS = Hello_World_Data_Collector::class; /** - * Constructor. + * Get the task URL. + * + * @return string */ - public function __construct() { - $this->data_collector = new Hello_World_Data_Collector(); + protected function get_url() { + $hello_world_post_id = $this->get_data_collector()->collect(); - $hello_world_post_id = $this->data_collector->collect(); - - if ( 0 !== $hello_world_post_id ) { - $this->url = (string) \get_edit_post_link( $hello_world_post_id ); + if ( 0 === $hello_world_post_id ) { + return ''; } + // We don't use the edit_post_link() function because we need to bypass it's current_user_can() check. + $this->url = \esc_url( + \add_query_arg( + [ + 'post' => $hello_world_post_id, + 'action' => 'edit', + ], + \admin_url( 'post.php' ) + ) + ); + + return $this->url; } /** @@ -60,7 +72,7 @@ public function __construct() { * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Delete the "Hello World!" post.', 'progress-planner' ); } @@ -69,8 +81,8 @@ public function get_title() { * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %s:Hello World! link */ \esc_html__( 'On install, WordPress creates a %s post. This post is not needed and should be deleted.', 'progress-planner' ), '' . \esc_html__( '"Hello World!"', 'progress-planner' ) . '' @@ -83,6 +95,6 @@ public function get_description() { * @return bool */ public function should_add_task() { - return 0 !== $this->data_collector->collect(); + return 0 !== $this->get_data_collector()->collect(); } } diff --git a/classes/suggested-tasks/providers/class-interactive.php b/classes/suggested-tasks/providers/class-interactive.php index 81a8de173..2885486ef 100644 --- a/classes/suggested-tasks/providers/class-interactive.php +++ b/classes/suggested-tasks/providers/class-interactive.php @@ -25,7 +25,7 @@ abstract class Interactive extends Tasks { * @return void */ public function __construct() { - add_action( 'progress_planner_admin_page_after_widgets', [ $this, 'add_popover' ] ); + \add_action( 'progress_planner_admin_page_after_widgets', [ $this, 'add_popover' ] ); } /** diff --git a/classes/suggested-tasks/providers/class-permalink-structure.php b/classes/suggested-tasks/providers/class-permalink-structure.php index 6749b0ba6..f0993387d 100644 --- a/classes/suggested-tasks/providers/class-permalink-structure.php +++ b/classes/suggested-tasks/providers/class-permalink-structure.php @@ -27,11 +27,20 @@ class Permalink_Structure extends Tasks { protected const PROVIDER_ID = 'core-permalink-structure'; /** - * Constructor. + * Get the task URL. + * + * @return string */ - public function __construct() { - $this->url = \admin_url( 'options-permalink.php' ); + protected function get_url() { + return \admin_url( 'options-permalink.php' ); + } + /** + * Get the link setting. + * + * @return array + */ + public function get_link_setting() { $icon_el = 'label[for="permalink-input-month-name"], label[for="permalink-input-post-name"]'; // If the task is completed, we want to add icon element only to the selected option (not both). @@ -47,7 +56,7 @@ public function __construct() { } } - $this->link_setting = [ + return [ 'hook' => 'options-permalink.php', 'iconEl' => $icon_el, ]; @@ -58,7 +67,7 @@ public function __construct() { * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Set permalink structure', 'progress-planner' ); } @@ -67,8 +76,8 @@ public function get_title() { * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %1$s We recommend link */ \esc_html__( 'On install, WordPress sets the permalink structure to a format that is not SEO-friendly. %1$s changing it.', 'progress-planner' ), '' . \esc_html__( 'We recommend', 'progress-planner' ) . '', diff --git a/classes/suggested-tasks/providers/class-php-version.php b/classes/suggested-tasks/providers/class-php-version.php index 8428d2a4a..8a97dc06f 100644 --- a/classes/suggested-tasks/providers/class-php-version.php +++ b/classes/suggested-tasks/providers/class-php-version.php @@ -26,26 +26,35 @@ class Php_Version extends Tasks { */ protected const PROVIDER_ID = 'php-version'; + /** + * The minimum PHP version. + * + * @var string + */ + protected const RECOMMENDED_PHP_VERSION = '8.2'; + /** * Get the title. * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Update PHP version', 'progress-planner' ); } /** * Get the description. * + * @param array $task_data Optional data to include in the task. * @return string */ - public function get_description() { - return sprintf( - /* translators: %1$s: php version, %2$s: We recommend link */ - \esc_html__( 'Your site is running on PHP version %1$s. %2$s updating to PHP version 8.0 or higher.', 'progress-planner' ), - phpversion(), + protected function get_description( $task_data = [] ) { + return \sprintf( + /* translators: %1$s: php version, %2$s: We recommend link. %3$s: minimum PHP version recommended. */ + \esc_html__( 'Your site is running on PHP version %1$s. %2$s updating to PHP version %3$s or higher.', 'progress-planner' ), + \phpversion(), '' . \esc_html__( 'We recommend', 'progress-planner' ) . '', + \esc_html( self::RECOMMENDED_PHP_VERSION ) ); } @@ -55,6 +64,6 @@ public function get_description() { * @return bool */ public function should_add_task() { - return version_compare( phpversion(), '8.0', '<' ); + return \version_compare( \phpversion(), self::RECOMMENDED_PHP_VERSION, '<' ); } } diff --git a/classes/suggested-tasks/providers/class-remove-inactive-plugins.php b/classes/suggested-tasks/providers/class-remove-inactive-plugins.php index 24ac53504..5145b0de1 100644 --- a/classes/suggested-tasks/providers/class-remove-inactive-plugins.php +++ b/classes/suggested-tasks/providers/class-remove-inactive-plugins.php @@ -29,18 +29,19 @@ class Remove_Inactive_Plugins extends Tasks { protected const IS_ONBOARDING_TASK = false; /** - * The data collector. + * The data collector class name. * - * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Inactive_Plugins + * @var string */ - protected $data_collector; + protected const DATA_COLLECTOR_CLASS = Inactive_Plugins_Data_Collector::class; /** - * Constructor. + * Get the task URL. + * + * @return string */ - public function __construct() { - $this->data_collector = new Inactive_Plugins_Data_Collector(); - $this->url = \admin_url( 'plugins.php' ); + protected function get_url() { + return \admin_url( 'plugins.php' ); } /** @@ -48,7 +49,7 @@ public function __construct() { * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Remove inactive plugins', 'progress-planner' ); } @@ -57,8 +58,8 @@ public function get_title() { * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %1$s removing any plugins link */ \esc_html__( 'You have inactive plugins. Consider %1$s that are not activated to free up resources, and improve security.', 'progress-planner' ), '' . \esc_html__( 'removing any plugins', 'progress-planner' ) . '', @@ -71,6 +72,6 @@ public function get_description() { * @return bool */ public function should_add_task() { - return $this->data_collector->collect() > 0; + return $this->get_data_collector()->collect() > 0; } } diff --git a/classes/suggested-tasks/providers/class-remove-terms-without-posts.php b/classes/suggested-tasks/providers/class-remove-terms-without-posts.php index 5e4ab6e0f..65e7c42fc 100644 --- a/classes/suggested-tasks/providers/class-remove-terms-without-posts.php +++ b/classes/suggested-tasks/providers/class-remove-terms-without-posts.php @@ -51,18 +51,18 @@ class Remove_Terms_Without_Posts extends Tasks { protected $is_dismissable = true; /** - * The task priority. + * The task URL target. * * @var string */ - protected $priority = 'medium'; + protected $url_target = '_blank'; /** - * The data collector. + * The task priority. * - * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Terms_Without_Posts + * @var int */ - protected $data_collector; + protected $priority = 60; /** * The minimum number of posts. @@ -71,6 +71,13 @@ class Remove_Terms_Without_Posts extends Tasks { */ protected const MIN_POSTS = 1; + /** + * The data collector class name. + * + * @var string + */ + protected const DATA_COLLECTOR_CLASS = Terms_Without_Posts_Data_Collector::class; + /** * The completed term IDs. * @@ -82,8 +89,6 @@ class Remove_Terms_Without_Posts extends Tasks { * Constructor. */ public function __construct() { - $this->data_collector = new Terms_Without_Posts_Data_Collector(); - \add_filter( 'progress_planner_terms_without_posts_exclude_term_ids', [ $this, 'exclude_completed_terms' ] ); } @@ -107,107 +112,77 @@ public function init() { * @return void */ public function maybe_remove_irrelevant_tasks( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) { - $pending_tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'provider_id', $this->get_provider_id() ); + $pending_tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'provider_id' => $this->get_provider_id() ] ); if ( ! $pending_tasks ) { return; } foreach ( $pending_tasks as $task ) { - if ( isset( $task['term_id'] ) && isset( $task['taxonomy'] ) ) { - $term = \get_term( $task['term_id'], $task['taxonomy'] ); + /** + * The task post object. + * + * @var \Progress_Planner\Suggested_Tasks\Task $task + */ + if ( $task->target_term_id && $task->target_taxonomy ) { + $term = \get_term( $task->target_term_id, $task->target_taxonomy ); if ( \is_wp_error( $term ) || ! $term || $term->count > self::MIN_POSTS ) { - \progress_planner()->get_suggested_tasks()->delete_task( $task['task_id'] ); + \progress_planner()->get_suggested_tasks_db()->delete_recommendation( $task->ID ); } } } } - /** - * Get the task ID. - * - * @param array $data Optional data to include in the task ID. - * @return string - */ - public function get_task_id( $data = [] ) { - $parts = [ $this->get_provider_id() ]; - - // Add optional data parts if provided. - if ( ! empty( $data ) ) { - foreach ( $data as $value ) { - $parts[] = $value; - } - } - - return implode( '-', $parts ); - } - /** * Get the title. * - * @param string $task_id The task ID. + * @param array $task_data The task data. * * @return string */ - public function get_title( $task_id = '' ) { - if ( ! $task_id ) { - return ''; - } - - // Get the task data. - $task_data = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $task_id ); - - // We don't want to link if the term was deleted. - if ( empty( $task_data ) || ! $task_data[0] ) { - return ''; - } - - return \sprintf( - /* translators: %s: The term name */ - \esc_html__( 'Remove term named "%s"', 'progress-planner' ), - \esc_html( $task_data[0]['term_name'] ) - ); + protected function get_title_with_data( $task_data = [] ) { + $term = \get_term( $task_data['target_term_id'], $task_data['target_taxonomy'] ); + return ( $term && ! \is_wp_error( $term ) ) + ? \sprintf( + /* translators: %s: The term name */ + \esc_html__( 'Remove term named "%s"', 'progress-planner' ), + \esc_html( $term->name ) + ) + : ''; } /** * Get the description. * - * @param string $task_id The task ID. + * @param array $task_data The task data. * * @return string */ - public function get_description( $task_id = '' ) { - $term = $this->get_term_from_task_id( $task_id ); - - if ( ! $term ) { - return ''; - } - - return sprintf( - /* translators: %1$s: The term name, %2$s Read more link */ - \esc_html__( 'The "%1$s" term has one or less posts associated with it, we recommend removing it. %2$s', 'progress-planner' ), - $term->name, - '' . \esc_html__( 'Read more', 'progress-planner' ) . '' - ); + protected function get_description_with_data( $task_data = [] ) { + $term = \get_term( $task_data['target_term_id'], $task_data['target_taxonomy'] ); + return ( $term && ! \is_wp_error( $term ) ) + ? \sprintf( + /* translators: %1$s: The term name, %2$s Read more link */ + \esc_html__( 'The "%1$s" term has one or less posts associated with it, we recommend removing it. %2$s', 'progress-planner' ), + $term->name, + '' . \esc_html__( 'Read more', 'progress-planner' ) . '' + ) + : ''; } /** * Get the URL. * - * @param string $task_id The task ID. + * @param array $task_data The task data. * * @return string */ - public function get_url( $task_id = '' ) { - $term = $this->get_term_from_task_id( $task_id ); - - // We don't want to link if the term was deleted. - if ( ! $term ) { - return ''; - } - - return \admin_url( 'term.php?taxonomy=' . $term->taxonomy . '&tag_ID=' . $term->term_id ); + protected function get_url_with_data( $task_data = [] ) { + $term = \get_term( $task_data['target_term_id'], $task_data['target_taxonomy'] ); + return ( $term && ! \is_wp_error( $term ) ) + ? \admin_url( 'term.php?taxonomy=' . $term->taxonomy . '&tag_ID=' . $term->term_id ) + : ''; } /** @@ -216,7 +191,7 @@ public function get_url( $task_id = '' ) { * @return bool */ public function should_add_task() { - return ! empty( $this->data_collector->collect() ); + return ! empty( $this->get_data_collector()->collect() ); } /** @@ -228,13 +203,24 @@ public function should_add_task() { */ protected function is_specific_task_completed( $task_id ) { $term = $this->get_term_from_task_id( $task_id ); + return $term ? self::MIN_POSTS < $term->count : true; + } - // Terms was deleted. - if ( ! $term ) { - return true; - } - - return self::MIN_POSTS < $term->count; + /** + * Transform data collector data into task data format. + * + * @param array $data The data from data collector. + * @return array The transformed data with original data merged. + */ + protected function transform_collector_data( array $data ): array { + return \array_merge( + $data, + [ + 'target_term_id' => $data['term_id'], + 'target_taxonomy' => $data['taxonomy'], + 'target_term_name' => $data['name'], + ] + ); } /** @@ -243,7 +229,6 @@ protected function is_specific_task_completed( $task_id ) { * @return array */ public function get_tasks_to_inject() { - if ( true === $this->is_task_snoozed() || ! $this->should_add_task() // No need to add the task. @@ -251,7 +236,7 @@ public function get_tasks_to_inject() { return []; } - $data = $this->data_collector->collect(); + $data = $this->get_data_collector()->collect(); $task_id = $this->get_task_id( [ 'term_id' => $data['term_id'], @@ -263,47 +248,36 @@ public function get_tasks_to_inject() { return []; } - return [ - [ - 'task_id' => $task_id, - 'provider_id' => $this->get_provider_id(), - 'category' => $this->get_provider_category(), - 'term_id' => $data['term_id'], - 'taxonomy' => $data['taxonomy'], - 'term_name' => $data['name'], - 'date' => \gmdate( 'YW' ), - ], - ]; + // Transform the data to match the task data structure. + $task_data = $this->modify_injection_task_data( + $this->get_task_details( + $this->transform_collector_data( $data ) + ) + ); + + // Get the task post. + $task_post = \progress_planner()->get_suggested_tasks_db()->get_post( $task_data['task_id'] ); + + // Skip the task if it was already injected. + return $task_post ? [] : [ \progress_planner()->get_suggested_tasks_db()->add( $task_data ) ]; } /** - * Get the task details. + * Modify task data before injecting it. * - * @param string $task_id The task ID. + * @param array $task_data The task data. * * @return array */ - public function get_task_details( $task_id = '' ) { + protected function modify_injection_task_data( $task_data ) { + // Transform the data to match the task data structure. + $data = $this->transform_collector_data( $this->get_data_collector()->collect() ); - if ( ! $task_id ) { - return []; - } + $task_data['target_term_id'] = $data['target_term_id']; + $task_data['target_taxonomy'] = $data['target_taxonomy']; + $task_data['target_term_name'] = $data['target_term_name']; - $task_details = [ - 'task_id' => $task_id, - 'provider_id' => $this->get_provider_id(), - 'title' => $this->get_title( $task_id ), - 'parent' => $this->get_parent(), - 'priority' => $this->get_priority(), - 'category' => $this->get_provider_category(), - 'points' => $this->get_points(), - 'dismissable' => $this->is_dismissable(), - 'url' => $this->get_url( $task_id ), - 'url_target' => $this->get_url_target(), - 'description' => $this->get_description( $task_id ), - ]; - - return $task_details; + return $task_data; } /** @@ -314,25 +288,20 @@ public function get_task_details( $task_id = '' ) { * @return \WP_Term|null */ public function get_term_from_task_id( $task_id ) { - $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $task_id ); + $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $task_id ] ); if ( empty( $tasks ) ) { return null; } - $data = $tasks[0]; + $task = $tasks[0]; - if ( ! isset( $data['term_id'] ) || ! $data['term_id'] || ! isset( $data['taxonomy'] ) || ! $data['taxonomy'] ) { + if ( ! $task->target_term_id || ! $task->target_taxonomy ) { return null; } - $term = \get_term( $data['term_id'], $data['taxonomy'] ); - - if ( is_wp_error( $term ) ) { - return null; - } - - return $term; + $term = \get_term( $task->target_term_id, $task->target_taxonomy ); + return $term && ! \is_wp_error( $term ) ? $term : null; } /** @@ -341,18 +310,17 @@ public function get_term_from_task_id( $task_id ) { * @return array */ protected function get_completed_term_ids() { - if ( null !== $this->completed_term_ids ) { return $this->completed_term_ids; } $this->completed_term_ids = []; - $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'provider_id', $this->get_provider_id() ); + $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'provider_id' => $this->get_provider_id() ] ); if ( ! empty( $tasks ) ) { foreach ( $tasks as $task ) { - if ( isset( $task['status'] ) && 'completed' === $task['status'] ) { - $this->completed_term_ids[] = $task['term_id']; + if ( 'trash' === $task->post_status ) { + $this->completed_term_ids[] = $task->target_term_id; } } } @@ -367,8 +335,6 @@ protected function get_completed_term_ids() { * @return array */ public function exclude_completed_terms( $exclude_term_ids ) { - $exclude_term_ids = array_merge( $exclude_term_ids, $this->get_completed_term_ids() ); - - return $exclude_term_ids; + return \array_merge( $exclude_term_ids, $this->get_completed_term_ids() ); } } diff --git a/classes/suggested-tasks/providers/class-rename-uncategorized-category.php b/classes/suggested-tasks/providers/class-rename-uncategorized-category.php index 44c61b796..d8516b1db 100644 --- a/classes/suggested-tasks/providers/class-rename-uncategorized-category.php +++ b/classes/suggested-tasks/providers/class-rename-uncategorized-category.php @@ -36,18 +36,19 @@ class Rename_Uncategorized_Category extends Tasks { protected const CAPABILITY = 'manage_categories'; /** - * The data collector. + * The data collector class name. * - * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Uncategorized_Category + * @var string */ - protected $data_collector; + protected const DATA_COLLECTOR_CLASS = Uncategorized_Category_Data_Collector::class; /** - * Constructor. + * Get the task URL. + * + * @return string */ - public function __construct() { - $this->data_collector = new Uncategorized_Category_Data_Collector(); - $this->url = \admin_url( 'edit-tags.php?taxonomy=category&post_type=post' ); + protected function get_url() { + return \admin_url( 'edit-tags.php?taxonomy=category&post_type=post' ); } /** @@ -55,7 +56,7 @@ public function __construct() { * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Rename Uncategorized category', 'progress-planner' ); } @@ -64,8 +65,8 @@ public function get_title() { * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %1$s We recommend link */ \esc_html__( 'The Uncategorized category is used for posts that don\'t have a category. %1$s renaming it to something that fits your site better.', 'progress-planner' ), '' . \esc_html__( 'We recommend', 'progress-planner' ) . '', @@ -78,7 +79,7 @@ public function get_description() { * @return bool */ public function should_add_task() { - return 0 !== $this->data_collector->collect(); + return 0 !== $this->get_data_collector()->collect(); } /** @@ -87,6 +88,6 @@ public function should_add_task() { * @return void */ public function update_uncategorized_category_cache() { - $this->data_collector->update_uncategorized_category_cache(); + $this->get_data_collector()->update_uncategorized_category_cache(); // @phpstan-ignore-line method.notFound } } diff --git a/classes/suggested-tasks/providers/class-sample-page.php b/classes/suggested-tasks/providers/class-sample-page.php index c1382f5c7..5991ccd5d 100644 --- a/classes/suggested-tasks/providers/class-sample-page.php +++ b/classes/suggested-tasks/providers/class-sample-page.php @@ -36,23 +36,34 @@ class Sample_Page extends Tasks { protected const CAPABILITY = 'edit_pages'; /** - * The data collector. + * The data collector class name. * - * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Sample_Page + * @var string */ - protected $data_collector; + protected const DATA_COLLECTOR_CLASS = Sample_Page_Data_Collector::class; /** - * Constructor. + * Get the task URL. + * + * @return string */ - public function __construct() { - $this->data_collector = new Sample_Page_Data_Collector(); - - $sample_page_id = $this->data_collector->collect(); + protected function get_url() { + $sample_page_id = $this->get_data_collector()->collect(); if ( 0 !== $sample_page_id ) { - $this->url = (string) \get_edit_post_link( $sample_page_id ); + // We don't use the edit_post_link() function because we need to bypass it's current_user_can() check. + $this->url = \esc_url( + \add_query_arg( + [ + 'post' => $sample_page_id, + 'action' => 'edit', + ], + \admin_url( 'post.php' ) + ) + ); } + + return $this->url; } /** @@ -60,7 +71,7 @@ public function __construct() { * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Delete "Sample Page"', 'progress-planner' ); } @@ -69,8 +80,8 @@ public function get_title() { * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %s:Sample Page link */ \esc_html__( 'On install, WordPress creates a %s page. This page is not needed and should be deleted.', 'progress-planner' ), '' . \esc_html__( '"Sample Page"', 'progress-planner' ) . '' @@ -83,6 +94,6 @@ public function get_description() { * @return bool */ public function should_add_task() { - return 0 !== $this->data_collector->collect(); + return 0 !== $this->get_data_collector()->collect(); } } diff --git a/classes/suggested-tasks/providers/class-search-engine-visibility.php b/classes/suggested-tasks/providers/class-search-engine-visibility.php index 12e2dafe0..d94ff4bde 100644 --- a/classes/suggested-tasks/providers/class-search-engine-visibility.php +++ b/classes/suggested-tasks/providers/class-search-engine-visibility.php @@ -27,32 +27,42 @@ class Search_Engine_Visibility extends Tasks { protected const PROVIDER_ID = 'search-engine-visibility'; /** - * Constructor. + * Get the task URL. + * + * @return string + */ + protected function get_url() { + return \admin_url( 'options-reading.php' ); + } + + /** + * Get the link setting. + * + * @return array */ - public function __construct() { - $this->url = \admin_url( 'options-reading.php' ); - $this->link_setting = [ + public function get_link_setting() { + return [ 'hook' => 'options-reading.php', 'iconEl' => 'label[for="blog_public"]', ]; } /** - * Get the title. + * Get the task title. * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Allow your site to be indexed by search engines', 'progress-planner' ); } /** - * Get the description. + * Get the task description. * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %1$s allowing search engines link */ \esc_html__( 'Your site is not currently visible to search engines. Consider %1$s to index your site.', 'progress-planner' ), '' . \esc_html__( 'allowing search engines', 'progress-planner' ) . '', diff --git a/classes/suggested-tasks/providers/class-set-valuable-post-types.php b/classes/suggested-tasks/providers/class-set-valuable-post-types.php index 1c96aac43..35d0db935 100644 --- a/classes/suggested-tasks/providers/class-set-valuable-post-types.php +++ b/classes/suggested-tasks/providers/class-set-valuable-post-types.php @@ -29,15 +29,17 @@ class Set_Valuable_Post_Types extends Tasks { /** * The task priority. * - * @var string + * @var int */ - protected $priority = 'low'; + protected $priority = 70; /** - * Constructor. + * Get the task URL. + * + * @return string */ - public function __construct() { - $this->url = \admin_url( 'admin.php?page=progress-planner-settings' ); + protected function get_url() { + return \admin_url( 'admin.php?page=progress-planner-settings' ); } /** @@ -65,7 +67,7 @@ public function remove_upgrade_option() { * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Set valuable content types', 'progress-planner' ); } @@ -74,8 +76,8 @@ public function get_title() { * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %s:Read more link */ \esc_html__( 'Tell us which post types matter most for your site. Go to your settings and select your valuable content types. %s', 'progress-planner' ), '' . \esc_html__( 'Read more', 'progress-planner' ) . '' @@ -84,21 +86,21 @@ public function get_description() { /** * Check if the task should be added. - * We add tasks only to users who have have completed "Fill the settings page" task and have upgraded from v1.2 or have 'include_post_types' option empty. - * Reason being that this option was migrated, but it could be missed, and post type selection should be revisited. + * We add tasks only to users who have have completed "Fill the settings page" task + * and have upgraded from v1.2 or have 'include_post_types' option empty. + * Reason being that this option was migrated, + * but it could be missed, and post type selection should be revisited. * * @return bool */ public function should_add_task() { - - // Check the "Settings saved" task, if the has not been added as 'pending' don't add the task. - $settings_saved_task = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'provider_id', 'settings-saved' ); - if ( empty( $settings_saved_task ) ) { + $saved_posts = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'provider_id' => 'settings-saved' ] ); + if ( empty( $saved_posts ) ) { return false; } - // Save settings task completed? - $save_settings_task_completed = 'completed' === $settings_saved_task[0]['status']; + // Is the task trashed? + $post_trashed = 'trash' === $saved_posts[0]->post_status; // Upgraded from <= 1.2? $upgraded = (bool) \get_option( 'progress_planner_set_valuable_post_types', false ); @@ -107,7 +109,7 @@ public function should_add_task() { $include_post_types = \progress_planner()->get_settings()->get( 'include_post_types', [] ); // Add the task only to users who have completed the "Settings saved" task and have upgraded from v1.2 or have 'include_post_types' option empty. - return $save_settings_task_completed && ( true === $upgraded || empty( $include_post_types ) ); + return $post_trashed && ( true === $upgraded || empty( $include_post_types ) ); } /** diff --git a/classes/suggested-tasks/providers/class-settings-saved.php b/classes/suggested-tasks/providers/class-settings-saved.php index 72357f69f..3112a49c4 100644 --- a/classes/suggested-tasks/providers/class-settings-saved.php +++ b/classes/suggested-tasks/providers/class-settings-saved.php @@ -22,9 +22,9 @@ class Settings_Saved extends Tasks { /** * The task priority. * - * @var string + * @var int */ - protected $priority = 'high'; + protected $priority = 1; /** * Whether the task is an onboarding task. @@ -34,10 +34,12 @@ class Settings_Saved extends Tasks { protected const IS_ONBOARDING_TASK = false; /** - * Constructor. + * Get the task URL. + * + * @return string */ - public function __construct() { - $this->url = \admin_url( 'admin.php?page=progress-planner-settings' ); + protected function get_url() { + return \admin_url( 'admin.php?page=progress-planner-settings' ); } /** @@ -45,7 +47,7 @@ public function __construct() { * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Fill settings page', 'progress-planner' ); } @@ -54,7 +56,7 @@ public function get_title() { * * @return string */ - public function get_description() { + protected function get_description() { return \esc_html__( 'Head over to the settings page and fill in the required information.', 'progress-planner' ); } diff --git a/classes/suggested-tasks/providers/class-site-icon.php b/classes/suggested-tasks/providers/class-site-icon.php index def8df8ae..619ac9371 100644 --- a/classes/suggested-tasks/providers/class-site-icon.php +++ b/classes/suggested-tasks/providers/class-site-icon.php @@ -27,32 +27,42 @@ class Site_Icon extends Tasks { protected const PROVIDER_ID = 'core-siteicon'; /** - * Constructor. + * Get the link setting. + * + * @return array */ - public function __construct() { - $this->url = \admin_url( 'options-general.php?pp-focus-el=' . $this->get_task_id() ); - $this->link_setting = [ + public function get_link_setting() { + return [ 'hook' => 'options-general.php', 'iconEl' => '.site-icon-section th', ]; } /** - * Get the title. + * Get the task URL. + * + * @return string + */ + protected function get_url() { + return \admin_url( 'options-general.php?pp-focus-el=' . $this->get_task_id() ); + } + + /** + * Get the task title. * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Set site icon', 'progress-planner' ); } /** - * Get the description. + * Get the task description. * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %s:site icon link */ \esc_html__( 'Set the %s to make your website look more professional.', 'progress-planner' ), '' . \esc_html__( 'site icon', 'progress-planner' ) . '' diff --git a/classes/suggested-tasks/providers/class-tasks.php b/classes/suggested-tasks/providers/class-tasks.php index 673d64754..e54a2bde0 100644 --- a/classes/suggested-tasks/providers/class-tasks.php +++ b/classes/suggested-tasks/providers/class-tasks.php @@ -8,7 +8,6 @@ namespace Progress_Planner\Suggested_Tasks\Providers; use Progress_Planner\Suggested_Tasks\Tasks_Interface; -use Progress_Planner\Suggested_Tasks\Task_Factory; /** * Add tasks for content updates. @@ -43,6 +42,13 @@ abstract class Tasks implements Tasks_Interface { */ protected const IS_ONBOARDING_TASK = false; + /** + * The data collector class name. + * + * @var string + */ + protected const DATA_COLLECTOR_CLASS = \Progress_Planner\Suggested_Tasks\Data_Collector\Base_Data_Collector::class; + /** * Whether the task is repetitive. * @@ -67,9 +73,9 @@ abstract class Tasks implements Tasks_Interface { /** * The task priority. * - * @var string + * @var int */ - protected $priority = 'medium'; + protected $priority = 50; /** * Whether the task is dismissable. @@ -78,6 +84,13 @@ abstract class Tasks implements Tasks_Interface { */ protected $is_dismissable = false; + /** + * Whether the task is snoozable. + * + * @var bool + */ + protected $is_snoozable = true; + /** * The task URL. * @@ -99,6 +112,13 @@ abstract class Tasks implements Tasks_Interface { */ protected $link_setting; + /** + * The data collector. + * + * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Base_Data_Collector|null + */ + protected $data_collector = null; + /** * Initialize the task provider. * @@ -112,7 +132,7 @@ public function init() { * * @return string */ - public function get_title() { + protected function get_title() { return ''; } @@ -121,7 +141,7 @@ public function get_title() { * * @return string */ - public function get_description() { + protected function get_description() { return ''; } @@ -146,10 +166,10 @@ public function get_parent() { /** * Get the task priority. * - * @return string + * @return int */ public function get_priority() { - return $this->priority; + return (int) $this->priority; } /** @@ -161,17 +181,22 @@ public function is_dismissable() { return $this->is_dismissable; } + /** + * Get whether the task is snoozable. + * + * @return bool + */ + public function is_snoozable() { + return $this->is_snoozable; + } + /** * Get the task URL. * * @return string */ - public function get_url() { - if ( $this->url ) { - return \esc_url( $this->url ); - } - - return ''; + protected function get_url() { + return $this->url ? \esc_url( $this->url ) : ''; } /** @@ -179,7 +204,7 @@ public function get_url() { * * @return string */ - public function get_url_target() { + protected function get_url_target() { return $this->url_target ? $this->url_target : '_self'; } @@ -198,7 +223,7 @@ public function get_link_setting() { * @return string */ public function get_provider_type() { - _deprecated_function( 'Progress_Planner\Suggested_Tasks\Providers\Tasks::get_provider_type()', '1.1.1', 'get_provider_category' ); + \_deprecated_function( 'Progress_Planner\Suggested_Tasks\Providers\Tasks::get_provider_type()', '1.1.1', 'get_provider_category' ); return $this->get_provider_category(); } @@ -223,25 +248,75 @@ public function get_provider_id() { /** * Get the task ID. * - * @param array $data Optional data to include in the task ID. + * @param array $task_data Optional data to include in the task ID. * @return string */ - public function get_task_id( $data = [] ) { - if ( ! $this->is_repetitive() ) { - return $this->get_provider_id(); + public function get_task_id( $task_data = [] ) { + $parts = [ $this->get_provider_id() ]; + + // Order is important here, new parameters should be added at the end. + if ( isset( $task_data['target_post_id'] ) ) { + $parts[] = $task_data['target_post_id']; } - $parts = [ $this->get_provider_id() ]; + if ( isset( $task_data['target_term_id'] ) ) { + $parts[] = $task_data['target_term_id']; + } + + if ( isset( $task_data['target_taxonomy'] ) ) { + $parts[] = $task_data['target_taxonomy']; + } - // Add optional data parts if provided. - if ( ! empty( $data['post_id'] ) ) { - $parts[] = $data['post_id']; + // If the task is repetitive, add the date as the last part. + if ( $this->is_repetitive() ) { + $parts[] = \gmdate( 'YW' ); } - // Always add the date as the last part. - $parts[] = \gmdate( 'YW' ); + return \implode( '-', $parts ); + } + + /** + * Get the data collector. + * + * @return \Progress_Planner\Suggested_Tasks\Data_Collector\Base_Data_Collector + */ + public function get_data_collector() { + if ( ! $this->data_collector ) { + $class_name = static::DATA_COLLECTOR_CLASS; + $this->data_collector = new $class_name(); // @phpstan-ignore-line assign.propertyType + } - return implode( '-', $parts ); + return $this->data_collector; // @phpstan-ignore-line return.type + } + + /** + * Get the title with data. + * + * @param array $task_data Optional data to include in the task. + * @return string + */ + protected function get_title_with_data( $task_data = [] ) { + return $this->get_title(); + } + + /** + * Get the description with data. + * + * @param array $task_data Optional data to include in the task. + * @return string + */ + protected function get_description_with_data( $task_data = [] ) { + return $this->get_description(); + } + + /** + * Get the URL with data. + * + * @param array $task_data Optional data to include in the task. + * @return string + */ + protected function get_url_with_data( $task_data = [] ) { + return $this->get_url(); } /** @@ -273,36 +348,20 @@ public function is_onboarding_task() { return static::IS_ONBOARDING_TASK; } - /** - * Get the data from a task-ID. - * - * @param string $task_id The task ID (unused here). - * - * @return array The data. - */ - public function get_data_from_task_id( $task_id ) { - $data = [ - 'provider_id' => $this->get_provider_id(), - 'id' => $task_id, - ]; - - return $data; - } - /** * Check if a task category is snoozed. * * @return bool */ public function is_task_snoozed() { - $snoozed = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'status', 'snoozed' ); + $snoozed = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'post_status' => 'future' ] ); if ( empty( $snoozed ) ) { return false; } foreach ( $snoozed as $task ) { - $task_object = Task_Factory::create_task_from( 'id', $task['task_id'] ); - $provider_id = $task_object->get_provider_id(); + $task = \progress_planner()->get_suggested_tasks_db()->get_post( $task->task_id ); + $provider_id = $task ? $task->get_provider_id() : ''; if ( $provider_id === $this->get_provider_id() ) { return true; @@ -328,7 +387,7 @@ public function is_task_relevant() { * * @param string $task_id The task ID. * - * @return false|\Progress_Planner\Suggested_Tasks\Task The task data or false if the task is not completed. + * @return \Progress_Planner\Suggested_Tasks\Task|false The task data or false if the task is not completed. */ public function evaluate_task( $task_id ) { // Early bail if the user does not have the capability to manage options. @@ -336,22 +395,32 @@ public function evaluate_task( $task_id ) { return false; } + $task = \progress_planner()->get_suggested_tasks_db()->get_post( $task_id ); + + if ( ! $task ) { + return false; + } + if ( ! $this->is_repetitive() ) { - if ( 0 !== strpos( $task_id, $this->get_task_id() ) ) { + // Collaborator tasks have custom task_ids, so strpos check does not work for them. + if ( ! $task->task_id || ( 0 !== \strpos( $task->task_id, $this->get_task_id() ) && 'collaborator' !== $this->get_provider_id() ) ) { return false; } - return $this->is_task_completed( $task_id ) ? Task_Factory::create_task_from( 'id', $task_id ) : false; + return $this->is_task_completed( $task->task_id ) ? $task : false; } - $task_object = Task_Factory::create_task_from( 'id', $task_id ); - $task_data = $task_object->get_data(); - - if ( $task_data['provider_id'] === $this->get_provider_id() && \gmdate( 'YW' ) === $task_data['date'] && $this->is_task_completed( $task_id ) ) { + if ( + $task->provider && + $task->provider->slug === $this->get_provider_id() && + \DateTime::createFromFormat( 'Y-m-d H:i:s', $task->post_date ) && + \gmdate( 'YW' ) === \gmdate( 'YW', \DateTime::createFromFormat( 'Y-m-d H:i:s', $task->post_date )->getTimestamp() ) && // @phpstan-ignore-line + $this->is_task_completed( $task->task_id ) + ) { // Allow adding more data, for example in case of 'create-post' tasks we are adding the post_id. - $task_data = $this->modify_evaluated_task_data( $task_data ); - $task_object->set_data( $task_data ); + $task_data = $this->modify_evaluated_task_data( $task->get_data() ); + $task->update( $task_data ); - return $task_object; + return $task; } return false; @@ -359,9 +428,8 @@ public function evaluate_task( $task_id ) { /** * Check if the task condition is satisfied. - * (bool) true means that the task condition is satisfied, meaning that we don't need to add the task or task was completed. * - * @return bool + * @return bool true means that the task condition is satisfied, meaning that we don't need to add the task or task was completed. */ abstract protected function should_add_task(); @@ -404,7 +472,6 @@ protected function check_task_condition() { * @return array */ public function get_tasks_to_inject() { - $task_id = $this->get_task_id(); if ( @@ -415,18 +482,13 @@ public function get_tasks_to_inject() { return []; } - $task_data = [ - 'task_id' => $task_id, - 'provider_id' => $this->get_provider_id(), - 'category' => $this->get_provider_category(), - 'date' => \gmdate( 'YW' ), - ]; + $task_data = $this->modify_injection_task_data( $this->get_task_details() ); - $task_data = $this->modify_injection_task_data( $task_data ); + // Get the task post. + $task_post = \progress_planner()->get_suggested_tasks_db()->get_post( $task_data['task_id'] ); - return [ - $task_data, - ]; + // Skip the task if it was already injected. + return $task_post ? [] : [ \progress_planner()->get_suggested_tasks_db()->add( $task_data ) ]; } /** @@ -456,24 +518,26 @@ protected function modify_evaluated_task_data( $task_data ) { /** * Get the task details. * - * @param string $task_id The task ID. + * @param array $task_data The task data. * * @return array */ - public function get_task_details( $task_id = '' ) { - + public function get_task_details( $task_data = [] ) { return [ - 'task_id' => $this->get_task_id(), + 'task_id' => $this->get_task_id( $task_data ), 'provider_id' => $this->get_provider_id(), - 'title' => $this->get_title(), + 'post_title' => $this->get_title_with_data( $task_data ), + 'description' => $this->get_description_with_data( $task_data ), 'parent' => $this->get_parent(), 'priority' => $this->get_priority(), 'category' => $this->get_provider_category(), 'points' => $this->get_points(), - 'url' => $this->capability_required() ? \esc_url( $this->get_url() ) : '', - 'description' => $this->get_description(), + 'date' => \gmdate( 'YW' ), + 'url' => $this->get_url_with_data( $task_data ), + 'url_target' => $this->get_url_target(), 'link_setting' => $this->get_link_setting(), 'dismissable' => $this->is_dismissable(), + 'snoozable' => $this->is_snoozable(), ]; } } diff --git a/classes/suggested-tasks/providers/class-update-term-description.php b/classes/suggested-tasks/providers/class-update-term-description.php index 931f9aac5..bebeb1a22 100644 --- a/classes/suggested-tasks/providers/class-update-term-description.php +++ b/classes/suggested-tasks/providers/class-update-term-description.php @@ -43,6 +43,13 @@ class Update_Term_Description extends Tasks { */ protected const CAPABILITY = 'edit_others_posts'; + /** + * The data collector class name. + * + * @var string + */ + protected const DATA_COLLECTOR_CLASS = Terms_Without_Description_Data_Collector::class; + /** * Whether the task is dismissable. * @@ -51,18 +58,18 @@ class Update_Term_Description extends Tasks { protected $is_dismissable = true; /** - * The task priority. + * The task URL target. * * @var string */ - protected $priority = 'low'; + protected $url_target = '_blank'; /** - * The data collector. + * The task priority. * - * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Terms_Without_Description + * @var int */ - protected $data_collector; + protected $priority = 80; /** * The completed term IDs. @@ -71,13 +78,6 @@ class Update_Term_Description extends Tasks { */ protected $completed_term_ids = null; - /** - * Constructor. - */ - public function __construct() { - $this->data_collector = new Terms_Without_Description_Data_Collector(); - } - /** * Initialize the task. */ @@ -99,106 +99,65 @@ public function init() { * @return void */ public function maybe_remove_irrelevant_tasks( $term, $tt_id, $taxonomy, $deleted_term, $object_ids ) { - $pending_tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'provider_id', $this->get_provider_id() ); + $pending_tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'provider_id' => $this->get_provider_id() ] ); if ( ! $pending_tasks ) { return; } foreach ( $pending_tasks as $task ) { - if ( isset( $task['term_id'] ) && isset( $task['taxonomy'] ) ) { - - if ( (int) $task['term_id'] === (int) $deleted_term->term_id ) { - \progress_planner()->get_suggested_tasks()->delete_task( $task['task_id'] ); - } - } - } - } - - /** - * Get the task ID. - * - * @param array $data Optional data to include in the task ID. - * @return string - */ - public function get_task_id( $data = [] ) { - $parts = [ $this->get_provider_id() ]; - - // Add optional data parts if provided. - if ( ! empty( $data ) ) { - foreach ( $data as $value ) { - $parts[] = $value; + if ( $task->target_term_id && $task->target_taxonomy && (int) $task->target_term_id === (int) $deleted_term->term_id ) { + \progress_planner()->get_suggested_tasks_db()->delete_recommendation( $task->ID ); } } - - return implode( '-', $parts ); } /** * Get the title. * - * @param string $task_id The task ID. + * @param array $task_data The task data. * * @return string */ - public function get_title( $task_id = '' ) { - if ( ! $task_id ) { - return ''; - } - - // Get the task data. - $task_data = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $task_id ); - - // We don't want to link if the term was deleted. - if ( empty( $task_data ) || ! $task_data[0] ) { - return ''; - } - - return \sprintf( + protected function get_title_with_data( $task_data = [] ) { + $term = \get_term( $task_data['target_term_id'], $task_data['target_taxonomy'] ); + return $term && ! \is_wp_error( $term ) ? \sprintf( /* translators: %s: The term name */ \esc_html__( 'Write a description for term named "%s"', 'progress-planner' ), - \esc_html( $task_data[0]['term_name'] ) - ); + \esc_html( $term->name ) + ) : ''; } /** * Get the description. * - * @param string $task_id The task ID. + * @param array $task_data The task data. * * @return string */ - public function get_description( $task_id = '' ) { - $term = $this->get_term_from_task_id( $task_id ); + public function get_description_with_data( $task_data = [] ) { + $term = \get_term( $task_data['target_term_id'], $task_data['target_taxonomy'] ); - if ( ! $term ) { - return ''; - } - - return sprintf( + return $term && ! \is_wp_error( $term ) ? \sprintf( /* translators: %1$s: The term name, %2$s Read more link */ \esc_html__( 'Your "%1$s" archives probably show the description of that specific term. %2$s', 'progress-planner' ), $term->name, '' . \esc_html__( 'Read more', 'progress-planner' ) . '' - ); + ) : ''; } /** * Get the URL. * - * @param string $task_id The task ID. + * @param array $task_data The task data. * * @return string */ - public function get_url( $task_id = '' ) { - $term = $this->get_term_from_task_id( $task_id ); - - // We don't want to link if the term was deleted. - if ( ! $term ) { - return ''; - } - - return \admin_url( 'term.php?taxonomy=' . $term->taxonomy . '&tag_ID=' . $term->term_id ); + protected function get_url_with_data( $task_data = [] ) { + $term = \get_term( $task_data['target_term_id'], $task_data['target_taxonomy'] ); + return $term && ! \is_wp_error( $term ) + ? \admin_url( 'term.php?taxonomy=' . $term->taxonomy . '&tag_ID=' . $term->term_id ) + : ''; } /** @@ -207,7 +166,7 @@ public function get_url( $task_id = '' ) { * @return bool */ public function should_add_task() { - return ! empty( $this->data_collector->collect() ); + return ! empty( $this->get_data_collector()->collect() ); } /** @@ -225,26 +184,39 @@ protected function is_specific_task_completed( $task_id ) { return true; } - $term_description = trim( $term->description ); + $term_description = \trim( $term->description ); return '' !== $term_description && ' ' !== $term_description; } + /** + * Transform data collector data into task data format. + * + * @param array $data The data from data collector. + * @return array The transformed data with original data merged. + */ + protected function transform_collector_data( array $data ): array { + return \array_merge( + $data, + [ + 'target_term_id' => $data['term_id'], + 'target_taxonomy' => $data['taxonomy'], + 'target_term_name' => $data['name'], + ] + ); + } + /** * Get an array of tasks to inject. * * @return array */ public function get_tasks_to_inject() { - - if ( - true === $this->is_task_snoozed() || - ! $this->should_add_task() // No need to add the task. - ) { + if ( true === $this->is_task_snoozed() || ! $this->should_add_task() ) { return []; } - $data = $this->data_collector->collect(); + $data = $this->get_data_collector()->collect(); $task_id = $this->get_task_id( [ 'term_id' => $data['term_id'], @@ -256,47 +228,40 @@ public function get_tasks_to_inject() { return []; } - return [ - [ - 'task_id' => $task_id, - 'provider_id' => $this->get_provider_id(), - 'category' => $this->get_provider_category(), - 'term_id' => $data['term_id'], - 'taxonomy' => $data['taxonomy'], - 'term_name' => $data['name'], - 'date' => \gmdate( 'YW' ), - ], - ]; + // Transform the data to match the task data structure. + $task_data = $this->modify_injection_task_data( + $this->get_task_details( + $this->transform_collector_data( $data ) + ) + ); + + // Get the task post. + $task_post = \progress_planner()->get_suggested_tasks_db()->get_post( $task_data['task_id'] ); + + // Skip the task if it was already injected. + if ( $task_post ) { + return []; + } + + return [ \progress_planner()->get_suggested_tasks_db()->add( $task_data ) ]; } /** - * Get the task details. + * Modify task data before injecting it. * - * @param string $task_id The task ID. + * @param array $task_data The task data. * * @return array */ - public function get_task_details( $task_id = '' ) { + protected function modify_injection_task_data( $task_data ) { + // Transform the data to match the task data structure. + $data = $this->transform_collector_data( $this->get_data_collector()->collect() ); - if ( ! $task_id ) { - return []; - } + $task_data['target_term_id'] = $data['target_term_id']; + $task_data['target_taxonomy'] = $data['target_taxonomy']; + $task_data['target_term_name'] = $data['target_term_name']; - $task_details = [ - 'task_id' => $task_id, - 'provider_id' => $this->get_provider_id(), - 'title' => $this->get_title( $task_id ), - 'parent' => $this->get_parent(), - 'priority' => $this->get_priority(), - 'category' => $this->get_provider_category(), - 'points' => $this->get_points(), - 'dismissable' => $this->is_dismissable(), - 'url' => $this->get_url( $task_id ), - 'url_target' => $this->get_url_target(), - 'description' => $this->get_description( $task_id ), - ]; - - return $task_details; + return $task_data; } /** @@ -307,25 +272,20 @@ public function get_task_details( $task_id = '' ) { * @return \WP_Term|null */ public function get_term_from_task_id( $task_id ) { - $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $task_id ); + $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $task_id ] ); if ( empty( $tasks ) ) { return null; } - $data = $tasks[0]; - - if ( ! isset( $data['term_id'] ) || ! $data['term_id'] || ! isset( $data['taxonomy'] ) || ! $data['taxonomy'] ) { - return null; - } - - $term = \get_term( $data['term_id'], $data['taxonomy'] ); + $task = $tasks[0]; - if ( is_wp_error( $term ) ) { + if ( ! $task->target_term_id || ! $task->target_taxonomy ) { return null; } - return $term; + $term = \get_term( $task->target_term_id, $task->target_taxonomy ); + return $term && ! \is_wp_error( $term ) ? $term : null; } /** @@ -334,18 +294,17 @@ public function get_term_from_task_id( $task_id ) { * @return array */ protected function get_completed_term_ids() { - if ( null !== $this->completed_term_ids ) { return $this->completed_term_ids; } $this->completed_term_ids = []; - $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'provider_id', $this->get_provider_id() ); + $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'provider_id' => $this->get_provider_id() ] ); if ( ! empty( $tasks ) ) { foreach ( $tasks as $task ) { - if ( isset( $task['status'] ) && 'completed' === $task['status'] ) { - $this->completed_term_ids[] = $task['term_id']; + if ( 'trash' === $task->post_status ) { + $this->completed_term_ids[] = $task->target_term_id; } } } @@ -360,8 +319,6 @@ protected function get_completed_term_ids() { * @return array */ public function exclude_completed_terms( $exclude_term_ids ) { - $exclude_term_ids = array_merge( $exclude_term_ids, $this->get_completed_term_ids() ); - - return $exclude_term_ids; + return \array_merge( $exclude_term_ids, $this->get_completed_term_ids() ); } } diff --git a/classes/suggested-tasks/providers/class-user.php b/classes/suggested-tasks/providers/class-user.php index b59bfe3e2..fad97673b 100644 --- a/classes/suggested-tasks/providers/class-user.php +++ b/classes/suggested-tasks/providers/class-user.php @@ -12,6 +12,20 @@ */ class User extends Tasks { + /** + * Whether the task is dismissable. + * + * @var bool + */ + protected $is_dismissable = true; + + /** + * Whether the task is snoozable. + * + * @var bool + */ + protected $is_snoozable = false; + /** * Whether the task is an onboarding task. * @@ -48,59 +62,19 @@ public function should_add_task() { * @return array */ public function get_tasks_to_inject() { - - $tasks = []; - $saved_tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); - foreach ( $saved_tasks as $task_data ) { - if ( isset( $task_data['provider_id'] ) && self::PROVIDER_ID === $task_data['provider_id'] ) { - $tasks[] = [ - 'task_id' => $task_data['task_id'], - 'provider_id' => $this->get_provider_id(), - 'category' => $this->get_provider_category(), - 'points' => 0, - ]; - } - } - - return $tasks; + return []; } /** * Get the task details. * - * @param string $task_id The task ID. + * @param array $task_data Optional data to include in the task. * * @return array */ - public function get_task_details( $task_id = '' ) { + public function get_task_details( $task_data = [] ) { // Get the user tasks from the database. - $tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); - - foreach ( $tasks as $task ) { - if ( $task['task_id'] !== $task_id ) { - continue; - } - - return wp_parse_args( - $task, - [ - 'task_id' => '', - 'title' => '', - 'parent' => 0, - 'provider_id' => 'user', - 'category' => 'user', - 'priority' => 'medium', - 'points' => 0, - 'url' => '', - 'url_target' => '_self', - 'description' => '', - 'link_setting' => [], - 'dismissable' => true, - 'snoozable' => false, - ] - ); - } - - return []; + $task_post = \progress_planner()->get_suggested_tasks_db()->get_post( $task_data['task_id'] ); + return $task_post ? $task_post->get_data() : []; } } diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-add-yoast-providers.php b/classes/suggested-tasks/providers/integrations/yoast/class-add-yoast-providers.php index 01f04e0b2..bf85f0a14 100644 --- a/classes/suggested-tasks/providers/integrations/yoast/class-add-yoast-providers.php +++ b/classes/suggested-tasks/providers/integrations/yoast/class-add-yoast-providers.php @@ -23,9 +23,8 @@ class Add_Yoast_Providers { * Constructor. */ public function __construct() { - if ( function_exists( 'YoastSEO' ) ) { + if ( \function_exists( 'YoastSEO' ) ) { \add_filter( 'progress_planner_suggested_tasks_providers', [ $this, 'add_providers' ], 11, 1 ); - \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); } } @@ -45,14 +44,13 @@ public function enqueue_assets( $hook ) { $focus_tasks = []; foreach ( $this->providers as $provider ) { - - // Add Ravi icon if the task is pending or is completed. + // Add Ravi icon if the task is published or is completed. if ( $provider->is_task_relevant() || \progress_planner()->get_suggested_tasks()->was_task_completed( $provider->get_task_id() ) ) { - if ( method_exists( $provider, 'get_focus_tasks' ) ) { + if ( \method_exists( $provider, 'get_focus_tasks' ) ) { $focus_task = $provider->get_focus_tasks(); if ( $focus_task ) { - $focus_tasks = array_merge( $focus_tasks, $focus_task ); + $focus_tasks = \array_merge( $focus_tasks, $focus_task ); } } } @@ -65,7 +63,7 @@ public function enqueue_assets( $hook ) { 'name' => 'progressPlannerYoastFocusElement', 'data' => [ 'tasks' => $focus_tasks, - 'base_url' => constant( 'PROGRESS_PLANNER_URL' ), + 'base_url' => \constant( 'PROGRESS_PLANNER_URL' ), ], ] ); @@ -80,7 +78,6 @@ public function enqueue_assets( $hook ) { * @return array */ public function add_providers( $providers ) { - $this->providers = [ new Archive_Author(), new Archive_Date(), @@ -94,12 +91,12 @@ public function add_providers( $providers ) { ]; // Yoast SEO Premium. - if ( defined( 'WPSEO_PREMIUM_VERSION' ) ) { + if ( \defined( 'WPSEO_PREMIUM_VERSION' ) ) { $this->providers[] = new Cornerstone_Workout(); $this->providers[] = new Orphaned_Content_Workout(); } - return array_merge( + return \array_merge( $providers, $this->providers ); diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-archive-author.php b/classes/suggested-tasks/providers/integrations/yoast/class-archive-author.php index 644a52932..02cde70a7 100644 --- a/classes/suggested-tasks/providers/integrations/yoast/class-archive-author.php +++ b/classes/suggested-tasks/providers/integrations/yoast/class-archive-author.php @@ -29,18 +29,19 @@ class Archive_Author extends Yoast_Provider { protected const PROVIDER_ID = 'yoast-author-archive'; /** - * The data collector. + * The data collector class name. * - * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Post_Author + * @var string */ - protected $data_collector; + protected const DATA_COLLECTOR_CLASS = Post_Author::class; /** - * Constructor. + * Get the task URL. + * + * @return string */ - public function __construct() { - $this->data_collector = new Post_Author(); - $this->url = \admin_url( 'admin.php?page=wpseo_page_settings#/author-archives' ); + protected function get_url() { + return \admin_url( 'admin.php?page=wpseo_page_settings#/author-archives' ); } /** @@ -48,7 +49,7 @@ public function __construct() { * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Yoast SEO: disable the author archive', 'progress-planner' ); } @@ -57,8 +58,8 @@ public function get_title() { * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %s: "Read more" link. */ \esc_html__( 'Yoast SEO can disable the author archive when you have only one author, as it is the same as the homepage. %s.', 'progress-planner' ), '' . \esc_html__( 'Read more', 'progress-planner' ) . '' @@ -90,13 +91,12 @@ public function get_focus_tasks() { * @return bool */ public function should_add_task() { - if ( ! $this->is_task_relevant() ) { return false; } // If the author archive is already disabled, we don't need to add the task. - if ( YoastSEO()->helpers->options->get( 'disable-author' ) === true ) { + if ( \YoastSEO()->helpers->options->get( 'disable-author' ) === true ) { return false; } @@ -112,10 +112,6 @@ public function should_add_task() { */ public function is_task_relevant() { // If there is more than one author, we don't need to add the task. - if ( $this->data_collector->collect() > self::MINIMUM_AUTHOR_WITH_POSTS ) { - return false; - } - - return true; + return $this->get_data_collector()->collect() <= self::MINIMUM_AUTHOR_WITH_POSTS; } } diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-archive-date.php b/classes/suggested-tasks/providers/integrations/yoast/class-archive-date.php index 9612fc173..e5f0eba52 100644 --- a/classes/suggested-tasks/providers/integrations/yoast/class-archive-date.php +++ b/classes/suggested-tasks/providers/integrations/yoast/class-archive-date.php @@ -20,10 +20,12 @@ class Archive_Date extends Yoast_Provider { protected const PROVIDER_ID = 'yoast-date-archive'; /** - * Constructor. + * Get the task URL. + * + * @return string */ - public function __construct() { - $this->url = \admin_url( 'admin.php?page=wpseo_page_settings#/date-archives' ); + protected function get_url() { + return \admin_url( 'admin.php?page=wpseo_page_settings#/date-archives' ); } /** @@ -31,7 +33,7 @@ public function __construct() { * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Yoast SEO: disable the date archive', 'progress-planner' ); } @@ -40,8 +42,8 @@ public function get_title() { * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %s: "Read more" link. */ \esc_html__( 'Yoast SEO can disable the date archive, which is really only useful for news sites and blogs. %s.', 'progress-planner' ), '' . \esc_html__( 'Read more', 'progress-planner' ) . '' @@ -73,13 +75,8 @@ public function get_focus_tasks() { * @return bool */ public function should_add_task() { - - if ( ! $this->is_task_relevant() ) { - return false; - } - // If the date archive is already disabled, we don't need to add the task. - return YoastSEO()->helpers->options->get( 'disable-date' ) !== true; + return $this->is_task_relevant() && \YoastSEO()->helpers->options->get( 'disable-date' ) !== true; } /** @@ -92,10 +89,8 @@ public function should_add_task() { public function is_task_relevant() { // If the permalink structure includes %year%, %monthnum%, or %day%, we don't need to add the task. $permalink_structure = \get_option( 'permalink_structure' ); - if ( strpos( $permalink_structure, '%year%' ) !== false || strpos( $permalink_structure, '%monthnum%' ) !== false || strpos( $permalink_structure, '%day%' ) !== false ) { - return false; - } - - return true; + return \strpos( $permalink_structure, '%year%' ) === false + && \strpos( $permalink_structure, '%monthnum%' ) === false + && \strpos( $permalink_structure, '%day%' ) === false; } } diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-archive-format.php b/classes/suggested-tasks/providers/integrations/yoast/class-archive-format.php index fe92ed42c..69bcc55a0 100644 --- a/classes/suggested-tasks/providers/integrations/yoast/class-archive-format.php +++ b/classes/suggested-tasks/providers/integrations/yoast/class-archive-format.php @@ -29,18 +29,19 @@ class Archive_Format extends Yoast_Provider { protected const MINIMUM_POSTS_WITH_FORMAT = 3; /** - * The data collector. + * The data collector class name. * - * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Archive_Format + * @var string */ - protected $data_collector; + protected const DATA_COLLECTOR_CLASS = Archive_Format_Data_Collector::class; /** - * Constructor. + * Get the task URL. + * + * @return string */ - public function __construct() { - $this->data_collector = new Archive_Format_Data_Collector(); - $this->url = \admin_url( 'admin.php?page=wpseo_page_settings#/format-archives' ); + protected function get_url() { + return \admin_url( 'admin.php?page=wpseo_page_settings#/format-archives' ); } /** @@ -48,7 +49,7 @@ public function __construct() { * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Yoast SEO: disable the format archives', 'progress-planner' ); } @@ -57,8 +58,8 @@ public function get_title() { * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %s: "Read more" link. */ \esc_html__( 'WordPress creates an archive for each post format. This is not useful and can be disabled in the Yoast SEO settings. %s.', 'progress-planner' ), '' . \esc_html__( 'Read more', 'progress-planner' ) . '' @@ -90,16 +91,8 @@ public function get_focus_tasks() { * @return bool */ public function should_add_task() { - if ( ! $this->is_task_relevant() ) { - return false; - } - - // If the post format archive is already disabled, we don't need to add the task. - if ( YoastSEO()->helpers->options->get( 'disable-post_format' ) === true ) { - return false; - } - - return true; + return $this->is_task_relevant() + && \YoastSEO()->helpers->options->get( 'disable-post_format' ) !== true; } /** @@ -110,13 +103,7 @@ public function should_add_task() { * @return bool */ public function is_task_relevant() { - $archive_format_count = $this->data_collector->collect(); - // If there are more than X posts with a post format, we don't need to add the task. X is set in the class. - if ( $archive_format_count > static::MINIMUM_POSTS_WITH_FORMAT ) { - return false; - } - - return true; + return $this->get_data_collector()->collect() <= static::MINIMUM_POSTS_WITH_FORMAT; } } diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-cornerstone-workout.php b/classes/suggested-tasks/providers/integrations/yoast/class-cornerstone-workout.php index 808e44671..dee134a0e 100644 --- a/classes/suggested-tasks/providers/integrations/yoast/class-cornerstone-workout.php +++ b/classes/suggested-tasks/providers/integrations/yoast/class-cornerstone-workout.php @@ -33,9 +33,9 @@ class Cornerstone_Workout extends Yoast_Provider { /** * The task priority. * - * @var string + * @var int */ - protected $priority = 'low'; + protected $priority = 90; /** * Whether the task is dismissable. @@ -84,22 +84,22 @@ public function maybe_update_workout_status( $old_value, $value, $option ) { return; } - // Check if there is pending task. - $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $this->get_task_id() ); + // Check if there is a published task. + $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $this->get_task_id() ] ); - // If there is no pending task, return. - if ( empty( $tasks ) || 'pending' !== $tasks[0]['status'] ) { + // If there is no published task, return. + if ( empty( $tasks ) || 'publish' !== $tasks[0]->post_status ) { return; } // For this type of task only the provider ID is needed, but just in case. - if ( $this->is_task_dismissed( $tasks[0] ) ) { + if ( $this->is_task_dismissed( $tasks[0]->get_data() ) ) { return; } // There should be 3 steps in the workout. - $workout_was_completed = 3 === count( $old_value['workouts']['cornerstone']['finishedSteps'] ); - $workout_completed = 3 === count( $value['workouts']['cornerstone']['finishedSteps'] ); + $workout_was_completed = 3 === \count( $old_value['workouts']['cornerstone']['finishedSteps'] ); + $workout_completed = 3 === \count( $value['workouts']['cornerstone']['finishedSteps'] ); // Dismiss the task if workout wasn't completed before and now is. if ( ! $workout_was_completed && $workout_completed ) { @@ -110,23 +110,19 @@ public function maybe_update_workout_status( $old_value, $value, $option ) { /** * Get the task title. * - * @param string $task_id The task ID. - * * @return string */ - public function get_title( $task_id = '' ) { + protected function get_title() { return \esc_html__( 'Yoast SEO: do Yoast SEO\'s Cornerstone Content Workout', 'progress-planner' ); } /** * Get the task description. * - * @param string $task_id The task ID. - * * @return string */ - public function get_description( $task_id = '' ) { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %s: "Read more" link. */ \esc_html__( 'Improve your most important pages with Yoast SEO\'s Cornerstone Content Workout. %s.', 'progress-planner' ), '' . \esc_html__( 'Learn more', 'progress-planner' ) . '' @@ -136,12 +132,10 @@ public function get_description( $task_id = '' ) { /** * Get the task URL. * - * @param string $task_id The task ID. - * * @return string */ - public function get_url( $task_id = '' ) { - return $this->capability_required() ? \esc_url( admin_url( 'admin.php?page=wpseo_workouts#cornerstone' ) ) : ''; + protected function get_url() { + return \esc_url( \admin_url( 'admin.php?page=wpseo_workouts#cornerstone' ) ); } /** @@ -150,46 +144,14 @@ public function get_url( $task_id = '' ) { * @return bool */ public function should_add_task() { - if ( ! defined( 'WPSEO_PREMIUM_VERSION' ) ) { - return false; - } - - $task_data = [ - 'provider_id' => $this->get_provider_id(), - ]; - - // Skip if the task has been dismissed. - if ( $this->is_task_dismissed( $task_data ) ) { + if ( ! \defined( 'WPSEO_PREMIUM_VERSION' ) ) { return false; } - return true; - } - - /** - * Get the task details. - * - * @param string $task_id The task ID. - * - * @return array - */ - public function get_task_details( $task_id = '' ) { - if ( ! $task_id ) { - return []; - } - - return [ - 'task_id' => $task_id, - 'provider_id' => $this->get_provider_id(), - 'title' => $this->get_title( $task_id ), - 'parent' => $this->get_parent(), - 'priority' => $this->get_priority(), - 'category' => $this->get_provider_category(), - 'points' => $this->get_points(), - 'dismissable' => $this->is_dismissable, - 'url' => $this->get_url( $task_id ), - 'url_target' => $this->get_url_target(), - 'description' => $this->get_description( $task_id ), - ]; + return ! $this->is_task_dismissed( + [ + 'provider_id' => $this->get_provider_id(), + ] + ); } } diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-emoji-scripts.php b/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-emoji-scripts.php index b270341fd..4c1675d62 100644 --- a/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-emoji-scripts.php +++ b/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-emoji-scripts.php @@ -20,28 +20,30 @@ class Crawl_Settings_Emoji_Scripts extends Yoast_Provider { protected const PROVIDER_ID = 'yoast-crawl-settings-emoji-scripts'; /** - * Constructor. + * Get the task URL. + * + * @return string */ - public function __construct() { - $this->url = \admin_url( 'admin.php?page=wpseo_page_settings#/crawl-optimization#input-wpseo-remove_emoji_scripts' ); + protected function get_url() { + return \admin_url( 'admin.php?page=wpseo_page_settings#/crawl-optimization#input-wpseo-remove_emoji_scripts' ); } /** - * Get the title. + * Get the task title. * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Yoast SEO: remove emoji scripts', 'progress-planner' ); } /** - * Get the description. + * Get the task description. * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %s: "Read more" link. */ \esc_html__( 'Remove JavaScript used for converting emoji characters in older browsers. %s.', 'progress-planner' ), '' . \esc_html__( 'Read more', 'progress-planner' ) . '' diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-feed-authors.php b/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-feed-authors.php index b5b4dcba8..abcad1074 100644 --- a/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-feed-authors.php +++ b/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-feed-authors.php @@ -29,36 +29,37 @@ class Crawl_Settings_Feed_Authors extends Yoast_Provider { protected const PROVIDER_ID = 'yoast-crawl-settings-feed-authors'; /** - * The data collector. + * The data collector class name. * - * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Post_Author + * @var string */ - protected $data_collector; + protected const DATA_COLLECTOR_CLASS = Post_Author::class; /** - * Constructor. + * Get the task URL. + * + * @return string */ - public function __construct() { - $this->data_collector = new Post_Author(); - $this->url = \admin_url( 'admin.php?page=wpseo_page_settings#/crawl-optimization#input-wpseo-remove_feed_authors' ); + protected function get_url() { + return \admin_url( 'admin.php?page=wpseo_page_settings#/crawl-optimization#input-wpseo-remove_feed_authors' ); } /** - * Get the title. + * Get the task title. * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Yoast SEO: remove post authors feeds', 'progress-planner' ); } /** - * Get the description. + * Get the task description. * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %s: "Read more" link. */ \esc_html__( 'Remove URLs which provide information about recent posts by specific authors. %s.', 'progress-planner' ), '' . \esc_html__( 'Read more', 'progress-planner' ) . '' @@ -90,7 +91,6 @@ public function get_focus_tasks() { * @return bool */ public function should_add_task() { - if ( ! $this->is_task_relevant() ) { return false; } @@ -115,10 +115,6 @@ public function should_add_task() { */ public function is_task_relevant() { // If there is more than one author, we don't need to add the task. - if ( $this->data_collector->collect() > self::MINIMUM_AUTHOR_WITH_POSTS ) { - return false; - } - - return true; + return $this->get_data_collector()->collect() <= self::MINIMUM_AUTHOR_WITH_POSTS; } } diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-feed-global-comments.php b/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-feed-global-comments.php index 0e71abc81..a4d512ad5 100644 --- a/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-feed-global-comments.php +++ b/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-feed-global-comments.php @@ -20,10 +20,12 @@ class Crawl_Settings_Feed_Global_Comments extends Yoast_Provider { protected const PROVIDER_ID = 'yoast-crawl-settings-feed-global-comments'; /** - * Constructor. + * Get the task URL. + * + * @return string */ - public function __construct() { - $this->url = \admin_url( 'admin.php?page=wpseo_page_settings#/crawl-optimization#input-wpseo-remove_feed_global_comments' ); + protected function get_url() { + return \admin_url( 'admin.php?page=wpseo_page_settings#/crawl-optimization#input-wpseo-remove_feed_global_comments' ); } /** @@ -31,7 +33,7 @@ public function __construct() { * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Yoast SEO: remove global comment feeds', 'progress-planner' ); } @@ -40,8 +42,8 @@ public function get_title() { * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %s: "Read more" link. */ \esc_html__( 'Remove URLs which provide an overview of recent comments on your site. %s.', 'progress-planner' ), '' . \esc_html__( 'Read more', 'progress-planner' ) . '' diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-fix-orphaned-content.php b/classes/suggested-tasks/providers/integrations/yoast/class-fix-orphaned-content.php index 0dd818787..2ea2b4ede 100644 --- a/classes/suggested-tasks/providers/integrations/yoast/class-fix-orphaned-content.php +++ b/classes/suggested-tasks/providers/integrations/yoast/class-fix-orphaned-content.php @@ -36,13 +36,6 @@ class Fix_Orphaned_Content extends Yoast_Provider { */ protected const PROVIDER_ID = 'yoast-fix-orphaned-content'; - /** - * The data collector. - * - * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Yoast_Orphaned_Content - */ - protected $data_collector; - /** * Whether the task is dismissable. * @@ -58,11 +51,11 @@ class Fix_Orphaned_Content extends Yoast_Provider { protected $completed_post_ids = null; /** - * Constructor. + * The data collector class name. + * + * @var string */ - public function __construct() { - $this->data_collector = new Yoast_Orphaned_Content(); - } + protected const DATA_COLLECTOR_CLASS = Yoast_Orphaned_Content::class; /** * Initialize the task provider. @@ -74,55 +67,27 @@ public function init() { } /** - * Get the task ID. - * - * @param array $data Optional data to include in the task ID. - * @return string - */ - public function get_task_id( $data = [] ) { - $parts = [ $this->get_provider_id() ]; - - // Add optional data parts if provided. - if ( ! empty( $data ) ) { - foreach ( $data as $value ) { - $parts[] = $value; - } - } - - return implode( '-', $parts ); - } - - /** - * Get the title. + * Get the title with data. * - * @param string $task_id The task ID. + * @param array $task_data The task data. * * @return string */ - public function get_title( $task_id = '' ) { - // Get the task data. - $task_data = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $task_id ); - - // We don't want to link if the term was deleted. - if ( empty( $task_data ) || ! $task_data[0] ) { - return ''; - } - - return sprintf( + protected function get_title_with_data( $task_data = [] ) { + return \sprintf( /* translators: %s: Post title. */ \esc_html__( 'Yoast SEO: add internal links to article "%s"!', 'progress-planner' ), - \esc_html( $task_data[0]['post_title'] ) + \esc_html( $task_data['target_post_title'] ) ); } /** * Get the description. * - * @param string $task_id The task ID. * @return string */ - public function get_description( $task_id = '' ) { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %s: "Read more" link. */ \esc_html__( 'Yoast SEO detected that this article has no links pointing to it. %s.', 'progress-planner' ), '' . \esc_html__( 'Read more', 'progress-planner' ) . '' @@ -132,19 +97,12 @@ public function get_description( $task_id = '' ) { /** * Get the URL. * - * @param string $task_id The task ID. + * @param array $task_data The task data. * * @return string */ - public function get_url( $task_id = '' ) { - $post = $this->get_post_from_task_id( $task_id ); - - // We don't want to link if the post was deleted. - if ( ! $post ) { - return ''; - } - - return 'https://prpl.fyi/fix-orphaned-content'; + protected function get_url_with_data( $task_data = [] ) { + return \get_post( $task_data['target_post_id'] ) ? 'https://prpl.fyi/fix-orphaned-content' : ''; } /** @@ -153,7 +111,7 @@ public function get_url( $task_id = '' ) { * @return bool */ public function should_add_task() { - return ! empty( $this->data_collector->collect() ); + return ! empty( $this->get_data_collector()->collect() ); } /** @@ -189,21 +147,33 @@ protected function is_specific_task_completed( $task_id ) { return 0 !== (int) $linked_count; } + /** + * Transform data collector data into task data format. + * + * @param array $data The data from data collector. + * @return array The transformed data with original data merged. + */ + protected function transform_collector_data( array $data ): array { + return \array_merge( + $data, + [ + 'target_post_id' => $data['post_id'], + 'target_post_title' => $data['post_title'], + ] + ); + } + /** * Get an array of tasks to inject. * * @return array */ public function get_tasks_to_inject() { - - if ( - true === $this->is_task_snoozed() || - ! $this->should_add_task() // No need to add the task. - ) { + if ( true === $this->is_task_snoozed() || ! $this->should_add_task() ) { return []; } - $data = $this->data_collector->collect(); + $data = $this->get_data_collector()->collect(); $task_id = $this->get_task_id( [ 'post_id' => $data['post_id'], @@ -215,45 +185,28 @@ public function get_tasks_to_inject() { return []; } - return [ - [ - 'task_id' => $task_id, - 'provider_id' => $this->get_provider_id(), - 'category' => $this->get_provider_category(), - 'post_id' => $data['post_id'], - 'post_title' => $data['post_title'], - ], - ]; + // Transform the data to match the task data structure. + $task_data = $this->modify_injection_task_data( + $this->get_task_details( + $this->transform_collector_data( $data ) + ) + ); + + return \progress_planner()->get_suggested_tasks_db()->get_post( $task_data['task_id'] ) + ? [] + : [ \progress_planner()->get_suggested_tasks_db()->add( $task_data ) ]; } /** - * Get the task details. + * Modify task data before injecting it. * - * @param string $task_id The task ID. + * @param array $task_data The task data. * * @return array */ - public function get_task_details( $task_id = '' ) { - - if ( ! $task_id ) { - return []; - } - - $task_details = [ - 'task_id' => $task_id, - 'provider_id' => $this->get_provider_id(), - 'title' => $this->get_title( $task_id ), - 'parent' => $this->get_parent(), - 'priority' => $this->get_priority(), - 'category' => $this->get_provider_category(), - 'points' => $this->get_points(), - 'dismissable' => $this->is_dismissable(), - 'url' => $this->get_url( $task_id ), - 'url_target' => $this->get_url_target(), - 'description' => $this->get_description( $task_id ), - ]; - - return $task_details; + protected function modify_injection_task_data( $task_data ) { + $task_data['target_post_id'] = $this->transform_collector_data( $this->get_data_collector()->collect() )['target_post_id']; + return $task_data; } /** @@ -264,15 +217,15 @@ public function get_task_details( $task_id = '' ) { * @return \WP_Post|null */ public function get_post_from_task_id( $task_id ) { - $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $task_id ); + $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $task_id ] ); if ( empty( $tasks ) ) { return null; } - $data = $tasks[0]; + $task = $tasks[0]; - return isset( $data['post_id'] ) && $data['post_id'] ? \get_post( $data['post_id'] ) : null; + return $task->target_post_id ? \get_post( $task->target_post_id ) : null; } /** @@ -281,18 +234,17 @@ public function get_post_from_task_id( $task_id ) { * @return array */ protected function get_completed_post_ids() { - if ( null !== $this->completed_post_ids ) { return $this->completed_post_ids; } $this->completed_post_ids = []; - $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'provider_id', $this->get_provider_id() ); + $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'provider_id' => $this->get_provider_id() ] ); if ( ! empty( $tasks ) ) { foreach ( $tasks as $task ) { - if ( isset( $task['status'] ) && 'completed' === $task['status'] ) { - $this->completed_post_ids[] = $task['post_id']; + if ( 'trash' === $task->post_status ) { + $this->completed_post_ids[] = $task->target_post_id; } } } @@ -307,6 +259,6 @@ protected function get_completed_post_ids() { * @return array */ public function exclude_completed_posts( $exclude_post_ids ) { - return array_merge( $exclude_post_ids, $this->get_completed_post_ids() ); + return \array_merge( $exclude_post_ids, $this->get_completed_post_ids() ); } } diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-media-pages.php b/classes/suggested-tasks/providers/integrations/yoast/class-media-pages.php index 0b645bae8..c97bf383f 100644 --- a/classes/suggested-tasks/providers/integrations/yoast/class-media-pages.php +++ b/classes/suggested-tasks/providers/integrations/yoast/class-media-pages.php @@ -20,10 +20,12 @@ class Media_Pages extends Yoast_Provider { protected const PROVIDER_ID = 'yoast-media-pages'; /** - * Constructor. + * Get the task URL. + * + * @return string */ - public function __construct() { - $this->url = \admin_url( 'admin.php?page=wpseo_page_settings#/media-pages' ); + protected function get_url() { + return \admin_url( 'admin.php?page=wpseo_page_settings#/media-pages' ); } /** @@ -31,7 +33,7 @@ public function __construct() { * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Yoast SEO: disable the media pages', 'progress-planner' ); } @@ -40,8 +42,8 @@ public function get_title() { * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %s: "Read more" link. */ \esc_html__( 'Yoast SEO can disable the media / attachment pages, which are the pages that show the media files. You really don\'t need them, except when you are displaying photos or art on your site through them. %s.', 'progress-planner' ), '' . \esc_html__( 'Read more', 'progress-planner' ) . '' @@ -74,6 +76,6 @@ public function get_focus_tasks() { */ public function should_add_task() { // If the media pages are already disabled, we don't need to add the task. - return YoastSEO()->helpers->options->get( 'disable-attachment' ) !== true; + return \YoastSEO()->helpers->options->get( 'disable-attachment' ) !== true; } } diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-organization-logo.php b/classes/suggested-tasks/providers/integrations/yoast/class-organization-logo.php index c9edb45be..90c7e22f3 100644 --- a/classes/suggested-tasks/providers/integrations/yoast/class-organization-logo.php +++ b/classes/suggested-tasks/providers/integrations/yoast/class-organization-logo.php @@ -30,8 +30,16 @@ class Organization_Logo extends Yoast_Provider { * Constructor. */ public function __construct() { - $this->yoast_seo = YoastSEO(); - $this->url = \admin_url( 'admin.php?page=wpseo_page_settings#/site-representation' ); + $this->yoast_seo = \YoastSEO(); + } + + /** + * Get the task URL. + * + * @return string + */ + protected function get_url() { + return \admin_url( 'admin.php?page=wpseo_page_settings#/site-representation' ); } /** @@ -39,7 +47,7 @@ public function __construct() { * * @return string */ - public function get_title() { + protected function get_title() { return $this->yoast_seo->helpers->options->get( 'company_or_person', 'company' ) !== 'person' ? \esc_html__( 'Yoast SEO: set your organization logo', 'progress-planner' ) : \esc_html__( 'Yoast SEO: set your person logo', 'progress-planner' ); @@ -50,13 +58,13 @@ public function get_title() { * * @return string */ - public function get_description() { + protected function get_description() { return $this->yoast_seo->helpers->options->get( 'company_or_person', 'company' ) !== 'person' - ? sprintf( + ? \sprintf( /* translators: %s: "Read more" link. */ \esc_html__( 'To make Yoast SEO output the correct Schema, you need to set your organization logo in the Yoast SEO settings. %s.', 'progress-planner' ), '' . \esc_html__( 'Read more', 'progress-planner' ) . '' - ) : sprintf( + ) : \sprintf( /* translators: %s: "Read more" link. */ \esc_html__( 'To make Yoast SEO output the correct Schema, you need to set your person logo in the Yoast SEO settings. %s.', 'progress-planner' ), '' . \esc_html__( 'Read more', 'progress-planner' ) . '' @@ -97,14 +105,17 @@ public function get_focus_tasks() { * @return bool */ public function should_add_task() { - // If the site is for a person, and the person logo is already set, we don't need to add the task. - if ( $this->yoast_seo->helpers->options->get( 'company_or_person', 'company' ) === 'company' && $this->yoast_seo->helpers->options->get( 'company_logo' ) ) { + if ( $this->yoast_seo->helpers->options->get( 'company_or_person', 'company' ) === 'company' + && $this->yoast_seo->helpers->options->get( 'company_logo' ) + ) { return false; } // If the site is for a person, and the organization logo is already set, we don't need to add the task. - if ( $this->yoast_seo->helpers->options->get( 'company_or_person', 'company' ) === 'person' && $this->yoast_seo->helpers->options->get( 'person_logo' ) ) { + if ( $this->yoast_seo->helpers->options->get( 'company_or_person', 'company' ) === 'person' + && $this->yoast_seo->helpers->options->get( 'person_logo' ) + ) { return false; } diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-orphaned-content-workout.php b/classes/suggested-tasks/providers/integrations/yoast/class-orphaned-content-workout.php index 6479a77f9..c12cb6be2 100644 --- a/classes/suggested-tasks/providers/integrations/yoast/class-orphaned-content-workout.php +++ b/classes/suggested-tasks/providers/integrations/yoast/class-orphaned-content-workout.php @@ -33,9 +33,9 @@ class Orphaned_Content_Workout extends Yoast_Provider { /** * The task priority. * - * @var string + * @var int */ - protected $priority = 'low'; + protected $priority = 90; /** * Whether the task is dismissable. @@ -80,26 +80,29 @@ public function init() { * @return void */ public function maybe_update_workout_status( $old_value, $value, $option ) { - if ( 'wpseo_premium' !== $option || ! isset( $value['workouts']['orphaned'] ) || ! isset( $old_value['workouts']['orphaned'] ) ) { + if ( 'wpseo_premium' !== $option + || ! isset( $value['workouts']['orphaned'] ) + || ! isset( $old_value['workouts']['orphaned'] ) + ) { return; } - // Check if there is pending task. - $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $this->get_task_id() ); + // Check if there is a published task. + $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $this->get_task_id() ] ); - // If there is no pending task, return. - if ( empty( $tasks ) || 'pending' !== $tasks[0]['status'] ) { + // If there is no published task, return. + if ( empty( $tasks ) || 'publish' !== $tasks[0]->post_status ) { return; } // For this type of task only the provider ID is needed, but just in case. - if ( $this->is_task_dismissed( $tasks[0] ) ) { + if ( $this->is_task_dismissed( $tasks[0]->get_data() ) ) { return; } // There should be 3 steps in the workout. - $workout_was_completed = 3 === count( $old_value['workouts']['orphaned']['finishedSteps'] ); - $workout_completed = 3 === count( $value['workouts']['orphaned']['finishedSteps'] ); + $workout_was_completed = 3 === \count( $old_value['workouts']['orphaned']['finishedSteps'] ); + $workout_completed = 3 === \count( $value['workouts']['orphaned']['finishedSteps'] ); // Dismiss the task if workout wasn't completed before and now is. if ( ! $workout_was_completed && $workout_completed ) { @@ -110,23 +113,19 @@ public function maybe_update_workout_status( $old_value, $value, $option ) { /** * Get the task title. * - * @param string $task_id The task ID. - * * @return string */ - public function get_title( $task_id = '' ) { + protected function get_title() { return \esc_html__( 'Yoast SEO: do Yoast SEO\'s Orphaned Content Workout', 'progress-planner' ); } /** * Get the task description. * - * @param string $task_id The task ID. - * * @return string */ - public function get_description( $task_id = '' ) { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %s: "Read more" link. */ \esc_html__( 'Improve your internal linking structure with Yoast SEO\'s Orphaned Content Workout. %s.', 'progress-planner' ), '' . \esc_html__( 'Lean more', 'progress-planner' ) . '' @@ -136,12 +135,10 @@ public function get_description( $task_id = '' ) { /** * Get the task URL. * - * @param string $task_id The task ID. - * * @return string */ - public function get_url( $task_id = '' ) { - return $this->capability_required() ? \esc_url( admin_url( 'admin.php?page=wpseo_workouts#orphaned' ) ) : ''; + protected function get_url() { + return \esc_url( \admin_url( 'admin.php?page=wpseo_workouts#orphaned' ) ); } /** @@ -150,46 +147,14 @@ public function get_url( $task_id = '' ) { * @return bool */ public function should_add_task() { - if ( ! defined( 'WPSEO_PREMIUM_VERSION' ) ) { - return false; - } - - $task_data = [ - 'provider_id' => $this->get_provider_id(), - ]; - - // Skip if the task has been dismissed. - if ( $this->is_task_dismissed( $task_data ) ) { + if ( ! \defined( 'WPSEO_PREMIUM_VERSION' ) ) { return false; } - return true; - } - - /** - * Get the task details. - * - * @param string $task_id The task ID. - * - * @return array - */ - public function get_task_details( $task_id = '' ) { - if ( ! $task_id ) { - return []; - } - - return [ - 'task_id' => $task_id, - 'provider_id' => $this->get_provider_id(), - 'title' => $this->get_title( $task_id ), - 'parent' => $this->get_parent(), - 'priority' => $this->get_priority(), - 'category' => $this->get_provider_category(), - 'points' => $this->get_points(), - 'dismissable' => $this->is_dismissable, - 'url' => $this->get_url( $task_id ), - 'url_target' => $this->get_url_target(), - 'description' => $this->get_description( $task_id ), - ]; + return ! $this->is_task_dismissed( + [ + 'provider_id' => $this->get_provider_id(), + ] + ); } } diff --git a/classes/suggested-tasks/providers/interactive/class-email-sending.php b/classes/suggested-tasks/providers/interactive/class-email-sending.php index baedb4730..0bb195b0c 100644 --- a/classes/suggested-tasks/providers/interactive/class-email-sending.php +++ b/classes/suggested-tasks/providers/interactive/class-email-sending.php @@ -45,9 +45,9 @@ class Email_Sending extends Interactive { /** * The task priority. * - * @var string + * @var int */ - protected $priority = 'high'; + protected $priority = 1; /** * The popover ID. @@ -106,21 +106,24 @@ class Email_Sending extends Interactive { public function init() { // Enqueue the scripts. - add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); + \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); // Add the AJAX action. - add_action( 'wp_ajax_prpl_test_email_sending', [ $this, 'ajax_test_email_sending' ] ); + \add_action( 'wp_ajax_prpl_test_email_sending', [ $this, 'ajax_test_email_sending' ] ); // Set the email error message. - add_action( 'wp_mail_failed', [ $this, 'set_email_error' ] ); + \add_action( 'wp_mail_failed', [ $this, 'set_email_error' ] ); // By now all plugins should be loaded and hopefully add actions registered, so we can check if phpmailer is filtered. \add_action( 'init', [ $this, 'check_if_wp_mail_is_filtered' ], PHP_INT_MAX ); \add_action( 'init', [ $this, 'check_if_wp_mail_has_override' ], PHP_INT_MAX ); $this->email_subject = \esc_html__( 'Your Progress Planner test message!', 'progress-planner' ); - // translators: %1$s

    tags, %2$s the admin URL. - $this->email_content = sprintf( \esc_html__( 'You just used Progress Planner to verify if sending email works on your website. %1$s The good news; it does! Click here to %2$s.', 'progress-planner' ), '

    ', '' . \esc_html__( 'mark Ravi\'s Recommendation as completed', 'progress-planner' ) . '', '' . \esc_html__( 'here', 'progress-planner' ) . '' ); + $this->email_content = \sprintf( + // translators: %1$s the admin URL. + \__( 'You just used Progress Planner to verify if sending email works on your website.

    The good news; it does! Click here to mark Ravi\'s Recommendation as completed.', 'progress-planner' ), + \admin_url( 'admin.php?page=progress-planner&prpl_complete_task=' . $this->get_task_id() ) + ); } /** @@ -128,16 +131,17 @@ public function init() { * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Test if your website can send emails correctly', 'progress-planner' ); } /** * Get the description. * + * @param array $task_data Optional data to include in the task. * @return string */ - public function get_description() { + protected function get_description( $task_data = [] ) { return \esc_html__( 'Your website tries to send you important email. Test if sending email from your site works well.', 'progress-planner' ); } @@ -153,11 +157,9 @@ public function enqueue_scripts() { return; } - $handle = 'progress-planner/web-components/prpl-task-' . $this->get_provider_id(); - // Enqueue the web component. \progress_planner()->get_admin__enqueue()->enqueue_script( - $handle, + 'progress-planner/web-components/prpl-task-' . $this->get_provider_id(), [ 'name' => 'prplEmailSending', 'data' => [ @@ -197,9 +199,8 @@ public function check_if_wp_mail_is_filtered() { public function check_if_wp_mail_has_override() { // Just in case, since it will trigger PHP fatal error if the function doesn't exist. - if ( function_exists( 'wp_mail' ) ) { - $ref = new \ReflectionFunction( 'wp_mail' ); - $file_path = $ref->getFileName(); + if ( \function_exists( 'wp_mail' ) ) { + $file_path = ( new \ReflectionFunction( 'wp_mail' ) )->getFileName(); $this->is_wp_mail_overridden = $file_path && $file_path !== ABSPATH . 'wp-includes/pluggable.php'; } @@ -227,17 +228,17 @@ public function ajax_test_email_sending() { $email_address = isset( $_GET['email_address'] ) ? \sanitize_email( \wp_unslash( $_GET['email_address'] ) ) : ''; if ( ! $email_address ) { - wp_send_json_error( \esc_html__( 'Invalid email address.', 'progress-planner' ) ); + \wp_send_json_error( \esc_html__( 'Invalid email address.', 'progress-planner' ) ); } $headers = [ 'Content-Type: text/html; charset=UTF-8' ]; - $result = wp_mail( $email_address, $this->email_subject, $this->email_content, $headers ); + $result = \wp_mail( $email_address, $this->email_subject, $this->email_content, $headers ); if ( $result ) { - wp_send_json_success( \esc_html__( 'Email sent successfully.', 'progress-planner' ) ); + \wp_send_json_success( \esc_html__( 'Email sent successfully.', 'progress-planner' ) ); } - wp_send_json_error( $this->email_error ); + \wp_send_json_error( $this->email_error ); } /** @@ -257,231 +258,29 @@ public function set_email_error( $e ) { * @return void */ public function the_popover_content() { - ?> - - -
    -
    -

    -

    -

    -
    -
    -

    -
    - - the_asset( 'images/icon_exclamation_triangle_solid.svg' ); ?> - - - - -
    -
    - - -
    - -
    -
    -
    -
    - - - - - - - - - - - - - - - -
    - the_view( + 'popovers/email-sending.php', + [ + 'prpl_popover_id' => $this->popover_id, + 'prpl_provider_id' => $this->get_provider_id(), + 'prpl_email_subject' => $this->email_subject, + 'prpl_email_error' => $this->email_error, + 'prpl_troubleshooting_guide_url' => $this->troubleshooting_guide_url, + 'prpl_is_there_sending_email_override' => $this->is_there_sending_email_override(), + ] + ); } /** - * Get the task details. + * Modify task data before injecting it. * - * @param string $task_id The task ID. + * @param array $task_data The task data. * * @return array */ - public function get_task_details( $task_id = '' ) { - - if ( ! $task_id ) { - $task_id = $this->get_provider_id(); - } + protected function modify_injection_task_data( $task_data ) { + $task_data['popover_id'] = 'prpl-popover-' . $this->popover_id; - return [ - 'task_id' => $task_id, - 'title' => $this->get_title(), - 'parent' => $this->get_parent(), - 'priority' => $this->get_priority(), - 'category' => $this->get_provider_category(), - 'provider_id' => $this->get_provider_id(), - 'points' => $this->get_points(), - 'dismissable' => $this->is_dismissable(), - 'popover_id' => 'prpl-popover-' . $this->popover_id, - 'description' => $this->get_description(), - ]; + return $task_data; } } diff --git a/classes/suggested-tasks/providers/traits/class-dismissable-task.php b/classes/suggested-tasks/providers/traits/class-dismissable-task.php index 0203b0421..8c0886ece 100644 --- a/classes/suggested-tasks/providers/traits/class-dismissable-task.php +++ b/classes/suggested-tasks/providers/traits/class-dismissable-task.php @@ -43,29 +43,26 @@ protected function init_dismissable_task() { /** * Handle task dismissal by storing the task data and dismissal date. * - * @param string $task_id The task ID. + * @param string $post_id The post ID. * * @return void */ - public function handle_task_dismissal( $task_id ) { - + public function handle_task_dismissal( $post_id ) { // If no task ID is provided, return. - if ( ! $task_id ) { + if ( ! $post_id ) { return; } // Get the task data. - $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $task_id ); + $task = \progress_planner()->get_suggested_tasks_db()->get_post( $post_id ); // If no task data is found, return. - if ( ! $tasks ) { + if ( ! $task ) { return; } - $task_data = $tasks[0]; - // If the task provider ID does not match, return. - if ( ! isset( $task_data['provider_id'] ) || $this->get_provider_id() !== $task_data['provider_id'] ) { + if ( ! isset( $task->provider->slug ) || $this->get_provider_id() !== $task->provider->slug ) { return; } @@ -81,7 +78,7 @@ public function handle_task_dismissal( $task_id ) { } // Get the task identifier. - $task_identifier = $this->get_task_identifier( $task_data ); + $task_identifier = $this->get_task_identifier( $task->get_data() ); // If no task identifier is found, return. if ( ! $task_identifier ) { @@ -90,8 +87,8 @@ public function handle_task_dismissal( $task_id ) { // Store the task dismissal data. $dismissal_data = [ - 'date' => gmdate( 'YW' ), - 'timestamp' => time(), + 'date' => \gmdate( 'YW' ), + 'timestamp' => \time(), ]; /** @@ -101,7 +98,7 @@ public function handle_task_dismissal( $task_id ) { * @param array $task_data The task data. * @param string $provider_id The provider ID. */ - $dismissal_data = \apply_filters( 'progress_planner_task_dismissal_data', $dismissal_data, $task_data, $provider_id ); + $dismissal_data = \apply_filters( 'progress_planner_task_dismissal_data', $dismissal_data, $task->get_data(), $provider_id ); $dismissed_tasks[ $provider_id ][ $task_identifier ] = $dismissal_data; @@ -120,12 +117,12 @@ public function handle_task_dismissal( $task_id ) { protected function get_task_identifier( $task_data ) { $task_identifier = $this->get_provider_id(); - if ( isset( $task_data['post_id'] ) ) { - $task_identifier .= '-' . $task_data['post_id']; + if ( isset( $task_data['target_post_id'] ) ) { + $task_identifier .= '-' . $task_data['target_post_id']; } - if ( isset( $task_data['term_id'] ) ) { - $task_identifier .= '-' . $task_data['term_id']; + if ( isset( $task_data['target_term_id'] ) ) { + $task_identifier .= '-' . $task_data['target_term_id']; } return $task_identifier; @@ -166,12 +163,12 @@ protected function is_task_dismissed( $task_data ) { $dismissal_data = $dismissed_tasks[ $provider_key ][ $task_identifier ]; // If the task was dismissed in the current week, don't show it again. - if ( $dismissal_data['date'] === gmdate( 'YW' ) ) { + if ( $dismissal_data['date'] === \gmdate( 'YW' ) ) { return true; } // If the task was dismissed more than the expiration period ago, we can show it again. - if ( ( time() - $dismissal_data['timestamp'] ) > $this->get_expiration_period( $dismissal_data ) ) { + if ( ( \time() - $dismissal_data['timestamp'] ) > $this->get_expiration_period( $dismissal_data ) ) { unset( $dismissed_tasks[ $provider_key ][ $task_identifier ] ); \progress_planner()->get_settings()->set( $this->dismissed_tasks_option, $dismissed_tasks ); return false; @@ -187,9 +184,7 @@ protected function is_task_dismissed( $task_data ) { */ public function get_dismissed_tasks() { $dismissed_tasks = \progress_planner()->get_settings()->get( $this->dismissed_tasks_option, [] ); - $provider_key = $this->get_provider_id(); - - return $dismissed_tasks[ $provider_key ] ?? []; + return $dismissed_tasks[ $this->get_provider_id() ] ?? []; } /** @@ -213,7 +208,7 @@ public function cleanup_old_dismissals() { $has_changes = false; foreach ( $dismissed_tasks[ $provider_key ] as $identifier => $data ) { - if ( ( time() - $data['timestamp'] ) > $this->get_expiration_period( $data ) ) { + if ( ( \time() - $data['timestamp'] ) > $this->get_expiration_period( $data ) ) { unset( $dismissed_tasks[ $provider_key ][ $identifier ] ); $has_changes = true; } @@ -237,8 +232,8 @@ public function cleanup_old_dismissals() { * @return array */ public function add_post_id_to_dismissal_data( $dismissal_data, $task_data, $provider_id ) { - if ( $this->get_provider_id() === $provider_id && isset( $task_data['post_id'] ) ) { - $dismissal_data['post_id'] = $task_data['post_id']; + if ( $this->get_provider_id() === $provider_id && isset( $task_data['target_post_id'] ) ) { + $dismissal_data['post_id'] = $task_data['target_post_id']; } return $dismissal_data; } @@ -253,8 +248,8 @@ public function add_post_id_to_dismissal_data( $dismissal_data, $task_data, $pro * @return array */ public function add_term_id_to_dismissal_data( $dismissal_data, $task_data, $provider_id ) { - if ( $this->get_provider_id() === $provider_id && isset( $task_data['term_id'] ) ) { - $dismissal_data['term_id'] = $task_data['term_id']; + if ( $this->get_provider_id() === $provider_id && isset( $task_data['target_term_id'] ) ) { + $dismissal_data['term_id'] = $task_data['target_term_id']; } return $dismissal_data; } diff --git a/classes/ui/class-chart.php b/classes/ui/class-chart.php index 7e70cda7a..41c0ed0a7 100644 --- a/classes/ui/class-chart.php +++ b/classes/ui/class-chart.php @@ -59,7 +59,7 @@ public function get_chart_data( $args = [] ) { }, // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter 'count_callback' => function ( $activities, $date = null ) { - return count( $activities ); + return \count( $activities ); }, 'max' => null, 'type' => 'line', @@ -137,7 +137,7 @@ public function get_period_data( $period, $args, $previous_period_activities ) { 'label' => $period['start_date']->format( $args['dates_params']['format'] ), 'score' => null === $args['max'] ? $period_score - : min( $period_score, $args['max'] ), + : \min( $period_score, $args['max'] ), 'color' => $args['color']( $period_score, $period['start_date'] ), 'previous_period_activities' => $previous_period_activities, ]; @@ -153,6 +153,6 @@ public function get_period_data( $period, $args, $previous_period_activities ) { */ public function render_chart( $type, $data ) { $type = $type ? $type : 'line'; - echo ''; + echo ''; } } diff --git a/classes/update/class-update-111.php b/classes/update/class-update-111.php index aa60bc2f8..253cfd280 100644 --- a/classes/update/class-update-111.php +++ b/classes/update/class-update-111.php @@ -7,7 +7,7 @@ namespace Progress_Planner\Update; -use Progress_Planner\Suggested_Tasks\Task_Factory; +use Progress_Planner\Utils\Plugin_Migration_Helpers; /** * Update class for version 1.1.1. @@ -82,7 +82,7 @@ private function migrate_local_tasks() { $local_tasks_option = \get_option( 'progress_planner_local_tasks', [] ); if ( ! empty( $local_tasks_option ) ) { foreach ( $local_tasks_option as $task_id ) { - $task = Task_Factory::create_task_from( 'id', $task_id )->get_data(); + $task = Plugin_Migration_Helpers::parse_task_data_from_task_id( $task_id )->get_data(); $task['status'] = 'pending'; if ( ! isset( $task['task_id'] ) ) { @@ -107,8 +107,8 @@ private function migrate_suggested_tasks() { } foreach ( $suggested_tasks_option as $status => $tasks ) { foreach ( $tasks as $_task ) { - $task_id = is_string( $_task ) ? $_task : $_task['id']; - $task = Task_Factory::create_task_from( 'id', $task_id )->get_data(); + $task_id = \is_string( $_task ) ? $_task : $_task['id']; + $task = Plugin_Migration_Helpers::parse_task_data_from_task_id( $task_id )->get_data(); $task['status'] = $status; if ( 'snoozed' === $status && isset( $_task['time'] ) ) { $task['time'] = $_task['time']; @@ -198,7 +198,7 @@ private function migrate_todo_items() { foreach ( $todo_items as $todo_item ) { $this->add_local_task( [ - 'task_id' => 'user-task-' . md5( $todo_item['content'] ), + 'task_id' => 'user-task-' . \md5( $todo_item['content'] ), 'status' => $todo_item['done'] ? 'completed' : 'pending', 'provider_id' => 'user', 'category' => 'user', @@ -220,11 +220,11 @@ private function migrate_todo_items() { * @return string */ private function convert_task_id( $task_id ) { - if ( ! str_contains( $task_id, '|' ) ) { + if ( ! \str_contains( $task_id, '|' ) ) { return $task_id; } - $task_id = str_replace( 'type', 'provider_id', $task_id ); - $task_id = str_replace( 'provider_id/update-post', 'provider_id/review-post', $task_id ); // Update the provider_id for update-post tasks. + $task_id = \str_replace( 'type', 'provider_id', $task_id ); + $task_id = \str_replace( 'provider_id/update-post', 'provider_id/review-post', $task_id ); // Update the provider_id for update-post tasks. $parts = \explode( '|', $task_id ); \ksort( $parts ); return \implode( '|', $parts ); @@ -237,21 +237,20 @@ private function convert_task_id( $task_id ) { * @return void */ private function migrate_create_post_tasks() { - // Migrate the 'create-post' completed tasks. if ( ! empty( $this->local_tasks ) ) { foreach ( $this->local_tasks as $key => $task ) { if ( ! isset( $task['task_id'] ) ) { continue; } - if ( false !== strpos( $task['task_id'], 'provider_id/create-post' ) ) { - - // task_id needs to be unique, before we had 2 'create-post' tasks for the same week (short and long). - // So for tasks which are completed or pending_celebration we will make the task_id like: create-post-short-202501, - // and for pending tasks task_id will be (how it will be in the future, since we only have 1 type of create-post task per week): create-post-202501 . - + if ( false !== \strpos( $task['task_id'], 'provider_id/create-post' ) ) { + /* + * `task_id` needs to be unique, before we had 2 'create-post' tasks for the same week (short and long). + * For tasks which are completed or pending we will make the task_id like: create-post-short-202501 + * and for pending tasks, task_id will be (how it will be in the future, since we only have 1 type of create-post task per week): create-post-202501 . + */ // Only add legacy part of the task_id if the task is not pending. - if ( 'completed' === $task['status'] || 'pending_celebration' === $task['status'] ) { + if ( 'completed' === $task['status'] || 'pending' === $task['status'] ) { $this->local_tasks[ $key ]['task_id'] = $task['provider_id'] . '-' . ( $task['long'] ? 'long' : 'short' ) . '-' . $task['date']; } else { $this->local_tasks[ $key ]['task_id'] = $task['provider_id'] . '-' . $task['date']; @@ -282,7 +281,7 @@ private function migrate_create_post_activities() { if ( ! empty( $activities ) ) { foreach ( $activities as $activity ) { - if ( false !== strpos( $activity->data_id, 'provider_id/create-post' ) ) { + if ( false !== \strpos( $activity->data_id, 'provider_id/create-post' ) ) { $data = $this->get_data_from_task_id( $activity->data_id ); // NOTE: task_id needs to be unique, before we had 2 'create-post' tasks in the same week (short and long). @@ -303,15 +302,13 @@ private function migrate_create_post_activities() { * @return void */ private function migrate_review_post_tasks() { - // Migrate the 'create-post' completed tasks. if ( ! empty( $this->local_tasks ) ) { foreach ( $this->local_tasks as $key => $task ) { if ( ! isset( $task['task_id'] ) ) { continue; } - if ( false !== strpos( $task['task_id'], 'provider_id/review-post' ) ) { - + if ( false !== \strpos( $task['task_id'], 'provider_id/review-post' ) ) { $data = $this->get_data_from_task_id( $task['task_id'] ); // Get the date from the activity. @@ -345,7 +342,7 @@ private function migrate_review_post_activities() { if ( ! isset( $activity->data_id ) || ! isset( $activity->date ) ) { continue; } - if ( false !== strpos( $activity->data_id, 'provider_id/review-post' ) ) { + if ( false !== \strpos( $activity->data_id, 'provider_id/review-post' ) ) { $data = $this->get_data_from_task_id( $activity->data_id ); $new_data_id = $data['provider_id'] . '-' . $data['post_id'] . '-' . $activity->date->format( 'YW' ); @@ -374,11 +371,9 @@ private function get_date_from_activity( $task_id ) { ] ); - if ( ! empty( $activity ) ) { - return $activity[0]->date->format( 'YW' ); - } - - return \gmdate( 'YW' ); + return ( ! empty( $activity ) ) + ? $activity[0]->date->format( 'YW' ) + : \gmdate( 'YW' ); } /** diff --git a/classes/update/class-update-130.php b/classes/update/class-update-130.php index 566bab5e8..ca8e641bf 100644 --- a/classes/update/class-update-130.php +++ b/classes/update/class-update-130.php @@ -7,7 +7,7 @@ namespace Progress_Planner\Update; -use Progress_Planner\Suggested_Tasks\Task_Factory; +use Progress_Planner\Utils\Plugin_Migration_Helpers; use Progress_Planner\Suggested_Tasks\Task; /** @@ -83,7 +83,6 @@ private function restore_completed_tasks() { 'type' => 'completed', ], ) as $activity ) { - $continue_main_loop = false; // Check if the task with the same task_id exists, it means that task was recreated (and has pending status now). @@ -135,20 +134,15 @@ private function restore_completed_tasks() { * @return array The data. */ private function get_data_from_task_id( $task_id ) { + $task_object = Plugin_Migration_Helpers::parse_task_data_from_task_id( $task_id ); - $task_object = Task_Factory::create_task_from( 'id', $task_id ); - - if ( 0 === strpos( $task_object->get_task_id(), 'create-post-' ) || 0 === strpos( $task_object->get_task_id(), 'create-post-short-' ) ) { + if ( 0 === \strpos( $task_object->get_task_id(), 'create-post-' ) || 0 === \strpos( $task_object->get_task_id(), 'create-post-short-' ) ) { $task_object = $this->handle_legacy_post_tasks( $task_object ); - } - - // Review post task is not recognized by the Task_Factory (because it changed from piped format: post_id/2949|type/update-post -> review-post-2949-202415). - if ( 0 === strpos( $task_object->get_task_id(), 'review-post-' ) ) { + } elseif ( 0 === \strpos( $task_object->get_task_id(), 'review-post-' ) ) { + // Review post task is not recognized by the Task_Factory (because it changed from piped format: post_id/2949|type/update-post -> review-post-2949-202415). $task_object = $this->handle_legacy_review_post_tasks( $task_object ); - } - - // Yoast SEO tasks and Comment Hacks tasks are not recognized by the Task_Factory, since they are added recently. - if ( 0 === strpos( $task_object->get_task_id(), 'yoast-' ) || 0 === strpos( $task_object->get_task_id(), 'ch-comment' ) ) { + } elseif ( 0 === \strpos( $task_object->get_task_id(), 'yoast-' ) || 0 === \strpos( $task_object->get_task_id(), 'ch-comment' ) ) { + // Yoast SEO tasks and Comment Hacks tasks are not recognized by the Task_Factory, since they are added recently. $task_object = $this->handle_legacy_yoast_and_comment_hacks_tasks( $task_object ); } @@ -164,14 +158,14 @@ private function get_data_from_task_id( $task_id ) { */ private function handle_legacy_post_tasks( $task_object ) { // Handle legacy long post tasks, here we just need to set 'long' flag to true. - if ( 0 === strpos( $task_object->get_task_id(), 'create-post-long-' ) ) { + if ( 0 === \strpos( $task_object->get_task_id(), 'create-post-long-' ) ) { $data = $task_object->get_data(); $data['long'] = true; $task_object->set_data( $data ); } // Handle legacy short post tasks, here we just need to set 'long' flag to false. - if ( 0 === strpos( $task_object->get_task_id(), 'create-post-short-' ) ) { + if ( 0 === \strpos( $task_object->get_task_id(), 'create-post-short-' ) ) { $data = $task_object->get_data(); $data['long'] = false; $task_object->set_data( $data ); @@ -192,7 +186,7 @@ private function handle_legacy_review_post_tasks( $task_object ) { $task_provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( 'review-post' ); // Get the post ID and date from the task ID. - $parts = explode( '-', $task_object->get_task_id() ); + $parts = \explode( '-', $task_object->get_task_id() ); $data = [ 'task_id' => $task_object->get_task_id(), @@ -215,14 +209,13 @@ private function handle_legacy_review_post_tasks( $task_object ) { * @return Task The task object. */ private function handle_legacy_yoast_and_comment_hacks_tasks( $task_object ) { - - $data = [ - 'task_id' => $task_object->get_task_id(), - 'provider_id' => $task_object->get_task_id(), - 'category' => 'configuration', - ]; - - $task_object->set_data( $data ); + $task_object->set_data( + [ + 'task_id' => $task_object->get_task_id(), + 'provider_id' => $task_object->get_task_id(), + 'category' => 'configuration', + ] + ); return $task_object; } diff --git a/classes/update/class-update-140.php b/classes/update/class-update-140.php index 3a495ba85..97ffd1068 100644 --- a/classes/update/class-update-140.php +++ b/classes/update/class-update-140.php @@ -41,14 +41,14 @@ private function rename_tasks_option() { // This is to ensure that we don't lose any tasks, and at the same time we don't have duplicate tasks. $tasks = []; foreach ( $new_tasks as $new_task ) { - $tasks[ isset( $new_task['task_id'] ) ? $new_task['task_id'] : md5( maybe_serialize( $new_task ) ) ] = $new_task; + $tasks[ isset( $new_task['task_id'] ) ? $new_task['task_id'] : \md5( \maybe_serialize( $new_task ) ) ] = $new_task; } foreach ( $old_tasks as $old_task ) { - $tasks[ isset( $old_task['task_id'] ) ? $old_task['task_id'] : md5( maybe_serialize( $old_task ) ) ] = $old_task; + $tasks[ isset( $old_task['task_id'] ) ? $old_task['task_id'] : \md5( \maybe_serialize( $old_task ) ) ] = $old_task; } // Set the tasks option. - \progress_planner()->get_settings()->set( 'tasks', array_values( $tasks ) ); + \progress_planner()->get_settings()->set( 'tasks', \array_values( $tasks ) ); // Delete the old tasks option. \progress_planner()->get_settings()->delete( 'local_tasks' ); diff --git a/classes/update/class-update-150.php b/classes/update/class-update-150.php new file mode 100644 index 000000000..acefa9889 --- /dev/null +++ b/classes/update/class-update-150.php @@ -0,0 +1,132 @@ +migrate_tasks(); + } + + /** + * Migrate the tasks. + * + * @return void + */ + private function migrate_tasks() { + // Get all tasks. + $tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); + + // Migrate the tasks. + foreach ( $tasks as $task ) { + $this->migrate_task( $task ); + } + + // Delete the tasks option. + \progress_planner()->get_settings()->delete( 'tasks' ); + } + + /** + * Migrate a task. + * + * @param array $task The task to migrate. + * + * @return void + */ + private function migrate_task( $task ) { + // Skip tasks which don't have a provider ID or status. + if ( ! isset( $task['status'] ) || ! isset( $task['provider_id'] ) ) { + return; + } + + // Skip suggested tasks which are not completed or snoozed (but all user tasks are migrated). + if ( 'snoozed' !== $task['status'] && 'completed' !== $task['status'] && 'user' !== $task['provider_id'] ) { + return; + } + + $task_provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( $task['provider_id'] ); + + // Skip tasks which don't have a task provider. + if ( ! $task_provider ) { + return; + } + + // Now when we have target data - get the task details from the task provider, title, description, url, points, etc. + if ( 'user' === $task['provider_id'] ) { + // User tasks have different data structure, so we can copy directly. + $task_details = [ + 'post_title' => $task['title'], + 'description' => '', + 'points' => $task['points'], + 'provider_id' => 'user', + 'category' => 'user', + 'task_id' => $task['task_id'], + 'post_status' => 'pending' === $task['status'] ? 'publish' : $task['status'], + 'dismissable' => true, + 'snoozable' => false, + ]; + } else { + // Migrate the legacy task data, if the key exists. + // To avoid conflicts and confusion we have added 'target_' prefix to the keys. + $keys_to_migrate = [ + 'post_id', + 'post_title', + 'post_type', + 'term_id', + 'taxonomy', + 'term_name', + ]; + + // Data which is used to build task title, description, url. + $target_data = []; + + foreach ( $keys_to_migrate as $key ) { + if ( isset( $task[ $key ] ) ) { + $target_data[ 'target_' . $key ] = $task[ $key ]; + } + } + + $task_details = $task_provider->get_task_details( $target_data ); + + // Usually repeating tasks have a date. + if ( isset( $task['date'] ) ) { + $task_details['date'] = $task['date']; + } else { + // If not remove it, since get_task_details() method adds a date with \gmdate( 'YW' ) (which will be the date of the migration). + unset( $task_details['date'] ); + } + + // Snoozed tasks have a time. + if ( isset( $task['time'] ) ) { + // Checking if task was snoozed forever (PHP_INT_MAX). + $task_details['time'] = \is_float( $task['time'] ) ? \strtotime( '+10 years' ) : $task['time']; + } + + // Add target data to the task details, we need them in the details as well. + $task_details = \array_merge( $task_details, $target_data ); + + // Add status to the task details. + $task_details['post_status'] = $task['status']; + } + + // Add the task to the database. + \progress_planner()->get_suggested_tasks_db()->add( $task_details ); + } +} diff --git a/classes/utils/class-date.php b/classes/utils/class-date.php index a42884042..42f6cba79 100644 --- a/classes/utils/class-date.php +++ b/classes/utils/class-date.php @@ -24,10 +24,10 @@ class Date { * ]. */ public function get_range( $start_date, $end_date ) { - $dates = iterator_to_array( new \DatePeriod( $start_date, new \DateInterval( 'P1D' ), $end_date ), false ); + $dates = \iterator_to_array( new \DatePeriod( $start_date, new \DateInterval( 'P1D' ), $end_date ), false ); return [ 'start_date' => $dates[0], - 'end_date' => end( $dates ), + 'end_date' => \end( $dates ), ]; } @@ -60,7 +60,7 @@ public function get_periods( $start_date, $end_date, $frequency ) { break; } - $period = iterator_to_array( new \DatePeriod( $start_date, $interval, $end_date ), false ); + $period = \iterator_to_array( new \DatePeriod( $start_date, $interval, $end_date ), false ); $date_ranges = []; foreach ( $period as $key => $date ) { @@ -71,8 +71,8 @@ public function get_periods( $start_date, $end_date, $frequency ) { if ( empty( $date_ranges ) ) { return []; } - if ( $end_date->format( 'z' ) !== end( $date_ranges )['end_date']->format( 'z' ) ) { - $final_end = clone end( $date_ranges )['end_date']; + if ( $end_date->format( 'z' ) !== \end( $date_ranges )['end_date']->format( 'z' ) ) { + $final_end = clone \end( $date_ranges )['end_date']; $date_ranges[] = $this->get_range( $final_end->modify( '+1 day' ), $end_date ); } diff --git a/classes/utils/class-debug-tools.php b/classes/utils/class-debug-tools.php index 6b786cb6f..a6a1b0f79 100644 --- a/classes/utils/class-debug-tools.php +++ b/classes/utils/class-debug-tools.php @@ -37,7 +37,7 @@ public function __construct() { return; } - $this->current_url = wp_nonce_url( esc_url_raw( \wp_unslash( $_SERVER['REQUEST_URI'] ) ), 'prpl_debug_tools' ); + $this->current_url = \wp_nonce_url( \esc_url_raw( \wp_unslash( $_SERVER['REQUEST_URI'] ) ), 'prpl_debug_tools' ); \add_action( 'admin_bar_menu', [ $this, 'add_toolbar_items' ], 100 ); \add_action( 'init', [ $this, 'check_clear_cache' ] ); @@ -59,7 +59,7 @@ public function __construct() { * @return void */ public function add_toolbar_items( $admin_bar ) { - if ( ! current_user_can( 'manage_options' ) ) { + if ( ! \current_user_can( 'manage_options' ) ) { return; } @@ -79,7 +79,7 @@ public function add_toolbar_items( $admin_bar ) { 'id' => 'prpl-show-all-suggested-tasks', 'parent' => 'prpl-debug', 'title' => 'Show All Suggested Tasks', - 'href' => add_query_arg( 'prpl_show_all_suggested_tasks', '99', $this->current_url ), + 'href' => \add_query_arg( 'prpl_show_all_suggested_tasks', '99', $this->current_url ), ] ); @@ -101,7 +101,6 @@ public function add_toolbar_items( $admin_bar ) { * @return void */ protected function add_delete_submenu_item( $admin_bar ) { - if ( ! isset( $_SERVER['REQUEST_URI'] ) ) { return; } @@ -121,7 +120,7 @@ protected function add_delete_submenu_item( $admin_bar ) { 'id' => 'prpl-clear-cache', 'parent' => 'prpl-debug-delete', 'title' => 'Delete Cache', - 'href' => add_query_arg( 'prpl_clear_cache', '1', $this->current_url ), + 'href' => \add_query_arg( 'prpl_clear_cache', '1', $this->current_url ), ] ); @@ -131,7 +130,7 @@ protected function add_delete_submenu_item( $admin_bar ) { 'id' => 'prpl-delete-pending-tasks', 'parent' => 'prpl-debug-delete', 'title' => 'Delete Pending Tasks', - 'href' => add_query_arg( 'prpl_delete_pending_tasks', '1', $this->current_url ), + 'href' => \add_query_arg( 'prpl_delete_pending_tasks', '1', $this->current_url ), ] ); @@ -141,7 +140,7 @@ protected function add_delete_submenu_item( $admin_bar ) { 'id' => 'prpl-delete-suggested-tasks', 'parent' => 'prpl-debug-delete', 'title' => 'Delete Suggested Tasks', - 'href' => add_query_arg( 'prpl_delete_suggested_tasks', '1', $this->current_url ), + 'href' => \add_query_arg( 'prpl_delete_suggested_tasks', '1', $this->current_url ), ] ); @@ -151,7 +150,7 @@ protected function add_delete_submenu_item( $admin_bar ) { 'id' => 'prpl-delete-licenses', 'parent' => 'prpl-debug-delete', 'title' => 'Delete Licenses', - 'href' => add_query_arg( 'prpl_delete_licenses', '1', $this->current_url ), + 'href' => \add_query_arg( 'prpl_delete_licenses', '1', $this->current_url ), ] ); @@ -161,7 +160,7 @@ protected function add_delete_submenu_item( $admin_bar ) { 'id' => 'prpl-delete-badges', 'parent' => 'prpl-debug-delete', 'title' => 'Delete Badges', - 'href' => add_query_arg( 'prpl_delete_badges', '1', $this->current_url ), + 'href' => \add_query_arg( 'prpl_delete_badges', '1', $this->current_url ), ] ); } @@ -173,7 +172,6 @@ protected function add_delete_submenu_item( $admin_bar ) { * @return void */ protected function add_upgrading_tasks_submenu_item( $admin_bar ) { - $admin_bar->add_node( [ 'id' => 'prpl-upgrading-tasks', @@ -187,16 +185,11 @@ protected function add_upgrading_tasks_submenu_item( $admin_bar ) { foreach ( $onboard_task_provider_ids as $task_provider_id ) { $task_provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( $task_provider_id ); // @phpstan-ignore-line method.nonObject if ( $task_provider ) { // @phpstan-ignore-line - $task_provider_details = $task_provider->get_task_details(); - if ( empty( $task_provider_details ) ) { - continue; - } - $admin_bar->add_node( [ 'id' => 'prpl-upgrading-task-' . $task_provider_id, 'parent' => 'prpl-upgrading-tasks', - 'title' => $task_provider_details['title'], + 'title' => $task_provider_id, ] ); } @@ -222,14 +215,11 @@ protected function add_suggested_tasks_submenu_item( $admin_bar ) { ] ); - // Get suggested tasks. - $suggested_tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); - $menu_items = [ - 'pending' => 'Pending', - 'completed' => 'Completed', - 'snoozed' => 'Snoozed', - 'pending_celebration' => 'Pending Celebration', + 'publish' => 'Pending', + 'trash' => 'Completed', + 'future' => 'Snoozed', + 'pending' => 'Pending Celebration', ]; foreach ( $menu_items as $key => $title ) { @@ -241,33 +231,34 @@ protected function add_suggested_tasks_submenu_item( $admin_bar ) { ] ); - foreach ( $suggested_tasks as $task ) { - if ( ! isset( $task['task_id'] ) || $key !== $task['status'] ) { - continue; - } - - $title = $task['task_id']; - if ( isset( $task['status'] ) && 'snoozed' === $task['status'] && isset( $task['time'] ) ) { - $until = is_float( $task['time'] ) ? '(forever)' : '(until ' . \gmdate( 'Y-m-d H:i', $task['time'] ) . ')'; - $title .= ' ' . $until; - } + // Get suggested tasks. + $suggested_tasks = \progress_planner()->get_suggested_tasks_db()->get( [ 'post_status' => $key ] ); - // Add delete button. - $delete_url = add_query_arg( - [ - 'prpl_delete_single_task' => $task['task_id'], - '_wpnonce' => wp_create_nonce( 'prpl_debug_tools' ), - ], - $this->current_url - ); + if ( ! empty( $suggested_tasks ) ) { + foreach ( $suggested_tasks as $task ) { + $title = $task->post_title; + if ( $task->post_status && 'future' === $task->post_status && $task->post_date ) { + $until = '(until ' . $task->post_date . ')'; + $title .= ' ' . $until; + } - $admin_bar->add_node( - [ - 'id' => 'prpl-suggested-' . $key . '-' . $title, - 'parent' => 'prpl-suggested-' . $key, - 'title' => $title . ' ×', - ] - ); + // Add delete button. + $delete_url = \add_query_arg( + [ + 'prpl_delete_single_task' => $task->task_id, + '_wpnonce' => \wp_create_nonce( 'prpl_debug_tools' ), + ], + $this->current_url + ); + + $admin_bar->add_node( + [ + 'id' => 'prpl-suggested-' . $key . '-' . $title, + 'parent' => 'prpl-suggested-' . $key, + 'title' => $title . ' ×', + ] + ); + } } } } @@ -318,7 +309,7 @@ protected function add_activities_submenu_item( $admin_bar ) { protected function add_toggle_migrations_submenu_item( $admin_bar ) { $debug_enabled = \get_option( 'prpl_debug_migrations', false ); $title = $debug_enabled ? 'Upgrade Migrations Enabled' : 'Upgrade Migrations Disabled'; - $href = add_query_arg( 'prpl_toggle_migrations', '1', $this->current_url ); + $href = \add_query_arg( 'prpl_toggle_migrations', '1', $this->current_url ); $admin_bar->add_node( [ @@ -342,7 +333,7 @@ public function check_toggle_migrations() { if ( ! isset( $_GET['prpl_toggle_migrations'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended $_GET['prpl_toggle_migrations'] !== '1' || // phpcs:ignore WordPress.Security.NonceVerification.Recommended - ! current_user_can( 'manage_options' ) + ! \current_user_can( 'manage_options' ) ) { return; } @@ -359,7 +350,7 @@ public function check_toggle_migrations() { } // Redirect to the same page without the parameter. - wp_safe_redirect( remove_query_arg( [ 'prpl_toggle_migrations', '_wpnonce' ] ) ); + \wp_safe_redirect( \remove_query_arg( [ 'prpl_toggle_migrations', '_wpnonce' ] ) ); exit; } @@ -372,11 +363,10 @@ public function check_toggle_migrations() { * @return void */ public function check_delete_pending_tasks() { - if ( ! isset( $_GET['prpl_delete_pending_tasks'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended $_GET['prpl_delete_pending_tasks'] !== '1' || // phpcs:ignore WordPress.Security.NonceVerification.Recommended - ! current_user_can( 'manage_options' ) + ! \current_user_can( 'manage_options' ) ) { return; } @@ -384,21 +374,16 @@ public function check_delete_pending_tasks() { // Verify nonce for security. $this->verify_nonce(); - // Update the tasks. - \progress_planner()->get_settings()->set( - 'tasks', - array_values( - array_filter( // Filter out pending tasks. - \progress_planner()->get_settings()->get( 'tasks', [] ), // Get all tasks. - function ( $task ) { - return 'pending' !== $task['status']; - } - ) - ) - ); + // Get pending tasks. + $pending_tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'post_status' => 'publish' ] ); + + // Delete the pending tasks. + foreach ( $pending_tasks as $task ) { + \progress_planner()->get_suggested_tasks_db()->delete_recommendation( (int) $task->ID ); + } // Redirect to the same page without the parameter. - wp_safe_redirect( remove_query_arg( [ 'prpl_delete_pending_tasks', '_wpnonce' ] ) ); + \wp_safe_redirect( \remove_query_arg( [ 'prpl_delete_pending_tasks', '_wpnonce' ] ) ); exit; } @@ -411,11 +396,10 @@ function ( $task ) { * @return void */ public function check_delete_badges() { - if ( ! isset( $_GET['prpl_delete_badges'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended $_GET['prpl_delete_badges'] !== '1' || // phpcs:ignore WordPress.Security.NonceVerification.Recommended - ! current_user_can( 'manage_options' ) + ! \current_user_can( 'manage_options' ) ) { return; } @@ -432,7 +416,7 @@ public function check_delete_badges() { \update_option( \Progress_Planner\Settings::OPTION_NAME, $progress_planner_settings ); // Redirect to the same page without the parameter. - wp_safe_redirect( remove_query_arg( [ 'prpl_delete_badges', '_wpnonce' ] ) ); + \wp_safe_redirect( \remove_query_arg( [ 'prpl_delete_badges', '_wpnonce' ] ) ); exit; } @@ -445,7 +429,7 @@ public function check_delete_badges() { public function check_show_all_suggested_tasks( $max_items_per_category ) { if ( ! isset( $_GET['prpl_show_all_suggested_tasks'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended - ! current_user_can( 'manage_options' ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended + ! \current_user_can( 'manage_options' ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended ) { return $max_items_per_category; } @@ -478,13 +462,13 @@ protected function add_more_info_submenu_item( $admin_bar ) { ); // Add Remote Server URL info. - if ( function_exists( 'progress_planner' ) ) { + if ( \function_exists( 'progress_planner' ) ) { $remote_url = \progress_planner()->get_remote_server_root_url(); $admin_bar->add_node( [ 'id' => 'prpl-remote-url', 'parent' => 'prpl-more-info', - 'title' => 'Remote URL: ' . esc_html( $remote_url ), + 'title' => 'Remote URL: ' . \esc_html( $remote_url ), ] ); } @@ -540,7 +524,7 @@ public function check_delete_suggested_tasks() { if ( ! isset( $_GET['prpl_delete_suggested_tasks'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended $_GET['prpl_delete_suggested_tasks'] !== '1' || // phpcs:ignore WordPress.Security.NonceVerification.Recommended - ! current_user_can( 'manage_options' ) + ! \current_user_can( 'manage_options' ) ) { return; } @@ -549,10 +533,10 @@ public function check_delete_suggested_tasks() { $this->verify_nonce(); // Delete the option. - \progress_planner()->get_settings()->set( 'tasks', [] ); + \progress_planner()->get_suggested_tasks_db()->delete_all_recommendations(); // Redirect to the same page without the parameter. - wp_safe_redirect( remove_query_arg( [ 'prpl_delete_suggested_tasks', '_wpnonce' ] ) ); + \wp_safe_redirect( \remove_query_arg( [ 'prpl_delete_suggested_tasks', '_wpnonce' ] ) ); exit; } @@ -568,8 +552,8 @@ public function check_clear_cache() { if ( ! isset( $_GET['prpl_clear_cache'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended $_GET['prpl_clear_cache'] !== '1' || // phpcs:ignore WordPress.Security.NonceVerification.Recommended - ! current_user_can( 'manage_options' ) || - ! function_exists( 'progress_planner' ) + ! \current_user_can( 'manage_options' ) || + ! \function_exists( 'progress_planner' ) ) { return; } @@ -581,7 +565,7 @@ public function check_clear_cache() { \progress_planner()->get_utils__cache()->delete_all(); // Redirect to the same page without the parameter. - wp_safe_redirect( remove_query_arg( [ 'prpl_clear_cache', '_wpnonce' ] ) ); + \wp_safe_redirect( \remove_query_arg( [ 'prpl_clear_cache', '_wpnonce' ] ) ); exit; } @@ -597,7 +581,7 @@ public function check_delete_licenses() { if ( ! isset( $_GET['prpl_delete_licenses'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended $_GET['prpl_delete_licenses'] !== '1' || // phpcs:ignore WordPress.Security.NonceVerification.Recommended - ! current_user_can( 'manage_options' ) + ! \current_user_can( 'manage_options' ) ) { return; } @@ -606,12 +590,12 @@ public function check_delete_licenses() { $this->verify_nonce(); // Delete the option. - delete_option( 'progress_planner_license_key' ); - delete_option( 'progress_planner_pro_license_key' ); - delete_option( 'progress_planner_pro_license_status' ); + \delete_option( 'progress_planner_license_key' ); + \delete_option( 'progress_planner_pro_license_key' ); + \delete_option( 'progress_planner_pro_license_status' ); // Redirect to the same page without the parameter. - wp_safe_redirect( remove_query_arg( [ 'prpl_delete_licenses', '_wpnonce' ] ) ); + \wp_safe_redirect( \remove_query_arg( [ 'prpl_delete_licenses', '_wpnonce' ] ) ); exit; } @@ -621,8 +605,8 @@ public function check_delete_licenses() { * @return void */ protected function verify_nonce() { - if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( \wp_unslash( $_GET['_wpnonce'] ), 'prpl_debug_tools' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - wp_die( esc_html__( 'Security check failed', 'progress-planner' ) ); + if ( ! isset( $_GET['_wpnonce'] ) || ! \wp_verify_nonce( \wp_unslash( $_GET['_wpnonce'] ), 'prpl_debug_tools' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + \wp_die( \esc_html__( 'Security check failed', 'progress-planner' ) ); } } @@ -637,7 +621,7 @@ protected function verify_nonce() { public function check_delete_single_task() { if ( ! isset( $_GET['prpl_delete_single_task'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended - ! current_user_can( 'manage_options' ) + ! \current_user_can( 'manage_options' ) ) { return; } @@ -645,13 +629,19 @@ public function check_delete_single_task() { // Verify nonce for security. $this->verify_nonce(); - $task_id = sanitize_text_field( wp_unslash( $_GET['prpl_delete_single_task'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $task_id = \sanitize_text_field( \wp_unslash( $_GET['prpl_delete_single_task'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + $task = \progress_planner()->get_suggested_tasks_db()->get_post( $task_id ); + + if ( ! $task ) { + return; + } // Delete the task. - \progress_planner()->get_suggested_tasks()->delete_task( $task_id ); + $task->delete(); // Redirect to the same page without the parameter. - wp_safe_redirect( remove_query_arg( [ 'prpl_delete_single_task', '_wpnonce' ] ) ); + \wp_safe_redirect( \remove_query_arg( [ 'prpl_delete_single_task', '_wpnonce' ] ) ); exit; } } diff --git a/classes/utils/class-deprecations.php b/classes/utils/class-deprecations.php new file mode 100644 index 000000000..23bcf79f7 --- /dev/null +++ b/classes/utils/class-deprecations.php @@ -0,0 +1,122 @@ + [ 'Progress_Planner\Activities\Activity', '1.1.1' ], + 'Progress_Planner\Query' => [ 'Progress_Planner\Activities\Query', '1.1.1' ], + 'Progress_Planner\Date' => [ 'Progress_Planner\Utils\Date', '1.1.1' ], + 'Progress_Planner\Cache' => [ 'Progress_Planner\Utils\Cache', '1.1.1' ], + 'Progress_Planner\Widgets\Activity_Scores' => [ 'Progress_Planner\Admin\Widgets\Activity_Scores', '1.1.1' ], + 'Progress_Planner\Widgets\Badge_Streak' => [ 'Progress_Planner\Admin\Widgets\Badge_Streak', '1.1.1' ], + 'Progress_Planner\Widgets\Challenge' => [ 'Progress_Planner\Admin\Widgets\Challenge', '1.1.1' ], + 'Progress_Planner\Widgets\Latest_Badge' => [ 'Progress_Planner\Admin\Widgets\Latest_Badge', '1.1.1' ], + 'Progress_Planner\Widgets\Published_Content' => [ 'Progress_Planner\Admin\Widgets\Published_Content', '1.1.1' ], + 'Progress_Planner\Widgets\Todo' => [ 'Progress_Planner\Admin\Widgets\Todo', '1.1.1' ], + 'Progress_Planner\Widgets\Whats_New' => [ 'Progress_Planner\Admin\Widgets\Whats_New', '1.1.1' ], + 'Progress_Planner\Widgets\Widget' => [ 'Progress_Planner\Admin\Widgets\Widget', '1.1.1' ], + 'Progress_Planner\Rest_API_Stats' => [ 'Progress_Planner\Rest\Stats', '1.1.1' ], + 'Progress_Planner\Rest_API_Tasks' => [ 'Progress_Planner\Rest\Tasks', '1.1.1' ], + 'Progress_Planner\Data_Collector\Base_Data_Collector' => [ 'Progress_Planner\Suggested_Tasks\Data_Collector\Base_Data_Collector', '1.1.1' ], + 'Progress_Planner\Data_Collector\Data_Collector_Manager' => [ 'Progress_Planner\Suggested_Tasks\Data_Collector\Data_Collector_Manager', '1.1.1' ], + 'Progress_Planner\Data_Collector\Hello_World' => [ 'Progress_Planner\Suggested_Tasks\Data_Collector\Hello_World', '1.1.1' ], + 'Progress_Planner\Data_Collector\Inactive_Plugins' => [ 'Progress_Planner\Suggested_Tasks\Data_Collector\Inactive_Plugins', '1.1.1' ], + 'Progress_Planner\Data_Collector\Last_Published_Post' => [ 'Progress_Planner\Suggested_Tasks\Data_Collector\Last_Published_Post', '1.1.1' ], + 'Progress_Planner\Data_Collector\Post_Author' => [ 'Progress_Planner\Suggested_Tasks\Data_Collector\Post_Author', '1.1.1' ], + 'Progress_Planner\Data_Collector\Sample_Page' => [ 'Progress_Planner\Suggested_Tasks\Data_Collector\Sample_Page', '1.1.1' ], + 'Progress_Planner\Data_Collector\Uncategorized_Category' => [ 'Progress_Planner\Suggested_Tasks\Data_Collector\Uncategorized_Category', '1.1.1' ], + 'Progress_Planner\Chart' => [ 'Progress_Planner\UI\Chart', '1.1.1' ], + 'Progress_Planner\Popover' => [ 'Progress_Planner\UI\Popover', '1.1.1' ], + 'Progress_Planner\Debug_Tools' => [ 'Progress_Planner\Utils\Debug_Tools', '1.1.1' ], + 'Progress_Planner\Onboard' => [ 'Progress_Planner\Utils\Onboard', '1.1.1' ], + 'Progress_Planner\Playground' => [ 'Progress_Planner\Utils\Playground', '1.1.1' ], + + 'Progress_Planner\Admin\Widgets\Published_Content' => [ 'Progress_Planner\Admin\Widgets\Content_Activity', '1.3.0' ], + + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Task_Local' => [ 'Progress_Planner\Suggested_Tasks\Task', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Local_Tasks_Interface' => [ 'Progress_Planner\Suggested_Tasks\Tasks_Interface', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks_Manager' => [ 'Progress_Planner\Suggested_Tasks\Tasks_Manager', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Local_Task_Factory' => [ 'Progress_Planner\Suggested_Tasks\Task_Factory', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time' => [ 'Progress_Planner\Suggested_Tasks\Providers\Task', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Repetitive' => [ 'Progress_Planner\Suggested_Tasks\Providers\Repetitive', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Local_Tasks' => [ 'Progress_Planner\Suggested_Tasks\Providers\Tasks', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\User' => [ 'Progress_Planner\Suggested_Tasks\Providers\User', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Add_Yoast_Providers' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Add_Yoast_Providers', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Archive_Author' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Archive_Author', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Archive_Date' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Archive_Date', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Archive_Format' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Archive_Format', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Crawl_Settings_Emoji_Scripts' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Crawl_Settings_Emoji_Scripts', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Crawl_Settings_Feed_Authors' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Crawl_Settings_Feed_Authors', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Crawl_Settings_Feed_Global_Comments' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Crawl_Settings_Feed_Global_Comments', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Media_Pages' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Media_Pages', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Organization_Logo' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Organization_Logo', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Integrations\Yoast\Yoast_Provider' => [ 'Progress_Planner\Suggested_Tasks\Providers\Integrations\Yoast\Yoast_Provider', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Blog_Description' => [ 'Progress_Planner\Suggested_Tasks\Providers\Blog_Description', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Debug_Display' => [ 'Progress_Planner\Suggested_Tasks\Providers\Debug_Display', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Disable_Comments' => [ 'Progress_Planner\Suggested_Tasks\Providers\Disable_Comments', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Hello_World' => [ 'Progress_Planner\Suggested_Tasks\Providers\Hello_World', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Permalink_Structure' => [ 'Progress_Planner\Suggested_Tasks\Providers\Permalink_Structure', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Php_Version' => [ 'Progress_Planner\Suggested_Tasks\Providers\Php_Version', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Remove_Inactive_Plugins' => [ 'Progress_Planner\Suggested_Tasks\Providers\Remove_Inactive_Plugins', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Rename_Uncategorized_Category' => [ 'Progress_Planner\Suggested_Tasks\Providers\Rename_Uncategorized_Category', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Sample_Page' => [ 'Progress_Planner\Suggested_Tasks\Providers\Sample_Page', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Search_Engine_Visibility' => [ 'Progress_Planner\Suggested_Tasks\Providers\Search_Engine_Visibility', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Set_Valuable_Post_Types' => [ 'Progress_Planner\Suggested_Tasks\Providers\Set_Valuable_Post_Types', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Settings_Saved' => [ 'Progress_Planner\Suggested_Tasks\Providers\Settings_Saved', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\One_Time\Site_Icon' => [ 'Progress_Planner\Suggested_Tasks\Providers\Site_Icon', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Repetitive\Core_Update' => [ 'Progress_Planner\Suggested_Tasks\Providers\Core_Update', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Repetitive\Create' => [ 'Progress_Planner\Suggested_Tasks\Providers\Repetitive\Create', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Local_Tasks\Providers\Repetitive\Review' => [ 'Progress_Planner\Suggested_Tasks\Providers\Repetitive\Review', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Remote_Tasks\Remote_Task_Factory' => [ 'Progress_Planner\Suggested_Tasks\Task_Factory', '1.4.0' ], + 'Progress_Planner\Suggested_Tasks\Remote_Tasks\Remote_Task' => [ 'Progress_Planner\Suggested_Tasks\Task', '1.4.0' ], + ]; + + /** + * Deprecated methods for the Base class. + * + * @var array + */ + const BASE_METHODS = [ + 'get_query' => [ 'get_activities__query', '1.1.1' ], + 'get_date' => [ 'get_utils__date', '1.1.1' ], + 'get_widgets__suggested_tasks' => [ 'get_admin__widgets__suggested_tasks', '1.1.1' ], + 'get_widgets__activity_scores' => [ 'get_admin__widgets__activity_scores', '1.1.1' ], + 'get_widgets__todo' => [ 'get_admin__widgets__todo', '1.1.1' ], + 'get_widgets__challenge' => [ 'get_admin__widgets__challenge', '1.1.1' ], + 'get_widgets__latest_badge' => [ 'get_admin__widgets__latest_badge', '1.1.1' ], + 'get_widgets__badge_streak' => [ 'get_admin__widgets__badge_streak', '1.1.1' ], + 'get_widgets__published_content' => [ 'get_admin__widgets__published_content', '1.1.1' ], + 'get_widgets__whats_new' => [ 'get_admin__widgets__whats_new', '1.1.1' ], + 'get_onboard' => [ 'get_utils__onboard', '1.1.1' ], + 'get_cache' => [ 'get_utils__cache', '1.1.1' ], + 'get_rest_api_stats' => [ 'get_rest__stats', '1.1.1' ], + 'get_rest_api_tasks' => [ 'get_rest__tasks', '1.1.1' ], + 'get_data_collector__data_collector_manager' => [ 'get_suggested_tasks__data_collector__data_collector_manager', '1.1.1' ], + 'get_debug_tools' => [ 'get_utils__debug_tools', '1.1.1' ], + 'get_playground' => [ 'get_utils__playground', '1.1.1' ], + 'get_chart' => [ 'get_ui__chart', '1.1.1' ], + 'get_popover' => [ 'get_ui__popover', '1.1.1' ], + + 'get_admin__widgets__published_content' => [ 'get_admin__widgets__content_activity', '1.3.0' ], + ]; +} diff --git a/classes/utils/class-onboard.php b/classes/utils/class-onboard.php index 019fc534a..99c3fa0c4 100644 --- a/classes/utils/class-onboard.php +++ b/classes/utils/class-onboard.php @@ -23,7 +23,6 @@ class Onboard { * Constructor. */ public function __construct() { - // Handle saving data from the onboarding form response. \add_action( 'wp_ajax_progress_planner_save_onboard_data', [ $this, 'save_onboard_response' ] ); @@ -68,7 +67,7 @@ public function save_onboard_response() { \wp_send_json_error( [ 'message' => \esc_html__( 'Missing data.', 'progress-planner' ) ] ); } - $license_key = \sanitize_text_field( wp_unslash( $_POST['key'] ) ); + $license_key = \sanitize_text_field( \wp_unslash( $_POST['key'] ) ); // False also if option value has not changed. if ( \update_option( 'progress_planner_license_key', $license_key, false ) ) { diff --git a/classes/utils/class-playground.php b/classes/utils/class-playground.php index 6626be211..37a69b80c 100644 --- a/classes/utils/class-playground.php +++ b/classes/utils/class-playground.php @@ -17,8 +17,9 @@ class Playground { */ public function __construct() { \add_action( 'init', [ $this, 'register_hooks' ], 9 ); - \add_action( 'plugins_loaded', [ $this, 'enable_debug_tools' ], 1 ); + \add_filter( 'progress_planner_tasks_show_ui', '__return_true' ); + \add_action( 'admin_footer', [ $this, 'inject_playground_js_patch' ] ); } /** @@ -29,7 +30,7 @@ public function __construct() { public function register_hooks() { if ( ! \get_option( 'progress_planner_license_key', false ) && ! \get_option( 'progress_planner_demo_data_generated', false ) ) { $this->generate_data(); - \update_option( 'progress_planner_license_key', str_replace( ' ', '-', $this->create_random_string( 20 ) ) ); + \update_option( 'progress_planner_license_key', \str_replace( ' ', '-', $this->create_random_string( 20 ) ) ); \update_option( 'progress_planner_force_show_onboarding', false ); \update_option( 'progress_planner_todo', @@ -49,6 +50,8 @@ public function register_hooks() { \add_action( 'progress_planner_admin_page_header_before', [ $this, 'show_header_notice' ] ); \add_action( 'wp_ajax_progress_planner_hide_onboarding', [ $this, 'hide_onboarding' ] ); \add_action( 'wp_ajax_progress_planner_show_onboarding', [ $this, 'show_onboarding' ] ); + + \progress_planner()->get_settings()->set( 'activation_date', ( new \DateTime() )->modify( '-2 months' )->format( 'Y-m-d' ) ); } /** @@ -76,14 +79,13 @@ private function toggle_onboarding( $action ) { } if ( $action === 'hide' ) { - \add_option( 'progress_planner_license_key', str_replace( ' ', '-', $this->create_random_string( 20 ) ) ); - \update_option( 'progress_planner_force_show_onboarding', false ); + \add_option( 'progress_planner_license_key', \str_replace( ' ', '-', $this->create_random_string( 20 ) ) ); $message = \esc_html__( 'Onboarding hidden successfully', 'progress-planner' ); } else { \delete_option( 'progress_planner_license_key' ); - \update_option( 'progress_planner_force_show_onboarding', true ); $message = \esc_html__( 'Onboarding shown successfully', 'progress-planner' ); } + \update_option( 'progress_planner_force_show_onboarding', $action !== 'hide' ); \wp_send_json_success( [ 'message' => $message ] ); } @@ -118,7 +120,7 @@ public function show_header_notice() { } $show_onboarding = \get_option( 'progress_planner_force_show_onboarding', false ); - $button_text = $show_onboarding ? __( 'Hide onboarding', 'progress-planner' ) : __( 'Show onboarding', 'progress-planner' ); + $button_text = $show_onboarding ? \__( 'Hide onboarding', 'progress-planner' ) : \__( 'Show onboarding', 'progress-planner' ); $action = $show_onboarding ? 'hide' : 'show'; $nonce = \wp_create_nonce( "progress_planner_{$action}_onboarding" ); ?> @@ -126,7 +128,7 @@ public function show_header_notice() {
    @@ -180,8 +182,8 @@ public function generate_data() { */ private function create_random_post( $random_date = true, $post_type = 'post' ) { $postarr = [ - 'post_title' => str_replace( '.', '', $this->create_random_string( 5 ) ), - 'post_content' => $this->create_random_string( wp_rand( 200, 500 ) ), + 'post_title' => \str_replace( '.', '', $this->create_random_string( 5 ) ), + 'post_content' => $this->create_random_string( \wp_rand( 200, 500 ) ), 'post_status' => 'publish', 'post_type' => $post_type, 'post_date' => $this->get_random_date_last_12_months(), @@ -204,13 +206,13 @@ private function get_random_date_last_12_months() { $now = \current_time( 'timestamp' ); // Timestamp for 12 months ago. - $last_year = strtotime( '-12 months', $now ); + $last_year = \strtotime( '-12 months', $now ); // Generate a random timestamp between last year and now. - $random_timestamp = wp_rand( $last_year, $now ); + $random_timestamp = \wp_rand( $last_year, $now ); // Format the random timestamp as a MySQL datetime string. - return gmdate( 'Y-m-d H:i:s', $random_timestamp ); + return \gmdate( 'Y-m-d H:i:s', $random_timestamp ); } /** @@ -228,21 +230,74 @@ private function create_random_string( $length ) { while ( $words_remaining > 0 ) { // Randomly decide the length of the current sentence (between 8 and 12 words). - $sentence_length = min( wp_rand( 8, 12 ), $words_remaining ); + $sentence_length = \min( \wp_rand( 8, 12 ), $words_remaining ); $words_remaining -= $sentence_length; // Select random words for the sentence. - $word_keys = array_rand( $words, $sentence_length ); + $word_keys = \array_rand( $words, $sentence_length ); $sentence = ''; foreach ( (array) $word_keys as $key ) { - $sentence .= $words[ $key ] . ' '; + $sentence .= $words[ $key ] . ' '; } // Capitalize the first word and add a period at the end. - $sentences .= ucfirst( trim( $sentence ) ) . '. '; + $sentences .= \ucfirst( \trim( $sentence ) ) . '. '; } - return trim( $sentences ); + return \trim( $sentences ); + } + + /** + * Inject a JS patch to work around the Playground environment. + * We override PUT requests to POST requests. + * + * @return void + */ + public function inject_playground_js_patch() { + ?> + + get_suggested_tasks()->get_tasks_manager()->get_task_provider( $task_id ); + return new Task( + [ + 'task_id' => $task_id, + 'category' => $task_provider ? $task_provider->get_provider_category() : '', + 'provider_id' => $task_provider ? $task_provider->get_provider_id() : '', + ] + ); + } + + // Repetitive tasks (update-core-202449). + $task_provider_id = \substr( $task_id, 0, $last_pos ); + + // Check for legacy create-post task_id, old task_ids were migrated to create-post-short' or 'create-post-long' (since we had 2 such tasks per week). + if ( 'create-post-short' === $task_provider_id || 'create-post-long' === $task_provider_id ) { + $task_provider_id = 'create-post'; + } + + $task_provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( $task_provider_id ); + + return new Task( + [ + 'task_id' => $task_id, + 'category' => $task_provider ? $task_provider->get_provider_category() : '', + 'provider_id' => $task_provider ? $task_provider->get_provider_id() : '', + 'date' => \substr( $task_id, $last_pos + 1 ), + ] + ); + } + + // Legacy piped format. + $data = [ 'task_id' => $task_id ]; + + // Parse detailed (piped) format (date/202510|long/1|provider_id/create-post). + $parts = \explode( '|', $task_id ); + foreach ( $parts as $part ) { + $part = \explode( '/', $part ); + if ( 2 !== \count( $part ) ) { + continue; + } + // Date should be a string, not a number. + $data[ $part[0] ] = ( 'date' !== $part[0] && \is_numeric( $part[1] ) ) + ? (int) $part[1] + : $part[1]; + } + \ksort( $data ); + + // Convert (int) 1 and (int) 0 to (bool) true and (bool) false. + if ( isset( $data['long'] ) ) { + $data['long'] = (bool) $data['long']; + } + if ( isset( $data['type'] ) && ! isset( $data['provider_id'] ) ) { + $data['provider_id'] = $data['type']; + unset( $data['type'] ); + } + + if ( isset( $data['provider_id'] ) ) { + $task_provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( $data['provider_id'] ); // @phpstan-ignore-line + $data['category'] = $task_provider ? $task_provider->get_provider_category() : ''; + } + + return new Task( $data ); + } +} diff --git a/classes/utils/class-system-status.php b/classes/utils/class-system-status.php new file mode 100644 index 000000000..8aeed9b5f --- /dev/null +++ b/classes/utils/class-system-status.php @@ -0,0 +1,149 @@ + 'publish', + 'post_type' => 'post', + 'date_query' => [ [ 'after' => '1 week ago' ] ], + 'posts_per_page' => 10, + ] + ) + ); + + // Get the number of activities in the past week. + $data['activities'] = \count( + \progress_planner()->get_activities__query()->query_activities( + [ + 'start_date' => new \DateTime( '-7 days' ), + ] + ) + ); + + // Get the website activity score. + $activity_score = new Activity_Scores(); + $data['website_activity'] = [ + 'score' => $activity_score->get_score(), + 'checklist' => $activity_score->get_checklist_results(), + ]; + + // Get the badges. + $badges = \array_merge( + \progress_planner()->get_badges()->get_badges( 'content' ), + \progress_planner()->get_badges()->get_badges( 'maintenance' ), + \progress_planner()->get_badges()->get_badges( 'monthly_flat' ) + ); + + $data['badges'] = []; + foreach ( $badges as $badge ) { + $data['badges'][ $badge->get_id() ] = \array_merge( + [ + 'id' => $badge->get_id(), + 'name' => $badge->get_name(), + ], + $badge->progress_callback() + ); + } + + $data['latest_badge'] = \progress_planner()->get_badges()->get_latest_completed_badge(); + + $scores = \progress_planner()->get_ui__chart()->get_chart_data( + [ + 'items_callback' => function ( $start_date, $end_date ) { + return \progress_planner()->get_activities__query()->query_activities( + [ + 'start_date' => $start_date, + 'end_date' => $end_date, + ] + ); + }, + 'dates_params' => [ + 'start_date' => \DateTime::createFromFormat( 'Y-m-d', \gmdate( 'Y-m-01' ) )->modify( '-6 months' ), + 'end_date' => new \DateTime(), + 'frequency' => 'monthly', + 'format' => 'M', + ], + 'count_callback' => function ( $activities, $date ) { + $score = 0; + foreach ( $activities as $activity ) { + $score += $activity->get_points( $date ); + } + return $score * 100 / Base::SCORE_TARGET; + }, + 'normalized' => true, + 'max' => 100, + ] + ); + + $data['scores'] = []; + foreach ( $scores as $item ) { + $data['scores'][] = [ + 'label' => $item['label'], + 'value' => $item['score'], + ]; + } + + // The website URL. + $data['website'] = \home_url(); + + // Timezone offset. + $data['timezone_offset'] = \wp_timezone()->getOffset( new \DateTime( 'midnight' ) ) / 3600; + $ravis_recommendations = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'post_status' => 'publish' ] ); + $data['recommendations'] = []; + foreach ( $ravis_recommendations as $recommendation ) { + $r = [ + 'id' => $recommendation->task_id, + 'title' => $recommendation->post_title, + 'url' => $recommendation->url, + 'provider_id' => $recommendation->get_provider_id(), + ]; + + if ( 'user' === $recommendation->get_provider_id() ) { + $r['points'] = (int) $recommendation->points; + } + $data['recommendations'][] = $r; + } + + $data['plugin_url'] = \esc_url( \get_admin_url( null, 'admin.php?page=progress-planner' ) ); + + $active_plugins = \get_option( 'active_plugins' ); + $data['plugins'] = []; + foreach ( $active_plugins as $plugin ) { + $plugin_data = \get_plugin_data( \WP_PLUGIN_DIR . '/' . $plugin ); + $data['plugins'][] = [ + 'plugin' => $plugin, + 'name' => $plugin_data['Name'] ?? 'N/A', // @phpstan-ignore-line nullCoalesce.offset + 'version' => $plugin_data['Version'] ?? 'N/A', // @phpstan-ignore-line nullCoalesce.offset + ]; + } + + return $data; + } +} diff --git a/classes/wp-cli/class-get-stats-command.php b/classes/wp-cli/class-get-stats-command.php new file mode 100644 index 000000000..bb05c85a6 --- /dev/null +++ b/classes/wp-cli/class-get-stats-command.php @@ -0,0 +1,45 @@ +get_system_status() ) ); // @phpstan-ignore-line + } +} diff --git a/classes/wp-cli/class-task-command.php b/classes/wp-cli/class-task-command.php new file mode 100644 index 000000000..b9737b2b0 --- /dev/null +++ b/classes/wp-cli/class-task-command.php @@ -0,0 +1,337 @@ +] + * : Accepted values: table, csv, json, count, yaml. Default: table + * + * [--fields=] + * : Limit the output to specific fields. Defaults to all fields. + * + * [--=] + * : Filter by one or more fields. + * + * ## EXAMPLES + * + * # List all tasks + * $ wp prpl task list + * + * # List tasks in JSON format + * $ wp prpl task list --format=json + * + * @param array $args Command arguments. + * @param array $assoc_args Command associative arguments. + * + * @return void + */ + public function list( $args, $assoc_args ) { + $tasks = $this->get_tasks( $assoc_args ); + + if ( empty( $tasks ) ) { + WP_CLI::log( 'No tasks found.' ); // @phpstan-ignore-line + return; + } + + $format = isset( $assoc_args['format'] ) ? $assoc_args['format'] : 'table'; + $fields = isset( $assoc_args['fields'] ) ? \explode( ',', $assoc_args['fields'] ) : [ 'task_id', 'provider_id', 'category', 'date', 'status' ]; + + WP_CLI\Utils\format_items( $format, $tasks, $fields ); // @phpstan-ignore-line + } + + /** + * Get a task. + * + * ## OPTIONS + * + * + * : The ID of the task to get. + * + * [--format=] + * : Accepted values: table, json, yaml. Default: table + * + * ## EXAMPLES + * + * # Get a task + * $ wp prpl task get 123 + * + * @param array $args Command arguments. + * @param array $assoc_args Command associative arguments. + * + * @return void + */ + public function get( $args, $assoc_args ) { + $task_id = $args[0]; + $task = $this->get_task( $task_id ); + + if ( ! $task ) { + \WP_CLI::error( "Task {$task_id} not found." ); // @phpstan-ignore-line + return; + } + + $format = isset( $assoc_args['format'] ) ? $assoc_args['format'] : 'table'; + \WP_CLI\Utils\format_items( $format, [ $task ], \array_keys( $task ) ); // @phpstan-ignore-line + } + + /** + * Update a task. + * + * ## OPTIONS + * + * + * : The ID of the task to update. + * + * [--=] + * : One or more fields to update. + * + * ## EXAMPLES + * + * # Update task status + * $ wp prpl task update 123 --status=completed + * + * @param array $args Command arguments. + * @param array $assoc_args Command associative arguments. + * + * @return void + */ + public function update( $args, $assoc_args ) { + $task_id = $args[0]; + $task = $this->get_task( $task_id ); + + if ( ! $task ) { + \WP_CLI::error( "Task {$task_id} not found." ); // @phpstan-ignore-line + return; + } + + $updated = $this->update_task( $task_id, $assoc_args ); + + if ( $updated ) { + \WP_CLI::success( "Task {$task_id} updated." ); // @phpstan-ignore-line + } else { + \WP_CLI::error( "Failed to update task {$task_id}." ); // @phpstan-ignore-line + } + } + + /** + * Delete a task. + * + * ## OPTIONS + * + * + * : The ID of the task to delete. + * + * [--force] + * : Skip the trash bin and permanently delete the task. + * + * ## EXAMPLES + * + * # Delete a task + * $ wp prpl task delete 123 + * + * @param array $args Command arguments. + * @param array $assoc_args Command associative arguments. + * + * @return void + */ + public function delete( $args, $assoc_args ) { + $task_id = $args[0]; + $task = $this->get_task( $task_id ); + + if ( ! $task ) { + \WP_CLI::error( "Task {$task_id} not found." ); // @phpstan-ignore-line + return; + } + + $force = isset( $assoc_args['force'] ) && $assoc_args['force']; + $deleted = $this->delete_task( $task_id, $force ); + + if ( $deleted ) { + \WP_CLI::success( "Task {$task_id} deleted." ); // @phpstan-ignore-line + } else { + \WP_CLI::error( "Failed to delete task {$task_id}." ); // @phpstan-ignore-line + } + } + + /** + * Get tasks from the database. + * + * @param array $args Query arguments. + * @return array + */ + private function get_tasks( $args ) { + $tasks = \progress_planner()->get_settings()->get( 'tasks', [] ); // Get tasks from the database, without filtering. + + if ( empty( $tasks ) ) { + return []; + } + + // Set fields which are not set for all tasks. + foreach ( $tasks as $key => $task ) { + if ( ! isset( $task['date'] ) ) { + $tasks[ $key ]['date'] = ''; + } + } + + return $tasks; + } + + /** + * Get a single task from the database. + * + * @param string $task_id Task ID. + * @return \Progress_Planner\Suggested_Tasks\Task|null + */ + private function get_task( $task_id ) { + $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $task_id ] ); + if ( empty( $tasks ) ) { + \WP_CLI::log( 'Task not found.' ); // @phpstan-ignore-line + return null; + } + + return $tasks[0]; + } + + /** + * Update a task in the database. + * + * @param string $task_id Task ID. + * @param array $data Task data to update. + * @return bool + */ + private function update_task( $task_id, $data ) { + $task = $this->get_task( $task_id ); + if ( ! $task ) { + \WP_CLI::log( 'Task not found.' ); // @phpstan-ignore-line + return false; + } + + \progress_planner()->get_suggested_tasks_db()->update_recommendation( $task->ID, $data ); + return true; + } + + /** + * Delete a task from the database. + * + * @param string $task_id Task ID. + * @param bool $force Whether to force delete. + * @return bool + */ + private function delete_task( $task_id, $force ) { + $task = $this->get_task( $task_id ); + if ( ! $task ) { + \WP_CLI::log( 'Task not found.' ); // @phpstan-ignore-line + return false; + } + + \progress_planner()->get_suggested_tasks_db()->delete_recommendation( $task->ID ); + return true; + } + + /** + * Create a task. + * + * ## OPTIONS + * + * [--task_id=] + * : The ID of the task. If not provided, one will be generated. + * + * [--title=] + * : The title of the task. Default: "Test task {timestamp}" + * + * [--description=<description>] + * : The description of the task. Default: "Test description {timestamp}" + * + * [--points=<points>] + * : The points value for the task. Default: 1 + * + * [--provider_id=<provider_id>] + * : The provider ID. Default: "collaborator" + * + * [--category=<category>] + * : The task category. Default: "collaborator" + * + * [--status=<status>] + * : The task status. Default: "pending" + * + * [--is_completed_callback=<is_completed_callback>] + * : The callback to check if the task is completed. + * + * ## EXAMPLES + * + * # Create a task with default values + * $ wp prpl task create + * + * # Create a task with custom values + * $ wp prpl task create --title="My Task" --description="Task description" --points=5 + * + * @param array $args Command arguments. + * @param array $assoc_args Command associative arguments. + * + * @return void + */ + public function create( $args, $assoc_args ) { + + $task_id = isset( $assoc_args['task_id'] ) ? $assoc_args['task_id'] : ''; + $title = isset( $assoc_args['title'] ) ? $assoc_args['title'] : ''; + $description = isset( $assoc_args['description'] ) ? $assoc_args['description'] : 'Test description '; + $points = isset( $assoc_args['points'] ) ? (int) $assoc_args['points'] : 1; + $provider_id = isset( $assoc_args['provider_id'] ) ? $assoc_args['provider_id'] : 'collaborator'; + $category = isset( $assoc_args['category'] ) ? $assoc_args['category'] : 'collaborator'; + $status = isset( $assoc_args['status'] ) ? $assoc_args['status'] : 'pending'; + $is_completed_callback = isset( $assoc_args['is_completed_callback'] ) ? $assoc_args['is_completed_callback'] : null; + $dismissable = isset( $assoc_args['dismissable'] ) ? $assoc_args['dismissable'] : true; + $snoozable = isset( $assoc_args['snoozable'] ) ? $assoc_args['snoozable'] : true; + + if ( empty( $task_id ) || empty( $title ) ) { + \WP_CLI::error( 'task_id and title are required.' ); // @phpstan-ignore-line + return; + } + + // We're creating a new task. + \progress_planner()->get_suggested_tasks_db()->add( + [ + 'task_id' => $task_id, + 'post_title' => $title, + 'description' => $description, + 'points' => $points, + 'provider_id' => $provider_id, + 'category' => $category, + 'status' => $status, + 'dismissable' => $dismissable, + 'snoozable' => $snoozable, + 'is_completed_callback' => $is_completed_callback, + ] + ); + + \WP_CLI::success( "Task {$task_id} created." ); // @phpstan-ignore-line + } +} diff --git a/composer.json b/composer.json index 4dedff8de..868d41e75 100644 --- a/composer.json +++ b/composer.json @@ -17,14 +17,15 @@ "phpstan/phpstan": "^2.0", "szepeviktor/phpstan-wordpress": "^2.0", "phpstan/extension-installer": "^1.4", - "yoast/yoastcs": "^3.0" + "yoast/yoastcs": "^3.0", + "friendsofphp/php-cs-fixer": "^3.75" }, "scripts": { "check-cs": [ "@php ./vendor/bin/phpcs -s" ], "fix-cs": [ - "@php ./vendor/bin/phpcbf" + "@php ./vendor/bin/phpcbf && ./vendor/bin/php-cs-fixer fix . --allow-risky=yes" ], "lint": [ "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --show-deprecated --exclude vendor --exclude node_modules --exclude .git" diff --git a/composer.lock b/composer.lock index 785eaad41..fec410158 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "aab239371254a726f917cc228cdb7319", + "content-hash": "94ed5942457f84745b0ce51a8b83716d", "packages": [], "packages-dev": [ { @@ -179,6 +179,296 @@ }, "time": "2024-08-29T20:15:04+00:00" }, + { + "name": "clue/ndjson-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-09-19T14:15:21+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, { "name": "dealerdirect/phpcodesniffer-composer-installer", "version": "v1.0.0", @@ -328,105 +618,317 @@ "time": "2022-12-30T00:15:36+00:00" }, { - "name": "hamcrest/hamcrest-php", - "version": "v2.0.1", + "name": "evenement/evenement", + "version": "v3.0.2", "source": { "type": "git", - "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3" + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", - "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", "shasum": "" }, "require": { - "php": "^5.3|^7.0|^8.0" - }, - "replace": { - "cordoval/hamcrest-php": "*", - "davedevelopment/hamcrest-php": "*", - "kodova/hamcrest-php": "*" + "php": ">=7.0" }, "require-dev": { - "phpunit/php-file-iterator": "^1.4 || ^2.0", - "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0" + "phpunit/phpunit": "^9 || ^6" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.1-dev" - } - }, "autoload": { - "classmap": [ - "hamcrest" - ] + "psr-4": { + "Evenement\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], - "description": "This is the PHP port of Hamcrest Matchers", + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", "keywords": [ - "test" + "event-dispatcher", + "event-emitter" ], "support": { - "issues": "https://github.com/hamcrest/hamcrest-php/issues", - "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1" + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" }, - "time": "2020-07-09T08:09:16+00:00" + "time": "2023-08-08T05:53:35+00:00" }, { - "name": "mockery/mockery", - "version": "1.6.12", + "name": "fidry/cpu-core-counter", + "version": "1.2.0", "source": { "type": "git", - "url": "https://github.com/mockery/mockery.git", - "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "8520451a140d3f46ac33042715115e290cf5785f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", - "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", "shasum": "" }, "require": { - "hamcrest/hamcrest-php": "^2.0.1", - "lib-pcre": ">=7.0", - "php": ">=7.3" - }, - "conflict": { - "phpunit/phpunit": "<8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.6.17", - "symplify/easy-coding-standard": "^12.1.14" + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" }, "type": "library", "autoload": { - "files": [ - "library/helpers.php", - "library/Mockery.php" - ], "psr-4": { - "Mockery\\": "library/Mockery" + "Fidry\\CpuCoreCounter\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Pádraic Brady", - "email": "padraic.brady@gmail.com", - "homepage": "https://github.com/padraic", - "role": "Author" - }, + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + }, + "funding": [ { - "name": "Dave Marshall", + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2024-08-06T10:04:20+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.75.0", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "399a128ff2fdaf4281e4e79b755693286cdf325c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/399a128ff2fdaf4281e4e79b755693286cdf325c", + "reference": "399a128ff2fdaf4281e4e79b755693286cdf325c", + "shasum": "" + }, + "require": { + "clue/ndjson-react": "^1.0", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.3", + "ext-filter": "*", + "ext-hash": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.2", + "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.5", + "react/event-loop": "^1.0", + "react/promise": "^2.0 || ^3.0", + "react/socket": "^1.0", + "react/stream": "^1.0", + "sebastian/diff": "^4.0 || ^5.1 || ^6.0 || ^7.0", + "symfony/console": "^5.4 || ^6.4 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", + "symfony/finder": "^5.4 || ^6.4 || ^7.0", + "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", + "symfony/polyfill-mbstring": "^1.31", + "symfony/polyfill-php80": "^1.31", + "symfony/polyfill-php81": "^1.31", + "symfony/process": "^5.4 || ^6.4 || ^7.2", + "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3.1 || ^2.6", + "infection/infection": "^0.29.14", + "justinrainbow/json-schema": "^5.3 || ^6.2", + "keradus/cli-executor": "^2.1", + "mikey179/vfsstream": "^1.6.12", + "php-coveralls/php-coveralls": "^2.7", + "php-cs-fixer/accessible-object": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", + "phpunit/phpunit": "^9.6.22 || ^10.5.45 || ^11.5.12", + "symfony/var-dumper": "^5.4.48 || ^6.4.18 || ^7.2.3", + "symfony/yaml": "^5.4.45 || ^6.4.18 || ^7.2.3" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "exclude-from-classmap": [ + "src/Fixer/Internal/*" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.75.0" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2025-03-31T18:40:42+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", + "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", + "shasum": "" + }, + "require": { + "php": "^5.3|^7.0|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1" + }, + "time": "2020-07-09T08:09:16+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", "email": "dave.marshall@atstsolutions.co.uk", "homepage": "https://davedevelopment.co.uk", "role": "Developer" @@ -1853,589 +2355,694 @@ "time": "2024-09-19T10:50:18+00:00" }, { - "name": "sebastian/cli-parser", - "version": "1.0.2", + "name": "psr/container", + "version": "1.1.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + "url": "https://github.com/php-fig/container.git", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" + "php": ">=7.4.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Psr\\Container\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], "support": { - "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.2" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-02T06:27:43+00:00" + "time": "2021-11-05T16:50:12+00:00" }, { - "name": "sebastian/code-unit", - "version": "1.0.8", + "name": "psr/event-dispatcher", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" + "php": ">=7.2.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" } ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:08:54+00:00" + "time": "2019-01-08T18:20:26+00:00" }, { - "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", + "name": "psr/log", + "version": "1.1.4", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" + "php": ">=5.3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + "source": "https://github.com/php-fig/log/tree/1.1.4" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2021-05-03T11:20:27+00:00" }, { - "name": "sebastian/comparator", - "version": "4.0.8", + "name": "react/cache", + "version": "v1.2.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\Cache\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" }, { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" }, { - "name": "Volker Dusch", - "email": "github@wallbash.com" + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" }, { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", + "description": "Async, Promise-based cache interface for ReactPHP", "keywords": [ - "comparator", - "compare", - "equality" + "cache", + "caching", + "promise", + "reactphp" ], "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2022-11-30T15:59:55+00:00" }, { - "name": "sebastian/complexity", - "version": "2.0.3", + "name": "react/child-process", + "version": "v0.6.6", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + "url": "https://github.com/reactphp/child-process.git", + "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/1721e2b93d89b745664353b9cfc8f155ba8a6159", + "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.4" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/socket": "^1.16", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\ChildProcess\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Library for calculating the complexity of PHP code units", - "homepage": "https://github.com/sebastianbergmann/complexity", + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], "support": { - "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.6" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2023-12-22T06:19:30+00:00" + "time": "2025-01-01T16:37:48+00:00" }, { - "name": "sebastian/diff", - "version": "4.0.6", + "name": "react/dns", + "version": "v1.13.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + "url": "https://github.com/reactphp/dns.git", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" }, "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\Dns\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" }, { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", + "description": "Async DNS resolver for ReactPHP", "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" + "async", + "dns", + "dns-resolver", + "reactphp" ], "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.13.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2024-03-02T06:30:58+00:00" + "time": "2024-06-13T14:18:03+00:00" }, { - "name": "sebastian/environment", - "version": "5.1.5", + "name": "react/event-loop", + "version": "v1.5.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "suggest": { - "ext-posix": "*" + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\EventLoop\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", "keywords": [ - "Xdebug", - "environment", - "hhvm" + "asynchronous", + "event-loop" ], "support": { - "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2023-02-03T06:03:51+00:00" + "time": "2023-11-13T13:48:05+00:00" }, { - "name": "sebastian/exporter", - "version": "4.0.6", + "name": "react/promise", + "version": "v3.2.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" + "php": ">=7.1.0" }, "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" }, { - "name": "Volker Dusch", - "email": "github@wallbash.com" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" }, { - "name": "Adam Harvey", - "email": "aharvey@php.net" + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" }, { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "https://www.github.com/sebastianbergmann/exporter", + "description": "A lightweight implementation of CommonJS Promises/A for PHP", "keywords": [ - "export", - "exporter" + "promise", + "promises" ], "support": { - "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.2.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2024-05-24T10:39:05+00:00" }, { - "name": "sebastian/global-state", - "version": "5.0.7", + "name": "react/socket", + "version": "v1.16.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "url": "https://github.com/reactphp/socket.git", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" }, "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-uopz": "*" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "React\\Socket\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", "keywords": [ - "global state" + "Connection", + "Socket", + "async", + "reactphp", + "stream" ], "support": { - "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.16.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2024-03-02T06:35:11+00:00" + "time": "2024-07-26T10:38:09+00:00" }, { - "name": "sebastian/lines-of-code", - "version": "1.0.4", + "name": "react/stream", + "version": "v1.4.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -2463,11 +3070,11 @@ "role": "lead" } ], - "description": "Library for counting the lines of code in PHP source code", - "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { - "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" }, "funding": [ { @@ -2475,26 +3082,24 @@ "type": "github" } ], - "time": "2023-12-22T06:20:34+00:00" + "time": "2024-03-02T06:27:43+00:00" }, { - "name": "sebastian/object-enumerator", - "version": "4.0.4", + "name": "sebastian/code-unit", + "version": "1.0.8", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=7.3" }, "require-dev": { "phpunit/phpunit": "^9.3" @@ -2502,7 +3107,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "1.0-dev" } }, "autoload": { @@ -2517,14 +3122,15 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" }, "funding": [ { @@ -2532,20 +3138,20 @@ "type": "github" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2020-10-26T13:08:54+00:00" }, { - "name": "sebastian/object-reflector", - "version": "2.0.4", + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", "shasum": "" }, "require": { @@ -2575,11 +3181,11 @@ "email": "sebastian@phpunit.de" } ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", "support": { - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" }, "funding": [ { @@ -2587,24 +3193,26 @@ "type": "github" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2020-09-28T05:30:19+00:00" }, { - "name": "sebastian/recursion-context", - "version": "4.0.5", + "name": "sebastian/comparator", + "version": "4.0.8", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" }, "require-dev": { "phpunit/phpunit": "^9.3" @@ -2634,15 +3242,24 @@ "email": "whatthejeff@gmail.com" }, { - "name": "Adam Harvey", - "email": "aharvey@php.net" + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" } ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "https://github.com/sebastianbergmann/recursion-context", + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" }, "funding": [ { @@ -2650,32 +3267,33 @@ "type": "github" } ], - "time": "2023-02-03T06:07:39+00:00" + "time": "2022-09-14T12:41:17+00:00" }, { - "name": "sebastian/resource-operations", - "version": "3.0.4", + "name": "sebastian/complexity", + "version": "2.0.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", "shasum": "" }, "require": { + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -2690,13 +3308,15 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", "support": { - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" }, "funding": [ { @@ -2704,32 +3324,33 @@ "type": "github" } ], - "time": "2024-03-14T16:00:52+00:00" + "time": "2023-12-22T06:19:30+00:00" }, { - "name": "sebastian/type", - "version": "3.2.1", + "name": "sebastian/diff", + "version": "4.0.6", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", "shasum": "" }, "require": { "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -2744,15 +3365,24 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" } ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" }, "funding": [ { @@ -2760,29 +3390,35 @@ "type": "github" } ], - "time": "2023-02-03T06:13:03+00:00" + "time": "2024-03-02T06:30:58+00:00" }, { - "name": "sebastian/version", - "version": "3.0.2", + "name": "sebastian/environment", + "version": "5.1.5", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", "shasum": "" }, "require": { "php": ">=7.3" }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -2797,226 +3433,2139 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "email": "sebastian@phpunit.de" } ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" - }, - "funding": [ + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:33:00+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:35:11+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "sirbrillig/phpcs-variable-analysis", + "version": "v2.11.21", + "source": { + "type": "git", + "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", + "reference": "eb2b351927098c24860daa7484e290d3eed693be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/eb2b351927098c24860daa7484e290d3eed693be", + "reference": "eb2b351927098c24860daa7484e290d3eed693be", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "squizlabs/php_codesniffer": "^3.5.6" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", + "phpcsstandards/phpcsdevcs": "^1.1", + "phpstan/phpstan": "^1.7", + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0 || ^10.5.32 || ^11.3.3", + "sirbrillig/phpcs-import-detection": "^1.1", + "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0" + }, + "type": "phpcodesniffer-standard", + "autoload": { + "psr-4": { + "VariableAnalysis\\": "VariableAnalysis/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Sam Graham", + "email": "php-codesniffer-variableanalysis@illusori.co.uk" + }, + { + "name": "Payton Swick", + "email": "payton@foolord.com" + } + ], + "description": "A PHPCS sniff to detect problems with variables.", + "keywords": [ + "phpcs", + "static analysis" + ], + "support": { + "issues": "https://github.com/sirbrillig/phpcs-variable-analysis/issues", + "source": "https://github.com/sirbrillig/phpcs-variable-analysis", + "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" + }, + "time": "2024-12-02T16:37:49+00:00" + }, + { + "name": "slevomat/coding-standard", + "version": "8.15.0", + "source": { + "type": "git", + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "7d1d957421618a3803b593ec31ace470177d7817" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/7d1d957421618a3803b593ec31ace470177d7817", + "reference": "7d1d957421618a3803b593ec31ace470177d7817", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", + "php": "^7.2 || ^8.0", + "phpstan/phpdoc-parser": "^1.23.1", + "squizlabs/php_codesniffer": "^3.9.0" + }, + "require-dev": { + "phing/phing": "2.17.4", + "php-parallel-lint/php-parallel-lint": "1.3.2", + "phpstan/phpstan": "1.10.60", + "phpstan/phpstan-deprecation-rules": "1.1.4", + "phpstan/phpstan-phpunit": "1.3.16", + "phpstan/phpstan-strict-rules": "1.5.2", + "phpunit/phpunit": "8.5.21|9.6.8|10.5.11" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" + ], + "support": { + "issues": "https://github.com/slevomat/coding-standard/issues", + "source": "https://github.com/slevomat/coding-standard/tree/8.15.0" + }, + "funding": [ + { + "url": "https://github.com/kukulich", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", + "type": "tidelift" + } + ], + "time": "2024-03-09T15:20:58+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.11.1", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "19473c30efe4f7b3cd42522d0b2e6e7f243c6f87" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/19473c30efe4f7b3cd42522d0b2e6e7f243c6f87", + "reference": "19473c30efe4f7b3cd42522d0b2e6e7f243c6f87", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2024-11-16T12:02:36+00:00" + }, + { + "name": "symfony/console", + "version": "v5.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.1|^6.0" + }, + "conflict": { + "psr/log": ">=3", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0" + }, + "require-dev": { + "psr/log": "^1|^2", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/lock": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v5.4.47" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-06T11:30:55+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "2.5-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v5.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "72982eb416f61003e9bb6e91f8b3213600dcf9e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/72982eb416f61003e9bb6e91f8b3213600dcf9e9", + "reference": "72982eb416f61003e9bb6e91f8b3213600dcf9e9", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/event-dispatcher-contracts": "^2|^3", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "symfony/dependency-injection": "<4.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/http-foundation": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/stopwatch": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.45" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v2.5.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f", + "reference": "e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/event-dispatcher": "^1" + }, + "suggest": { + "symfony/event-dispatcher-implementation": "" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "2.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v5.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "57c8294ed37d4a055b77057827c67f9558c95c54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/57c8294ed37d4a055b77057827c67f9558c95c54", + "reference": "57c8294ed37d4a055b77057827c67f9558c95c54", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "symfony/process": "^5.4|^6.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v5.4.45" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-22T13:05:35+00:00" + }, + { + "name": "symfony/finder", + "version": "v5.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "63741784cd7b9967975eec610b256eed3ede022b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/63741784cd7b9967975eec610b256eed3ede022b", + "reference": "63741784cd7b9967975eec610b256eed3ede022b", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v5.4.45" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-28T13:32:08+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v5.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "74e5b6f0db3e8589e6cfd5efb317a1fc2bb52fb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/74e5b6f0db3e8589e6cfd5efb317a1fc2bb52fb6", + "reference": "74e5b6f0db3e8589e6cfd5efb317a1fc2bb52fb6", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php73": "~1.0", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v5.4.45" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "url": "https://github.com/sebastianbergmann", + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { - "name": "sirbrillig/phpcs-variable-analysis", - "version": "v2.11.21", + "name": "symfony/polyfill-php81", + "version": "v1.32.0", "source": { "type": "git", - "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", - "reference": "eb2b351927098c24860daa7484e290d3eed693be" + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/eb2b351927098c24860daa7484e290d3eed693be", - "reference": "eb2b351927098c24860daa7484e290d3eed693be", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", "shasum": "" }, "require": { - "php": ">=5.4.0", - "squizlabs/php_codesniffer": "^3.5.6" + "php": ">=7.2" }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", - "phpcsstandards/phpcsdevcs": "^1.1", - "phpstan/phpstan": "^1.7", - "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0 || ^10.5.32 || ^11.3.3", - "sirbrillig/phpcs-import-detection": "^1.1", - "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0" + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } }, - "type": "phpcodesniffer-standard", "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "VariableAnalysis\\": "VariableAnalysis/" - } + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-2-Clause" + "MIT" ], "authors": [ { - "name": "Sam Graham", - "email": "php-codesniffer-variableanalysis@illusori.co.uk" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Payton Swick", - "email": "payton@foolord.com" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "A PHPCS sniff to detect problems with variables.", + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", "keywords": [ - "phpcs", - "static analysis" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/sirbrillig/phpcs-variable-analysis/issues", - "source": "https://github.com/sirbrillig/phpcs-variable-analysis", - "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0" }, - "time": "2024-12-02T16:37:49+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "slevomat/coding-standard", - "version": "8.15.0", + "name": "symfony/process", + "version": "v5.4.47", "source": { "type": "git", - "url": "https://github.com/slevomat/coding-standard.git", - "reference": "7d1d957421618a3803b593ec31ace470177d7817" + "url": "https://github.com/symfony/process.git", + "reference": "5d1662fb32ebc94f17ddb8d635454a776066733d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/7d1d957421618a3803b593ec31ace470177d7817", - "reference": "7d1d957421618a3803b593ec31ace470177d7817", + "url": "https://api.github.com/repos/symfony/process/zipball/5d1662fb32ebc94f17ddb8d635454a776066733d", + "reference": "5d1662fb32ebc94f17ddb8d635454a776066733d", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", - "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": "^1.23.1", - "squizlabs/php_codesniffer": "^3.9.0" + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" }, - "require-dev": { - "phing/phing": "2.17.4", - "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.10.60", - "phpstan/phpstan-deprecation-rules": "1.1.4", - "phpstan/phpstan-phpunit": "1.3.16", - "phpstan/phpstan-strict-rules": "1.5.2", - "phpunit/phpunit": "8.5.21|9.6.8|10.5.11" + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, - "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v5.4.47" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-06T11:36:42+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.5.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f37b419f7aea2e9abf10abd261832cace12e3300", + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { - "dev-master": "8.x-dev" + "dev-main": "2.5-dev" } }, "autoload": { "psr-4": { - "SlevomatCodingStandard\\": "SlevomatCodingStandard/" + "Symfony\\Contracts\\Service\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", "keywords": [ - "dev", - "phpcs" + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" ], "support": { - "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.15.0" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.4" }, "funding": [ { - "url": "https://github.com/kukulich", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-03-09T15:20:58+00:00" + "time": "2024-09-25T14:11:13+00:00" }, { - "name": "squizlabs/php_codesniffer", - "version": "3.11.1", + "name": "symfony/stopwatch", + "version": "v5.4.45", "source": { "type": "git", - "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "19473c30efe4f7b3cd42522d0b2e6e7f243c6f87" + "url": "https://github.com/symfony/stopwatch.git", + "reference": "fb2c199cf302eb207f8c23e7ee174c1c31a5c004" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/19473c30efe4f7b3cd42522d0b2e6e7f243c6f87", - "reference": "19473c30efe4f7b3cd42522d0b2e6e7f243c6f87", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/fb2c199cf302eb207f8c23e7ee174c1c31a5c004", + "reference": "fb2c199cf302eb207f8c23e7ee174c1c31a5c004", "shasum": "" }, "require": { - "ext-simplexml": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + "php": ">=7.2.5", + "symfony/service-contracts": "^1|^2|^3" }, - "bin": [ - "bin/phpcbf", - "bin/phpcs" - ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Greg Sherwood", - "role": "Former lead" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { - "name": "Juliette Reinders Folmer", - "role": "Current lead" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v5.4.45" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" }, { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "time": "2024-09-25T14:11:13+00:00" + }, + { + "name": "symfony/string", + "version": "v5.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/136ca7d72f72b599f2631aca474a4f8e26719799", + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" + }, + "conflict": { + "symfony/translation-contracts": ">=3.0" + }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0|^6.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", "keywords": [ - "phpcs", - "standards", - "static analysis" + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" ], "support": { - "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", - "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", - "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", - "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + "source": "https://github.com/symfony/string/tree/v5.4.47" }, "funding": [ { - "url": "https://github.com/PHPCSStandards", - "type": "github" + "url": "https://symfony.com/sponsor", + "type": "custom" }, { - "url": "https://github.com/jrfnl", + "url": "https://github.com/fabpot", "type": "github" }, { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2024-11-16T12:02:36+00:00" + "time": "2024-11-10T20:33:58+00:00" }, { "name": "szepeviktor/phpstan-wordpress", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index b86767ed8..d89273320 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -16,3 +16,6 @@ parameters: - '#Call to an undefined method Progress_Planner\\Base\:\:get_[a-zA-Z0-9\\_]+\(\).#' - '#Cannot call method modify\(\) on DateTime\|false.#' - '#Cannot call method format\(\) on DateTime\|false.#' + - + identifier: variable.undefined + path: views/popovers/email-sending.php diff --git a/progress-planner.php b/progress-planner.php index f1066f331..55cb0c8c1 100644 --- a/progress-planner.php +++ b/progress-planner.php @@ -7,9 +7,9 @@ * Plugin name: Progress Planner * Plugin URI: https://prpl.fyi/home * Description: A plugin to help you fight procrastination and get things done. - * Requires at least: 6.3 + * Requires at least: 6.6 * Requires PHP: 7.4 - * Version: 1.5.0 + * Version: 1.6.0 * Author: Team Emilia Projects * Author URI: https://prpl.fyi/about * License: GPL-3.0+ @@ -18,13 +18,13 @@ */ // Exit if accessed directly. -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } -define( 'PROGRESS_PLANNER_FILE', __FILE__ ); -define( 'PROGRESS_PLANNER_DIR', __DIR__ ); -define( 'PROGRESS_PLANNER_URL', untrailingslashit( plugin_dir_url( __FILE__ ) ) ); +\define( 'PROGRESS_PLANNER_FILE', __FILE__ ); +\define( 'PROGRESS_PLANNER_DIR', __DIR__ ); +\define( 'PROGRESS_PLANNER_URL', \untrailingslashit( \plugin_dir_url( __FILE__ ) ) ); require_once PROGRESS_PLANNER_DIR . '/autoload.php'; @@ -42,4 +42,4 @@ function progress_planner() { return $progress_planner; } -progress_planner(); +\progress_planner(); diff --git a/readme.txt b/readme.txt index f3bf962b3..993fdcdea 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: planning, maintenance, writing, blogging Requires at least: 6.3 Tested up to: 6.8 Requires PHP: 7.4 -Stable tag: 1.5.0 +Stable tag: 1.6.0 License: GPL3+ License URI: https://www.gnu.org/licenses/gpl-3.0.en.html @@ -110,6 +110,17 @@ https://youtu.be/e1bmxZYyXFY == Changelog == += 1.6.0 = + +Enhancements: + +* Allow users to collect extra points for previous months' badges. +* Added WP-CLI commands to manage recommendations. + +Under the hood: + +* Ravi's Recommendations are now a custom post type. + = 1.5.0 = Added these recommendations from Ravi: diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 3343d02e6..9f31e0ef1 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -5,19 +5,19 @@ * @package Progress_Planner\Tests */ -$_tests_dir = getenv( 'WP_TESTS_DIR' ); +$_tests_dir = \getenv( 'WP_TESTS_DIR' ); if ( ! $_tests_dir ) { - $_tests_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib'; + $_tests_dir = \rtrim( \sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib'; } // Forward custom PHPUnit Polyfills configuration to PHPUnit bootstrap file. -$_phpunit_polyfills_path = getenv( 'WP_TESTS_PHPUNIT_POLYFILLS_PATH' ); +$_phpunit_polyfills_path = \getenv( 'WP_TESTS_PHPUNIT_POLYFILLS_PATH' ); if ( false !== $_phpunit_polyfills_path ) { - define( 'WP_TESTS_PHPUNIT_POLYFILLS_PATH', $_phpunit_polyfills_path ); + \define( 'WP_TESTS_PHPUNIT_POLYFILLS_PATH', $_phpunit_polyfills_path ); } -if ( ! file_exists( "{$_tests_dir}/includes/functions.php" ) ) { +if ( ! \file_exists( "{$_tests_dir}/includes/functions.php" ) ) { echo "Could not find {$_tests_dir}/includes/functions.php, have you run bin/install-wp-tests.sh ?" . PHP_EOL; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped exit( 1 ); } @@ -28,16 +28,16 @@ /** * Load PHPUnit Polyfills. */ -require_once dirname( __DIR__ ) . '/vendor/yoast/phpunit-polyfills/phpunitpolyfills-autoload.php'; +require_once \dirname( __DIR__ ) . '/vendor/yoast/phpunit-polyfills/phpunitpolyfills-autoload.php'; /** * Manually load the plugin being tested. */ function _manually_load_plugin() { - require dirname( __DIR__ ) . '/progress-planner.php'; + require \dirname( __DIR__ ) . '/progress-planner.php'; } -tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); +\tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); // Start up the WP testing environment. require "{$_tests_dir}/includes/bootstrap.php"; diff --git a/tests/e2e/constants/selectors.js b/tests/e2e/constants/selectors.js index 4e10d411d..64dad991f 100644 --- a/tests/e2e/constants/selectors.js +++ b/tests/e2e/constants/selectors.js @@ -4,8 +4,8 @@ const SELECTORS = { RR_ITEM_TEXT: 'h3 > span', - TODO_ITEM: 'ul#todo-list > prpl-suggested-task li', - TODO_COMPLETED_ITEM: 'ul#todo-list-completed > prpl-suggested-task li', + TODO_ITEM: 'ul#todo-list > li', + TODO_COMPLETED_ITEM: 'ul#todo-list-completed > li', TODO_LIST: 'ul#todo-list', TODO_LIST_COMPLETED: 'ul#todo-list-completed', }; diff --git a/tests/e2e/helpers/cleanup.js b/tests/e2e/helpers/cleanup.js new file mode 100644 index 000000000..9492c4f03 --- /dev/null +++ b/tests/e2e/helpers/cleanup.js @@ -0,0 +1,94 @@ +const SELECTORS = require( '../constants/selectors' ); + +/** + * Cleans up all active and completed tasks in the planner UI. + * Requires a Playwright `page`, `context`, and `baseUrl`. + * + * @param {Object} root0 + * @param {import('@playwright/test').Page} root0.page + * @param {import('@playwright/test').BrowserContext} root0.context + * @param {string} root0.baseUrl + * @return {Promise<void>} + */ +async function cleanUpPlannerTasks( { page, context, baseUrl } ) { + try { + if ( page.isClosed?.() ) return; + + await page.goto( + `${ baseUrl }/wp-admin/admin.php?page=progress-planner` + ); + await page.waitForLoadState( 'networkidle' ); + + // Clean up ACTIVE tasks + const todoItems = page.locator( SELECTORS.TODO_ITEM ); + while ( ( await todoItems.count() ) > 0 ) { + const firstItem = todoItems.first(); + const trash = firstItem.locator( '.trash' ); + + try { + console.log( + 'deleting TODO: ', + await firstItem.locator( 'h3 > span' ).textContent() + ); + await firstItem.scrollIntoViewIfNeeded(); + await firstItem.hover(); + await trash.waitFor( { state: 'visible', timeout: 3000 } ); + await trash.click(); + await page.waitForTimeout( 1500 ); + } catch ( err ) { + console.warn( + '[Cleanup] Failed to delete active todo item:', + err.message + ); + break; + } + } + + // Clean up COMPLETED tasks + const completedDetails = page.locator( + 'details#todo-list-completed-details' + ); + if ( await completedDetails.isVisible() ) { + await completedDetails.click(); + await page.waitForTimeout( 500 ); + + const completedItems = page.locator( + SELECTORS.TODO_COMPLETED_ITEM + ); + while ( ( await completedItems.count() ) > 0 ) { + const firstCompleted = completedItems.first(); + const trash = firstCompleted.locator( '.trash' ); + + try { + console.log( + 'deleting completed TODO: ', + await firstCompleted + .locator( 'h3 > span' ) + .textContent() + ); + await firstCompleted.scrollIntoViewIfNeeded(); + await firstCompleted.hover(); + await trash.waitFor( { state: 'visible', timeout: 3000 } ); + await trash.click(); + await page.waitForTimeout( 1500 ); + } catch ( err ) { + console.warn( + '[Cleanup] Failed to delete completed todo item:', + err.message + ); + break; + } + } + } + } catch ( e ) { + console.warn( '[Cleanup] Unexpected failure:', e.message ); + } + + try { + await context.close(); + } catch { + // context might already be closed + } +} + +module.exports = { cleanUpPlannerTasks }; diff --git a/tests/e2e/sequential/task-tagline.spec.js b/tests/e2e/sequential/task-tagline.spec.js index 339be9978..9504f80e3 100644 --- a/tests/e2e/sequential/task-tagline.spec.js +++ b/tests/e2e/sequential/task-tagline.spec.js @@ -25,7 +25,7 @@ function taglineTests( testContext = test ) { ( task ) => task.task_id === 'core-blogdescription' ); expect( blogDescriptionTask ).toBeDefined(); - expect( blogDescriptionTask.status ).toBe( 'pending' ); + expect( blogDescriptionTask.post_status ).toBe( 'publish' ); // Navigate to WordPress settings await page.goto( @@ -59,7 +59,7 @@ function taglineTests( testContext = test ) { ( task ) => task.task_id === 'core-blogdescription' ); expect( updatedTask ).toBeDefined(); - expect( updatedTask.status ).toBe( 'pending_celebration' ); + expect( updatedTask.post_status ).toBe( 'pending' ); // Go to Progress Planner dashboard await page.goto( @@ -104,7 +104,7 @@ function taglineTests( testContext = test ) { ( task ) => task.task_id === 'core-blogdescription' ); expect( completedTask ).toBeDefined(); - expect( completedTask.status ).toBe( 'completed' ); + expect( completedTask.post_status ).toBe( 'trash' ); } ); } ); diff --git a/tests/e2e/sequential/todo-complete.spec.js b/tests/e2e/sequential/todo-complete.spec.js index dd13c86ea..122c63991 100644 --- a/tests/e2e/sequential/todo-complete.spec.js +++ b/tests/e2e/sequential/todo-complete.spec.js @@ -1,5 +1,6 @@ const { test, expect, chromium } = require( '@playwright/test' ); const SELECTORS = require( '../constants/selectors' ); +const { cleanUpPlannerTasks } = require( '../helpers/cleanup' ); const TEST_TASK_TEXT = 'Task to be completed'; @@ -20,53 +21,11 @@ function todoCompleteTests( testContext = test ) { } ); testContext.afterEach( async () => { - // Clean up any remaining tasks - await page.goto( - `${ process.env.WORDPRESS_URL }/wp-admin/admin.php?page=progress-planner` - ); - await page.waitForLoadState( 'networkidle' ); - - // Clean up active tasks - const activeTodoItems = page.locator( SELECTORS.TODO_ITEM ); - - while ( ( await activeTodoItems.count() ) > 0 ) { - const firstItem = activeTodoItems.first(); - await firstItem.hover(); - await page.waitForTimeout( 500 ); - await firstItem.waitFor( { state: 'visible' } ); - await firstItem.locator( '.trash' ).click(); - await page.waitForTimeout( 500 ); - } - - // Clean up completed tasks if the section exists - const completedDetails = page.locator( - 'details#todo-list-completed-details' - ); - - if ( await completedDetails.isVisible() ) { - await completedDetails.click(); - await page.waitForTimeout( 500 ); - - const completedTodoItems = page.locator( - SELECTORS.TODO_COMPLETED_ITEM - ); - - while ( ( await completedTodoItems.count() ) > 0 ) { - const firstItem = completedTodoItems.first(); - await firstItem.hover(); - await page.waitForTimeout( 500 ); - await firstItem.waitFor( { state: 'visible' } ); - await firstItem.locator( '.trash' ).click(); - await page.waitForTimeout( 500 ); - } - } - - // Safely close context if it's still open - try { - await context.close(); - } catch ( error ) { - // Ignore errors if context is already closed - } + await cleanUpPlannerTasks( { + page, + context, + baseUrl: process.env.WORDPRESS_URL, + } ); } ); testContext.afterAll( async () => { @@ -82,7 +41,7 @@ function todoCompleteTests( testContext = test ) { await page.fill( '#new-todo-content', TEST_TASK_TEXT ); await page.keyboard.press( 'Enter' ); - await page.waitForTimeout( 500 ); + await page.waitForTimeout( 1500 ); // Get the task selector const todoItem = page.locator( SELECTORS.TODO_ITEM ); @@ -93,9 +52,7 @@ function todoCompleteTests( testContext = test ) { const todoItemElement = page.locator( `${ SELECTORS.TODO_LIST } ${ taskSelector }` ); - await todoItemElement - .locator( '.prpl-suggested-task-checkbox' ) - .click(); + await todoItemElement.locator( 'label' ).click(); await page.waitForTimeout( 1000 ); // Verify task is not in active list @@ -111,6 +68,9 @@ function todoCompleteTests( testContext = test ) { `${ SELECTORS.TODO_LIST_COMPLETED } ${ taskSelector }` ); await expect( completedTask ).toBeVisible(); + await expect( completedTask.locator( 'h3 > span' ) ).toHaveText( + TEST_TASK_TEXT + ); await expect( completedTask.locator( SELECTORS.RR_ITEM_TEXT ) ).toHaveText( TEST_TASK_TEXT ); @@ -131,7 +91,7 @@ function todoCompleteTests( testContext = test ) { // Create a new task await page.fill( '#new-todo-content', TEST_TASK_TEXT ); await page.keyboard.press( 'Enter' ); - await page.waitForTimeout( 500 ); + await page.waitForTimeout( 1500 ); // Get the task selector const todoItem = page.locator( SELECTORS.TODO_ITEM ); @@ -142,10 +102,8 @@ function todoCompleteTests( testContext = test ) { const todoItemElement = page.locator( `${ SELECTORS.TODO_LIST } ${ taskSelector }` ); - await todoItemElement - .locator( '.prpl-suggested-task-checkbox' ) - .click(); - await page.waitForTimeout( 1000 ); + await todoItemElement.locator( 'label' ).click(); + await page.waitForTimeout( 1500 ); // Verify task is not in active list await expect( @@ -162,6 +120,9 @@ function todoCompleteTests( testContext = test ) { `${ SELECTORS.TODO_LIST_COMPLETED } ${ taskSelector }` ); await expect( completedTask ).toBeVisible(); + await expect( completedTask.locator( 'h3 > span' ) ).toHaveText( + TEST_TASK_TEXT + ); await expect( completedTask.locator( SELECTORS.RR_ITEM_TEXT ) ).toHaveText( TEST_TASK_TEXT ); diff --git a/tests/e2e/sequential/todo-reorder.spec.js b/tests/e2e/sequential/todo-reorder.spec.js index 6340ca2b6..2495555f8 100644 --- a/tests/e2e/sequential/todo-reorder.spec.js +++ b/tests/e2e/sequential/todo-reorder.spec.js @@ -1,5 +1,6 @@ const { test, expect, chromium } = require( '@playwright/test' ); const SELECTORS = require( '../constants/selectors' ); +const { cleanUpPlannerTasks } = require( '../helpers/cleanup' ); const FIRST_TASK_TEXT = 'First task to reorder'; const SECOND_TASK_TEXT = 'Second task to reorder'; @@ -21,30 +22,11 @@ function todoReorderTests( testContext = test ) { } ); testContext.afterEach( async () => { - // Clean up any remaining tasks - await page.goto( - `${ process.env.WORDPRESS_URL }/wp-admin/admin.php?page=progress-planner` - ); - await page.waitForLoadState( 'networkidle' ); - - // Clean up active tasks - const activeTodoItems = page.locator( SELECTORS.TODO_ITEM ); - - while ( ( await activeTodoItems.count() ) > 0 ) { - const firstItem = activeTodoItems.first(); - await firstItem.hover(); - await page.waitForTimeout( 500 ); - await firstItem.waitFor( { state: 'visible' } ); - await firstItem.locator( '.trash' ).click(); - await page.waitForTimeout( 500 ); - } - - // Safely close context if it's still open - try { - await context.close(); - } catch ( error ) { - // Ignore errors if context is already closed - } + await cleanUpPlannerTasks( { + page, + context, + baseUrl: process.env.WORDPRESS_URL, + } ); } ); testContext.afterAll( async () => { @@ -61,17 +43,17 @@ function todoReorderTests( testContext = test ) { // Create first task await page.fill( '#new-todo-content', FIRST_TASK_TEXT ); await page.keyboard.press( 'Enter' ); - await page.waitForTimeout( 500 ); + await page.waitForTimeout( 1500 ); // Create second task await page.fill( '#new-todo-content', SECOND_TASK_TEXT ); await page.keyboard.press( 'Enter' ); - await page.waitForTimeout( 500 ); + await page.waitForTimeout( 1500 ); // Create third task await page.fill( '#new-todo-content', THIRD_TASK_TEXT ); await page.keyboard.press( 'Enter' ); - await page.waitForTimeout( 500 ); + await page.waitForTimeout( 1500 ); // Get all todo items const todoItems = page.locator( SELECTORS.TODO_ITEM ); @@ -93,7 +75,7 @@ function todoReorderTests( testContext = test ) { await items[ 1 ] .locator( '.prpl-suggested-task-button.move-down' ) .click(); - await page.waitForTimeout( 500 ); + await page.waitForTimeout( 1500 ); // Verify new order const reorderedItems = await todoItems.all(); diff --git a/tests/e2e/sequential/todo.spec.js b/tests/e2e/sequential/todo.spec.js index 7e8fb4383..874b9951d 100644 --- a/tests/e2e/sequential/todo.spec.js +++ b/tests/e2e/sequential/todo.spec.js @@ -1,5 +1,6 @@ const { test, expect, chromium } = require( '@playwright/test' ); const SELECTORS = require( '../constants/selectors' ); +const { cleanUpPlannerTasks } = require( '../helpers/cleanup' ); const CREATE_TASK_TEXT = 'Test task to create'; const DELETE_TASK_TEXT = 'Test task to delete'; @@ -20,53 +21,11 @@ function todoTests( testContext = test ) { } ); testContext.afterEach( async () => { - // Clean up any remaining tasks - await page.goto( - `${ process.env.WORDPRESS_URL }/wp-admin/admin.php?page=progress-planner` - ); - await page.waitForLoadState( 'networkidle' ); - - // Clean up active tasks - const activeTodoItems = page.locator( SELECTORS.TODO_ITEM ); - - while ( ( await activeTodoItems.count() ) > 0 ) { - const firstItem = activeTodoItems.first(); - await firstItem.hover(); - await page.waitForTimeout( 500 ); - await firstItem.waitFor( { state: 'visible' } ); - await firstItem.locator( '.trash' ).click(); - await page.waitForTimeout( 500 ); - } - - // Clean up completed tasks if the section exists - const completedDetails = page.locator( - 'details#todo-list-completed-details' - ); - - if ( await completedDetails.isVisible() ) { - await completedDetails.click(); - await page.waitForTimeout( 500 ); - - const completedTodoItems = page.locator( - SELECTORS.TODO_COMPLETED_ITEM - ); - - while ( ( await completedTodoItems.count() ) > 0 ) { - const firstItem = completedTodoItems.first(); - await firstItem.hover(); - await page.waitForTimeout( 500 ); - await firstItem.waitFor( { state: 'visible' } ); - await firstItem.locator( '.trash' ).click(); - await page.waitForTimeout( 500 ); - } - } - - // Safely close context if it's still open - try { - await context.close(); - } catch ( error ) { - // Ignore errors if context is already closed - } + await cleanUpPlannerTasks( { + page, + context, + baseUrl: process.env.WORDPRESS_URL, + } ); } ); testContext.afterAll( async () => { @@ -110,7 +69,7 @@ function todoTests( testContext = test ) { await deleteItem.hover(); await deleteItem.waitFor( { state: 'visible' } ); await deleteItem.locator( '.trash' ).click(); - await page.waitForTimeout( 500 ); + await page.waitForTimeout( 1500 ); // Verify the todo was deleted const todoItem = page.locator( SELECTORS.TODO_ITEM ); diff --git a/tests/e2e/task-dismissible.spec.js b/tests/e2e/task-dismissible.spec.js index 458b746c2..0c9f38a0f 100644 --- a/tests/e2e/task-dismissible.spec.js +++ b/tests/e2e/task-dismissible.spec.js @@ -11,29 +11,26 @@ test.describe( 'PRPL Dismissable Tasks', () => { await page.waitForLoadState( 'networkidle' ); // Check if complete button exists - const completeButton = page - .locator( - 'button.prpl-suggested-task-button[data-action="complete"]' - ) - .first(); const initialCount = await page .locator( - 'button.prpl-suggested-task-button[data-action="complete"]' + '#prpl-suggested-tasks-list .prpl-suggested-task-checkbox:not(:disabled)' ) .count(); if ( initialCount > 0 ) { - // Get the task ID from the button - const taskId = await completeButton.getAttribute( 'data-task-id' ); + const completeButton = page + .locator( + '#prpl-suggested-tasks-list .prpl-suggested-task-checkbox:not(:disabled)' + ) + .first(); - // Hover over the task to show actions - const taskElement = page.locator( - `li[data-task-id="${ taskId }"]` - ); - await taskElement.hover(); + // Get the task ID from the button + const taskId = await completeButton + .locator( 'xpath=ancestor::li[1]' ) // .closest("li"), but playwright doesn't support it + .getAttribute( 'data-task-id' ); - // Click the complete button - await completeButton.click(); + // Click the on the parent of the checkbox (label, because it intercepts pointer events) + await completeButton.locator( '..' ).click(); // parent(), but playwright doesn't support it // Wait for animation await page.waitForTimeout( 3000 ); @@ -41,7 +38,7 @@ test.describe( 'PRPL Dismissable Tasks', () => { // Verify the task count decreased by 1 const finalCount = await page .locator( - 'button.prpl-suggested-task-button[data-action="complete"]' + '#prpl-suggested-tasks-list .prpl-suggested-task-checkbox:not(:disabled)' ) .count(); expect( finalCount ).toBe( initialCount - 1 ); @@ -59,7 +56,7 @@ test.describe( 'PRPL Dismissable Tasks', () => { ( task ) => task.task_id === taskId ); expect( completedTask ).toBeDefined(); - expect( completedTask.status ).toBe( 'completed' ); + expect( completedTask.post_status ).toBe( 'trash' ); } } ); } ); diff --git a/tests/e2e/task-snooze.spec.js b/tests/e2e/task-snooze.spec.js index 34e86983d..df787abf0 100644 --- a/tests/e2e/task-snooze.spec.js +++ b/tests/e2e/task-snooze.spec.js @@ -69,7 +69,7 @@ test.describe( 'PRPL Task Snooze', () => { const updatedTask = updatedTasks.find( ( task ) => task.task_id === taskToSnooze.task_id ); - expect( updatedTask.status ).toBe( 'snoozed' ); + expect( updatedTask.post_status ).toBe( 'future' ); } } ); } ); diff --git a/tests/phpunit/class-task-provider-test-trait.php b/tests/phpunit/class-task-provider-test-trait.php index f25adba99..8a724e7e8 100644 --- a/tests/phpunit/class-task-provider-test-trait.php +++ b/tests/phpunit/class-task-provider-test-trait.php @@ -7,8 +7,6 @@ namespace Progress_Planner\Tests; -use Progress_Planner\Suggested_Tasks; - /** * Task provider test case. */ @@ -21,13 +19,6 @@ trait Task_Provider_Test_Trait { */ protected $task_provider; - /** - * The suggested tasks instance. - * - * @var Suggested_Tasks - */ - protected $suggested_tasks; - /** * Setup the test. * @@ -35,7 +26,7 @@ trait Task_Provider_Test_Trait { */ public static function setUpBeforeClass(): void { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid // Set the current user to the admin user. - wp_set_current_user( 1 ); + \wp_set_current_user( 1 ); } /** @@ -45,7 +36,7 @@ public static function setUpBeforeClass(): void { // phpcs:ignore WordPress.Nami */ public static function tearDownAfterClass(): void { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid // Reset the current user. - wp_set_current_user( 0 ); + \wp_set_current_user( 0 ); } /** @@ -58,9 +49,6 @@ public function set_up() { // Get the task provider. $this->task_provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( $this->task_provider_id ); - - // Get the suggested tasks instance. - $this->suggested_tasks = \progress_planner()->get_suggested_tasks(); } /** @@ -72,7 +60,7 @@ public function tear_down() { parent::tear_down(); // Delete tasks. - \progress_planner()->get_settings()->set( 'tasks', [] ); + \progress_planner()->get_suggested_tasks_db()->delete_all_recommendations(); } /** @@ -88,65 +76,44 @@ abstract protected function complete_task(); * @return void */ public function test_task_provider() { - // Test that the blog description is empty. $this->assertTrue( $this->task_provider->should_add_task() ); - // Get all tasks to inject. + // WIP, get_tasks_to_inject() is injecting tasks. $tasks = $this->task_provider->get_tasks_to_inject(); - // Add the task(s) to the suggested tasks. - foreach ( $tasks as $task ) { - $this->suggested_tasks->get_tasks_manager()->add_pending_task( $task ); - } - // Verify that the task(s) are in the suggested tasks. - $pending_tasks = (array) \progress_planner()->get_settings()->get( 'tasks', [] ); - foreach ( $tasks as $task ) { - $item_found = false; - foreach ( $pending_tasks as $pending_task ) { - if ( $pending_task['task_id'] === $task['task_id'] ) { - $item_found = true; - break; - } - } - $this->assertTrue( $item_found ); - } + $pending_tasks = (array) \progress_planner()->get_suggested_tasks_db()->get_tasks_by( + [ + 'post_status' => 'publish', + 'provider' => $this->task_provider_id, + ] + ); + + // Assert that task is in the pending tasks. + $this->assertTrue( \has_term( $this->task_provider_id, 'prpl_recommendations_provider', $pending_tasks[0]->ID ) ); // Complete the task. $this->complete_task(); // Change the task status to pending celebration for all completed tasks. - foreach ( $this->suggested_tasks->get_tasks_manager()->evaluate_tasks() as $task ) { + foreach ( \progress_planner()->get_suggested_tasks()->get_tasks_manager()->evaluate_tasks() as $task ) { // Change the task status to pending celebration. - $this->suggested_tasks->mark_task_as( 'pending_celebration', $task->get_data()['task_id'] ); - - // In production we insert an activity here. - } - - // Verify that the task(s) we're testing is pending celebration. - foreach ( $tasks as $task ) { - $this->assertTrue( - $this->suggested_tasks->check_task_condition( - [ - 'status' => 'pending_celebration', - 'task_id' => $task['task_id'], - ] - ) + \progress_planner()->get_suggested_tasks_db()->update_recommendation( + $task->get_data()['ID'], + [ 'post_status' => 'pending' ] ); + // Verify that the task(s) we're testing is pending celebration. + $this->assertTrue( 'pending' === \get_post_status( $task->get_data()['ID'] ) ); } // Verify that the task(s) we're testing is completed. - foreach ( $tasks as $task ) { - $this->suggested_tasks->transition_task_status( $task['task_id'], 'pending_celebration', 'completed' ); - $this->assertTrue( - $this->suggested_tasks->check_task_condition( - [ - 'status' => 'completed', - 'task_id' => $task['task_id'], - ] - ) + foreach ( $tasks as $post_id ) { + \progress_planner()->get_suggested_tasks_db()->update_recommendation( + $post_id, + [ 'post_status' => 'trash' ] ); + $this->assertTrue( 'trash' === \get_post_status( $post_id ) ); } } } diff --git a/tests/phpunit/test-class-api-get-stats.php b/tests/phpunit/test-class-api-get-stats.php index a616cfa23..b6c630262 100644 --- a/tests/phpunit/test-class-api-get-stats.php +++ b/tests/phpunit/test-class-api-get-stats.php @@ -39,14 +39,12 @@ class Test_API_Get_Stats extends \WP_UnitTestCase { */ const REMOTE_API_RESPONSE = '[{"id":1619,"name":"Product page","settings":{"show_in_settings":"no","id":"product-page","title":"Product page","description":"Describes a product you sell"},"content_update_cycle":{"heading":"Content update cycle","update_cycle":"6 months","text":"<p>A {page_type} should be regularly updated. For this type of page, we suggest every {update_cycle}. We will remind you {update_cycle} after you’ve last saved this page.<\/p>\n","video":"","video_button_text":""}},{"id":1317,"name":"Blog post","settings":{"show_in_settings":"no","id":"blog","title":"Blog","description":"A blog post."},"content_update_cycle":{"heading":"Content update cycle","update_cycle":"6 months","text":"<p>A {page_type} should be regularly updated. For this type of page, we suggest updating them {update_cycle}. We will remind you {update_cycle} after you’ve last saved this page.<\/p>\n","video":"","video_button_text":""}},{"id":1316,"name":"FAQ page","settings":{"show_in_settings":"yes","id":"faq","title":"FAQ page","description":"Frequently Asked Questions."},"content_update_cycle":{"heading":"Content update cycle","update_cycle":"6 months","text":"<p>A {page_type} should be regularly updated. For this type of page, we suggest updating every {update_cycle}. We will remind you {update_cycle} after you’ve last saved this page.<\/p>\n","video":"","video_button_text":""}},{"id":1309,"name":"Contact page","settings":{"show_in_settings":"yes","id":"contact","title":"Contact","description":"Create an easy to use contact page."},"content_update_cycle":{"heading":"Content update cycle","update_cycle":"6 months","text":"<p>A {page_type} should be regularly updated. For this type of page, we suggest updating <strong>every {update_cycle}<\/strong>. We will remind you {update_cycle} after you’ve last saved this page.<\/p>\n","video":"","video_button_text":""}},{"id":1307,"name":"About page","settings":{"show_in_settings":"yes","id":"about","title":"About","description":"Who are you and why are you the person they need."},"content_update_cycle":{"heading":"Content update cycle","update_cycle":"6 months","text":"<p>A {page_type} should be regularly updated. For this type of page, we suggest updating every {update_cycle}. We will remind you {update_cycle} after you’ve last saved this page.<\/p>\n","video":"","video_button_text":""}},{"id":1269,"name":"Home page","settings":{"show_in_settings":"yes","id":"homepage","title":"Home page","description":"Describe your mission and much more."},"content_update_cycle":{"heading":"Content update cycle","update_cycle":"6 months","text":"<p>A {page_type} should be regularly updated. For this type of page, we suggest updating every {update_cycle}. We will remind you {update_cycle} after you’ve last saved this page.<\/p>\n","video":"","video_button_text":""}}]'; - /** * Run before the tests. * * @return void */ public static function setUpBeforeClass(): void { - self::set_lessons_cache(); \progress_planner()->get_page_types()->create_taxonomy(); @@ -81,7 +79,7 @@ public static function set_lessons_cache() { ) : \add_query_arg( [ 'site' => \get_site_url() ], $url ); - $cache_key = md5( $url ); + $cache_key = \md5( $url ); \progress_planner()->get_utils__cache()->set( $cache_key, self::get_lessons(), WEEK_IN_SECONDS ); } @@ -95,13 +93,13 @@ public function setUp(): void { $this->token = '123456789'; // Add a fake license key. - update_option( 'progress_planner_license_key', $this->token ); + \update_option( 'progress_planner_license_key', $this->token ); // Initiating the REST API. global $wp_rest_server; $wp_rest_server = new WP_REST_Server(); $this->server = $wp_rest_server; - do_action( 'rest_api_init' ); + \do_action( 'rest_api_init' ); } /** @@ -111,7 +109,7 @@ public function tearDown(): void { parent::tearDown(); // Delete the fake license key. - delete_option( 'progress_planner_license_key' ); + \delete_option( 'progress_planner_license_key' ); global $wp_rest_server; $wp_rest_server = null; @@ -123,7 +121,6 @@ public function tearDown(): void { * @return void. */ public function testEndpoint() { - $request = new WP_REST_Request( 'GET', '/progress-planner/v1/get-stats/' . $this->token ); $response = $this->server->dispatch( $request ); diff --git a/tests/phpunit/test-class-badges.php b/tests/phpunit/test-class-badges.php index 83ecc2869..4d263a1da 100644 --- a/tests/phpunit/test-class-badges.php +++ b/tests/phpunit/test-class-badges.php @@ -18,9 +18,7 @@ class Badges_Test extends \WP_UnitTestCase { * @return void */ public function test_content_badges() { - $badges = \progress_planner()->get_badges()->get_badges( 'content' ); - $this->assertCount( 3, $badges ); } @@ -30,9 +28,7 @@ public function test_content_badges() { * @return void */ public function test_maintenance_badges() { - $badges = \progress_planner()->get_badges()->get_badges( 'maintenance' ); - $this->assertCount( 3, $badges ); } @@ -42,9 +38,7 @@ public function test_maintenance_badges() { * @return void */ public function test_monthly_badges() { - $badges = \progress_planner()->get_badges()->get_badges( 'monthly' ); - $this->assertNotEmpty( $badges[ \gmdate( 'Y' ) ] ); } @@ -54,12 +48,10 @@ public function test_monthly_badges() { * @return void */ public function test_monthly_flat_badges() { - - $badges = \progress_planner()->get_badges()->get_badges( 'monthly_flat' ); - + $badges = \progress_planner()->get_badges()->get_badges( 'monthly_flat' ); $data['badges'] = []; foreach ( $badges as $badge ) { - $data['badges'][ $badge->get_id() ] = array_merge( + $data['badges'][ $badge->get_id() ] = \array_merge( [ 'id' => $badge->get_id(), 'name' => $badge->get_name(), diff --git a/tests/phpunit/test-class-content-activity.php b/tests/phpunit/test-class-content-activity.php index 39c4f9cd2..3d3f86335 100644 --- a/tests/phpunit/test-class-content-activity.php +++ b/tests/phpunit/test-class-content-activity.php @@ -56,7 +56,7 @@ public function test_points_decay_over_time( $days_ago, $expected_ratio ): void [ 'post_content' => 'Test content', 'post_status' => 'publish', - 'post_date' => \gmdate( 'Y-m-d H:i:s', strtotime( "-{$days_ago} days" ) ), + 'post_date' => \gmdate( 'Y-m-d H:i:s', \strtotime( "-{$days_ago} days" ) ), ] ); @@ -69,7 +69,7 @@ public function test_points_decay_over_time( $days_ago, $expected_ratio ): void $points = $content_activity->get_points( $date ); $base_points = Content::$points_config['publish']; - $expected_points = $days_ago >= 30 ? 0 : round( $base_points * $expected_ratio ); + $expected_points = $days_ago >= 30 ? 0 : \round( $base_points * $expected_ratio ); $this->assertEquals( $expected_points, $points ); } diff --git a/tests/phpunit/test-class-content-badges.php b/tests/phpunit/test-class-content-badges.php index ce6c26013..f7dcf3464 100644 --- a/tests/phpunit/test-class-content-badges.php +++ b/tests/phpunit/test-class-content-badges.php @@ -12,7 +12,6 @@ */ class Content_Badges_Test extends \WP_UnitTestCase { - /** * Current month. * @@ -38,9 +37,7 @@ public function set_up() { * @return void */ public function test_content_curator_0_progress() { - $group_badges = \progress_planner()->get_badges()->get_badges( 'content' ); - foreach ( $group_badges as $badge ) { if ( 'content-curator' === $badge->get_id() ) { $this->assertEquals( 0, $badge->progress_callback()['progress'] ); @@ -57,7 +54,6 @@ public function test_content_curator_0_progress() { * @return void */ public function test_content_curator_50_progress() { - // Insert 5 posts. for ( $i = 0; $i < 5; $i++ ) { $this->insert_post( 'Test post ' . $i ); @@ -81,7 +77,6 @@ public function test_content_curator_50_progress() { * @return void */ public function test_content_curator_100_progress() { - // Insert 10 posts. for ( $i = 0; $i < 10; $i++ ) { $this->insert_post( 'Test post ' . $i ); @@ -105,7 +100,6 @@ public function test_content_curator_100_progress() { * @return void */ public function test_revision_ranger_50_progress() { - // Insert 15 posts. for ( $i = 0; $i < 15; $i++ ) { $this->insert_post( 'Test post ' . $i ); @@ -129,7 +123,6 @@ public function test_revision_ranger_50_progress() { * @return void */ public function test_revision_ranger_100_progress() { - // Insert 30 posts. for ( $i = 0; $i < 30; $i++ ) { $this->insert_post( 'Test post ' . $i ); @@ -153,7 +146,6 @@ public function test_revision_ranger_100_progress() { * @return void */ public function test_revision_ranger_over_100_progress() { - // Insert 40 posts. for ( $i = 0; $i < 40; $i++ ) { $this->insert_post( 'Test post ' . $i ); @@ -177,7 +169,6 @@ public function test_revision_ranger_over_100_progress() { * @return void */ public function test_purposeful_publisher_50_progress() { - // Insert 25 posts. for ( $i = 0; $i < 25; $i++ ) { $this->insert_post( 'Test post ' . $i ); @@ -201,7 +192,6 @@ public function test_purposeful_publisher_50_progress() { * @return void */ public function test_purposeful_publisher_100_progress() { - // Insert 50 posts. for ( $i = 0; $i < 50; $i++ ) { $this->insert_post( 'Test post ' . $i ); @@ -225,7 +215,6 @@ public function test_purposeful_publisher_100_progress() { * @return void */ public function test_purposeful_publisher_over_100_progress() { - // Insert 60 posts. for ( $i = 0; $i < 60; $i++ ) { $this->insert_post( 'Test post ' . $i ); diff --git a/tests/phpunit/test-class-content.php b/tests/phpunit/test-class-content.php index ff8c6b88a..c7311e3c9 100644 --- a/tests/phpunit/test-class-content.php +++ b/tests/phpunit/test-class-content.php @@ -7,7 +7,6 @@ namespace Progress_Planner\Tests; -use DateTime; use Progress_Planner\Actions\Content; use WP_UnitTestCase; @@ -43,7 +42,7 @@ public function test_wp_insert_post_hook() { ]; // Insert a post and verify the hook was called. - $post_id = wp_insert_post( $post_data ); + $post_id = \wp_insert_post( $post_data ); // Assert that activities were created. $activities = \progress_planner()->get_activities__query()->query_activities( @@ -63,7 +62,7 @@ public function test_wp_insert_post_hook() { */ public function test_transition_post_status_hook() { // Create a draft post. - $post_id = wp_insert_post( + $post_id = \wp_insert_post( [ 'post_title' => 'Draft Post', 'post_content' => 'Draft content', @@ -72,7 +71,7 @@ public function test_transition_post_status_hook() { ); // Publish the post. - wp_publish_post( $post_id ); + \wp_publish_post( $post_id ); // Assert that publish activity was created. $activities = \progress_planner()->get_activities__query()->query_activities( @@ -93,7 +92,7 @@ public function test_transition_post_status_hook() { */ public function test_trash_post_hook() { // Create a post. - $post_id = wp_insert_post( + $post_id = \wp_insert_post( [ 'post_title' => 'Test Post', 'post_status' => 'publish', @@ -101,7 +100,7 @@ public function test_trash_post_hook() { ); // Trash the post. - wp_trash_post( $post_id ); + \wp_trash_post( $post_id ); // Assert that trash activity was created. $activities = \progress_planner()->get_activities__query()->query_activities( @@ -122,7 +121,7 @@ public function test_trash_post_hook() { */ public function test_delete_post_hook() { // Create a post. - $post_id = wp_insert_post( + $post_id = \wp_insert_post( [ 'post_title' => 'Test Post', 'post_status' => 'publish', @@ -130,7 +129,7 @@ public function test_delete_post_hook() { ); // Delete the post. - wp_delete_post( $post_id, true ); + \wp_delete_post( $post_id, true ); // Assert that delete activity was created. $activities = \progress_planner()->get_activities__query()->query_activities( @@ -150,9 +149,8 @@ public function test_delete_post_hook() { * Test multiple status transitions. */ public function test_multiple_status_transitions() { - // Create a draft post. - $post_id = wp_insert_post( + $post_id = \wp_insert_post( [ 'post_title' => 'Test Post', 'post_content' => 'Test content', @@ -172,7 +170,7 @@ public function test_multiple_status_transitions() { $this->assertCount( 0, $activities ); // Transition to pending. - wp_update_post( + \wp_update_post( [ 'ID' => $post_id, 'post_status' => 'pending', @@ -191,7 +189,7 @@ public function test_multiple_status_transitions() { $this->assertCount( 0, $activities ); // Transition to publish and update content (insert publish activity). - wp_update_post( + \wp_update_post( [ 'ID' => $post_id, 'post_content' => 'Updated content.', @@ -199,7 +197,7 @@ public function test_multiple_status_transitions() { ); // Transition back to draft. - wp_update_post( + \wp_update_post( [ 'ID' => $post_id, 'post_status' => 'draft', @@ -207,7 +205,7 @@ public function test_multiple_status_transitions() { ); // Transition to publish and update content again (since the post is updated less then 12 hours, we should not add an update activity because 'publish' activity is already created). - wp_update_post( + \wp_update_post( [ 'ID' => $post_id, 'post_status' => 'publish', @@ -225,7 +223,7 @@ public function test_multiple_status_transitions() { ); // Get the types in order. - $types = array_map( + $types = \array_map( function ( $activity ) { return $activity->type; }, @@ -244,9 +242,9 @@ public function tearDown(): void { parent::tearDown(); // Clean up any posts created during the test. - $posts = get_posts( [ 'numberposts' => -1 ] ); + $posts = \get_posts( [ 'numberposts' => -1 ] ); foreach ( $posts as $post ) { - wp_delete_post( $post->ID, true ); + \wp_delete_post( $post->ID, true ); } } } diff --git a/tests/phpunit/test-class-date.php b/tests/phpunit/test-class-date.php index be936a27c..1ff0f65a4 100644 --- a/tests/phpunit/test-class-date.php +++ b/tests/phpunit/test-class-date.php @@ -35,7 +35,7 @@ public function test_get_range() { public function test_get_periods( $start_date, $end_date, $frequency ) { $periods = \progress_planner()->get_utils__date()->get_periods( $start_date, $end_date, $frequency ); $this->assertEquals( $start_date, $periods[0]['start_date'] ); - $this->assertEquals( $end_date, end( $periods )['end_date']->modify( '+1 day' ) ); + $this->assertEquals( $end_date, \end( $periods )['end_date']->modify( '+1 day' ) ); } /** diff --git a/tests/phpunit/test-class-fewer-tags.php b/tests/phpunit/test-class-fewer-tags.php index 974204a64..4ed190446 100644 --- a/tests/phpunit/test-class-fewer-tags.php +++ b/tests/phpunit/test-class-fewer-tags.php @@ -42,13 +42,13 @@ class Fewer_Tags_Provider_Test extends \WP_UnitTestCase { */ public function setUp(): void { parent::setUp(); - $this->original_active_plugins = get_option( 'active_plugins', [] ); + $this->original_active_plugins = \get_option( 'active_plugins', [] ); // Initialize filesystem. global $wp_filesystem; if ( ! $wp_filesystem ) { require_once ABSPATH . 'wp-admin/includes/file.php'; - WP_Filesystem(); + \WP_Filesystem(); } $this->filesystem = $wp_filesystem; } @@ -57,7 +57,7 @@ public function setUp(): void { * Tear down the test. */ public function tearDown(): void { - update_option( 'active_plugins', $this->original_active_plugins ); + \update_option( 'active_plugins', $this->original_active_plugins ); parent::tearDown(); } @@ -66,9 +66,9 @@ public function tearDown(): void { */ public function test_should_add_task_when_plugin_inactive_and_tags_outnumber_posts() { // Create more tags than posts. - $tag1 = wp_insert_term( 'Tag 1', 'post_tag' ); + $tag1 = \wp_insert_term( 'Tag 1', 'post_tag' ); $this->assertNotWPError( $tag1 ); - $tag2 = wp_insert_term( 'Tag 2', 'post_tag' ); + $tag2 = \wp_insert_term( 'Tag 2', 'post_tag' ); $this->assertNotWPError( $tag2 ); // Create a new Fewer_Tags instance here so it's internal cache is populated with the correct data. @@ -77,8 +77,8 @@ public function test_should_add_task_when_plugin_inactive_and_tags_outnumber_pos $this->assertTrue( $this->task_provider->should_add_task() ); // Clean up. - wp_delete_term( $tag1['term_id'], 'post_tag' ); - wp_delete_term( $tag2['term_id'], 'post_tag' ); + \wp_delete_term( $tag1['term_id'], 'post_tag' ); + \wp_delete_term( $tag2['term_id'], 'post_tag' ); } /** @@ -86,11 +86,11 @@ public function test_should_add_task_when_plugin_inactive_and_tags_outnumber_pos */ public function test_should_add_task_when_plugin_inactive_but_tags_dont_outnumber_posts() { // Create one tag. - $tag = wp_insert_term( 'Tag 1', 'post_tag' ); + $tag = \wp_insert_term( 'Tag 1', 'post_tag' ); $this->assertNotWPError( $tag ); // Create two published posts. - $post1 = wp_insert_post( + $post1 = \wp_insert_post( [ 'post_title' => 'Test Post 1', 'post_status' => 'publish', @@ -99,7 +99,7 @@ public function test_should_add_task_when_plugin_inactive_but_tags_dont_outnumbe ); $this->assertNotWPError( $post1 ); - $post2 = wp_insert_post( + $post2 = \wp_insert_post( [ 'post_title' => 'Test Post 2', 'post_status' => 'publish', @@ -114,9 +114,9 @@ public function test_should_add_task_when_plugin_inactive_but_tags_dont_outnumbe $this->assertFalse( $this->task_provider->should_add_task() ); // Clean up. - wp_delete_post( $post1 ); - wp_delete_post( $post2 ); - wp_delete_term( $tag['term_id'], 'post_tag' ); + \wp_delete_post( $post1 ); + \wp_delete_post( $post2 ); + \wp_delete_term( $tag['term_id'], 'post_tag' ); } /** @@ -133,7 +133,7 @@ public function test_is_task_completed_when_plugin_active() { $this->filesystem->put_contents( $plugin_dir . '/fewer-tags.php', '<?php /* Plugin Name: Fewer Tags */' ); // Mock plugin as active in options. - update_option( 'active_plugins', [ 'fewer-tags/fewer-tags.php' ] ); + \update_option( 'active_plugins', [ 'fewer-tags/fewer-tags.php' ] ); // Create a new Fewer_Tags instance here so it's internal cache is populated with the correct data. $this->task_provider = new Fewer_Tags(); diff --git a/tests/phpunit/test-class-monthly-badge.php b/tests/phpunit/test-class-monthly-badge.php index 103f97d4d..73f57f7d8 100644 --- a/tests/phpunit/test-class-monthly-badge.php +++ b/tests/phpunit/test-class-monthly-badge.php @@ -15,7 +15,6 @@ */ class Monthly_Badge_Test extends \WP_UnitTestCase { - /** * Current month. * @@ -31,7 +30,7 @@ class Monthly_Badge_Test extends \WP_UnitTestCase { public function set_up() { parent::set_up(); - $this->current_month = strtolower( \gmdate( 'M' ) ); + $this->current_month = \strtolower( \gmdate( 'M' ) ); } /** @@ -40,7 +39,6 @@ public function set_up() { * @return void */ public function test_monthly_badge_0_percent() { - foreach ( \progress_planner()->get_badges()->get_badges( 'monthly_flat' ) as $badge ) { if ( 'monthly-' . $this->current_month === $badge->get_id() ) { $this->assertEquals( 0, $badge->progress_callback()['progress'] ); @@ -54,7 +52,6 @@ public function test_monthly_badge_0_percent() { * @return void */ public function test_monthly_badge_100_percent() { - for ( $i = 1; $i <= Monthly::TARGET_POINTS; $i++ ) { $this->insert_activity( 1000 + $i ); } @@ -66,14 +63,12 @@ public function test_monthly_badge_100_percent() { } } - /** * Test the monthly badge over 100 percent, we should top at 100 percent. * * @return void */ public function test_monthly_badge_over_100_percent() { - for ( $i = 1; $i <= Monthly::TARGET_POINTS + 2; $i++ ) { $this->insert_activity( 1000 + $i ); } diff --git a/tests/phpunit/test-class-page-types.php b/tests/phpunit/test-class-page-types.php index a7a17aecf..b90d9adc6 100644 --- a/tests/phpunit/test-class-page-types.php +++ b/tests/phpunit/test-class-page-types.php @@ -36,7 +36,6 @@ class Page_Types_Test extends \WP_UnitTestCase { * @return void */ public static function setUpBeforeClass(): void { - self::set_lessons_cache(); \progress_planner()->get_page_types()->create_taxonomy(); @@ -81,7 +80,7 @@ public static function set_lessons_cache() { ) : \add_query_arg( [ 'site' => \get_site_url() ], $url ); - $cache_key = md5( $url ); + $cache_key = \md5( $url ); \progress_planner()->get_utils__cache()->set( $cache_key, self::get_lessons(), WEEK_IN_SECONDS ); } @@ -124,7 +123,7 @@ public function test_maybe_update_terms() { public function test_get_page_types() { $page_types = \progress_planner()->get_page_types()->get_page_types(); $lessons = self::get_lessons(); - $this->assertCount( count( $lessons ), $page_types ); + $this->assertCount( \count( $lessons ), $page_types ); foreach ( $lessons as $lesson ) { $this->assertCount( @@ -145,7 +144,6 @@ function ( $page_type ) use ( $lesson ) { * @return void */ public function test_get_posts_by_type() { - // Assign the post to the "homepage" page type. \progress_planner()->get_page_types()->set_page_type_by_id( self::$homepage_post_id, @@ -194,13 +192,12 @@ public function test_assign_child_pages() { * @return void */ public function test_transition_post_status_updates_options() { - // Check if the options are set to default values. $this->assertEquals( 0, \get_option( 'page_on_front' ) ); $this->assertEquals( 'posts', \get_option( 'show_on_front' ) ); // Update homepage page to draft. - wp_update_post( + \wp_update_post( [ 'ID' => self::$homepage_post_id, 'post_status' => 'draft', @@ -213,7 +210,7 @@ public function test_transition_post_status_updates_options() { \wp_set_object_terms( self::$homepage_post_id, $term->term_id, \progress_planner()->get_page_types()::TAXONOMY_NAME ); // Update the page status to publish. - wp_update_post( + \wp_update_post( [ 'ID' => self::$homepage_post_id, 'post_status' => 'publish', diff --git a/tests/phpunit/test-class-suggested-tasks.php b/tests/phpunit/test-class-suggested-tasks.php index 38650ed12..7605cc30f 100644 --- a/tests/phpunit/test-class-suggested-tasks.php +++ b/tests/phpunit/test-class-suggested-tasks.php @@ -7,28 +7,10 @@ namespace Progress_Planner\Tests; -use Progress_Planner\Suggested_Tasks; - /** - * Suggested_Tasks test case. + * CPT_Recommendations test case. */ -class Suggested_Tasks_Test extends \WP_UnitTestCase { - - /** - * Suggested_Tasks object. - * - * @var Suggested_Tasks - */ - protected $suggested_tasks; - - /** - * Setup the test case. - * - * @return void - */ - public function set_up() { - $this->suggested_tasks = \progress_planner()->get_suggested_tasks(); - } +class CPT_Recommendations_Test extends \WP_UnitTestCase { /** * Test the task_cleanup method. @@ -39,45 +21,85 @@ public function test_task_cleanup() { // Tasks that should not be removed. $tasks_to_keep = [ [ - 'task_id' => 'review-post-14-' . \gmdate( 'YW' ), - 'date' => \gmdate( 'YW' ), + 'post_title' => 'review-post-14-' . \gmdate( 'YW' ), + 'task_id' => 'review-post-14-' . \gmdate( 'YW' ), + 'date' => \gmdate( 'YW' ), + 'category' => 'content-update', + 'provider_id' => 'review-post', + ], + [ + 'post_title' => 'create-post-' . \gmdate( 'YW' ), + 'task_id' => 'create-post-' . \gmdate( 'YW' ), + 'date' => \gmdate( 'YW' ), + 'category' => 'content-new', + 'provider_id' => 'create-post', + ], + [ + 'post_title' => 'update-core-' . \gmdate( 'YW' ), + 'task_id' => 'update-core-' . \gmdate( 'YW' ), + 'date' => \gmdate( 'YW' ), + 'category' => 'maintenance', + 'provider_id' => 'update-core', ], [ - 'task_id' => 'create-post-' . \gmdate( 'YW' ), - 'date' => \gmdate( 'YW' ), + 'post_title' => 'settings-saved-' . \gmdate( 'YW' ), + 'task_id' => 'settings-saved-' . \gmdate( 'YW' ), + 'date' => \gmdate( 'YW' ), + 'provider_id' => 'settings-saved', + 'category' => 'configuration', ], + + // Not repetitive task, but with past date. [ - 'task_id' => 'update-core-' . \gmdate( 'YW' ), - 'date' => \gmdate( 'YW' ), + 'post_title' => 'settings-saved-202451', + 'task_id' => 'settings-saved-202451', + 'date' => '202451', + 'provider_id' => 'settings-saved', + 'category' => 'configuration', ], + + // User task, with past date. [ - 'task_id' => 'settings-saved-' . \gmdate( 'YW' ), - 'date' => \gmdate( 'YW' ), + 'post_title' => 'user-task-1', + 'task_id' => 'user-task-1', + 'provider_id' => 'user', + 'category' => 'user', + 'date' => '202451', ], ]; foreach ( $tasks_to_keep as $task ) { - $this->suggested_tasks->get_tasks_manager()->add_pending_task( $task ); + \progress_planner()->get_suggested_tasks_db()->add( $task ); } // Tasks that should be removed. $tasks_to_remove = [ + + // Repetitive task with past date. [ - 'task_id' => 'update-core-202451', - 'date' => '202451', + 'post_title' => 'update-core-202451', + 'task_id' => 'update-core-202451', + 'date' => '202451', + 'category' => 'maintenance', + 'provider_id' => 'update-core', ], + + // Task with invalid provider. [ - 'task_id' => 'settings-saved-202451', - 'date' => '202451', + 'post_title' => 'invalid-task-1', + 'task_id' => 'invalid-task-1', + 'date' => '202451', + 'category' => 'invalid-category', + 'provider_id' => 'invalid-provider', ], ]; foreach ( $tasks_to_remove as $task ) { - $this->suggested_tasks->get_tasks_manager()->add_pending_task( $task ); + \progress_planner()->get_suggested_tasks_db()->add( $task ); } - $this->suggested_tasks->get_tasks_manager()->cleanup_pending_tasks(); - - $this->assertEquals( count( $tasks_to_keep ), \count( \progress_planner()->get_settings()->get( 'tasks', [] ) ) ); + \progress_planner()->get_suggested_tasks()->get_tasks_manager()->cleanup_pending_tasks(); + \wp_cache_flush_group( \Progress_Planner\Suggested_Tasks_DB::GET_TASKS_CACHE_GROUP ); // Clear the cache. + $this->assertEquals( \count( $tasks_to_keep ), \count( \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'post_status' => 'publish' ] ) ) ); } } diff --git a/tests/phpunit/test-class-terms-without-description-data-collector.php b/tests/phpunit/test-class-terms-without-description-data-collector.php index f5b449853..d997f66ff 100644 --- a/tests/phpunit/test-class-terms-without-description-data-collector.php +++ b/tests/phpunit/test-class-terms-without-description-data-collector.php @@ -38,12 +38,12 @@ public function setUp(): void { $this->data_collector->init(); // Get the default category and store its original description. - $default_cat = get_cat_ID( 'Uncategorized' ); - $default_term = get_term( $default_cat, 'category' ); + $default_cat = \get_cat_ID( 'Uncategorized' ); + $default_term = \get_term( $default_cat, 'category' ); $this->default_cat_original_description = $default_term->description; // Add a temporary description. - wp_update_term( $default_cat, 'category', [ 'description' => 'Temporary Description' ] ); + \wp_update_term( $default_cat, 'category', [ 'description' => 'Temporary Description' ] ); } /** @@ -51,8 +51,8 @@ public function setUp(): void { */ public function tearDown(): void { // Restore the original description. - $default_cat = get_cat_ID( 'Uncategorized' ); - wp_update_term( $default_cat, 'category', [ 'description' => $this->default_cat_original_description ] ); + $default_cat = \get_cat_ID( 'Uncategorized' ); + \wp_update_term( $default_cat, 'category', [ 'description' => $this->default_cat_original_description ] ); parent::tearDown(); } @@ -62,16 +62,16 @@ public function tearDown(): void { */ public function test_collect_returns_terms_without_description() { // Create a category with description. - $term_with_desc = wp_insert_term( 'Category With Description', 'category', [ 'description' => 'Test Description' ] ); + $term_with_desc = \wp_insert_term( 'Category With Description', 'category', [ 'description' => 'Test Description' ] ); $this->assertNotWPError( $term_with_desc ); // Create our test category with empty description. - $term_result = wp_insert_term( 'Test Category', 'category' ); + $term_result = \wp_insert_term( 'Test Category', 'category' ); $this->assertNotWPError( $term_result ); $term_id = $term_result['term_id']; // Create two posts and assign the term to them. - $post_id1 = wp_insert_post( + $post_id1 = \wp_insert_post( [ 'post_title' => 'Test Post 1', 'post_content' => 'Test content 1', @@ -80,9 +80,9 @@ public function test_collect_returns_terms_without_description() { ] ); $this->assertNotWPError( $post_id1 ); - wp_set_object_terms( $post_id1, $term_id, 'category' ); + \wp_set_object_terms( $post_id1, $term_id, 'category' ); - $post_id2 = wp_insert_post( + $post_id2 = \wp_insert_post( [ 'post_title' => 'Test Post 2', 'post_content' => 'Test content 2', @@ -91,7 +91,7 @@ public function test_collect_returns_terms_without_description() { ] ); $this->assertNotWPError( $post_id2 ); - wp_set_object_terms( $post_id2, $term_id, 'category' ); + \wp_set_object_terms( $post_id2, $term_id, 'category' ); // Get the data. $this->data_collector->update_cache(); @@ -104,8 +104,8 @@ public function test_collect_returns_terms_without_description() { $this->assertEquals( 'category', $result['taxonomy'] ); // Clean up. - wp_delete_post( $post_id1 ); - wp_delete_post( $post_id2 ); + \wp_delete_post( $post_id1 ); + \wp_delete_post( $post_id2 ); } /** @@ -113,7 +113,7 @@ public function test_collect_returns_terms_without_description() { */ public function test_collect_ignores_terms_with_description() { // Create a category with description. - $term_result = wp_insert_term( 'Test Category', 'category', [ 'description' => 'Test Description' ] ); + $term_result = \wp_insert_term( 'Test Category', 'category', [ 'description' => 'Test Description' ] ); $this->assertNotWPError( $term_result ); // Get the data. @@ -129,12 +129,12 @@ public function test_collect_ignores_terms_with_description() { */ public function test_collect_respects_excluded_terms() { // Create a category with empty description. - $term_result = wp_insert_term( 'Test Category', 'category' ); + $term_result = \wp_insert_term( 'Test Category', 'category' ); $this->assertNotWPError( $term_result ); $term_id = $term_result['term_id']; // Add filter to exclude the term. - add_filter( + \add_filter( 'progress_planner_terms_without_description_exclude_term_ids', function () use ( $term_id ) { return [ $term_id ]; @@ -154,12 +154,12 @@ function () use ( $term_id ) { */ public function test_cache_is_updated_when_term_is_edited() { // Create a category with empty description. - $term_result = wp_insert_term( 'Test Category', 'category' ); + $term_result = \wp_insert_term( 'Test Category', 'category' ); $this->assertNotWPError( $term_result ); $term_id = $term_result['term_id']; // Create a post and assign the term to it. - $post_id1 = wp_insert_post( + $post_id1 = \wp_insert_post( [ 'post_title' => 'Test Post 1', 'post_content' => 'Test content 1', @@ -168,9 +168,9 @@ public function test_cache_is_updated_when_term_is_edited() { ] ); $this->assertNotWPError( $post_id1 ); - wp_set_object_terms( $post_id1, $term_id, 'category' ); + \wp_set_object_terms( $post_id1, $term_id, 'category' ); - $post_id2 = wp_insert_post( + $post_id2 = \wp_insert_post( [ 'post_title' => 'Test Post 2', 'post_content' => 'Test content 2', @@ -179,7 +179,7 @@ public function test_cache_is_updated_when_term_is_edited() { ] ); $this->assertNotWPError( $post_id2 ); - wp_set_object_terms( $post_id2, $term_id, 'category' ); + \wp_set_object_terms( $post_id2, $term_id, 'category' ); // Get initial data. $this->data_collector->update_cache(); @@ -187,7 +187,7 @@ public function test_cache_is_updated_when_term_is_edited() { $this->assertIsArray( $initial_result ); // Edit the term to add description. - wp_update_term( + \wp_update_term( $term_id, 'category', [ @@ -203,8 +203,8 @@ public function test_cache_is_updated_when_term_is_edited() { $this->assertNull( $updated_result ); // Clean up. - wp_delete_post( $post_id1 ); - wp_delete_post( $post_id2 ); + \wp_delete_post( $post_id1 ); + \wp_delete_post( $post_id2 ); } /** @@ -212,12 +212,12 @@ public function test_cache_is_updated_when_term_is_edited() { */ public function test_cache_is_updated_when_term_is_deleted() { // Create a category with empty description. - $term_result = wp_insert_term( 'Test Category', 'category' ); + $term_result = \wp_insert_term( 'Test Category', 'category' ); $this->assertNotWPError( $term_result ); $term_id = $term_result['term_id']; // Create a post and assign the term to it. - $post_id1 = wp_insert_post( + $post_id1 = \wp_insert_post( [ 'post_title' => 'Test Post 1', 'post_content' => 'Test content 1', @@ -226,9 +226,9 @@ public function test_cache_is_updated_when_term_is_deleted() { ] ); $this->assertNotWPError( $post_id1 ); - wp_set_object_terms( $post_id1, $term_id, 'category' ); + \wp_set_object_terms( $post_id1, $term_id, 'category' ); - $post_id2 = wp_insert_post( + $post_id2 = \wp_insert_post( [ 'post_title' => 'Test Post 2', 'post_content' => 'Test content 2', @@ -237,7 +237,7 @@ public function test_cache_is_updated_when_term_is_deleted() { ] ); $this->assertNotWPError( $post_id2 ); - wp_set_object_terms( $post_id2, $term_id, 'category' ); + \wp_set_object_terms( $post_id2, $term_id, 'category' ); // Get initial data. $this->data_collector->update_cache(); @@ -245,7 +245,7 @@ public function test_cache_is_updated_when_term_is_deleted() { $this->assertIsArray( $initial_result ); // Delete the term. - wp_delete_term( $term_id, 'category' ); + \wp_delete_term( $term_id, 'category' ); // Get data again. $this->data_collector->update_cache(); @@ -255,8 +255,8 @@ public function test_cache_is_updated_when_term_is_deleted() { $this->assertNull( $updated_result ); // Clean up. - wp_delete_post( $post_id1 ); - wp_delete_post( $post_id2 ); + \wp_delete_post( $post_id1 ); + \wp_delete_post( $post_id2 ); } /** @@ -264,7 +264,7 @@ public function test_cache_is_updated_when_term_is_deleted() { */ public function test_collect_ignores_non_public_taxonomies() { // Register a non-public taxonomy. - register_taxonomy( + \register_taxonomy( 'test_taxonomy', 'post', [ @@ -273,7 +273,7 @@ public function test_collect_ignores_non_public_taxonomies() { ); // Create a term in the non-public taxonomy. - $term_result = wp_insert_term( 'Test Term', 'test_taxonomy' ); + $term_result = \wp_insert_term( 'Test Term', 'test_taxonomy' ); $this->assertNotWPError( $term_result ); // Get the data. @@ -284,6 +284,6 @@ public function test_collect_ignores_non_public_taxonomies() { $this->assertNull( $result ); // Clean up. - unregister_taxonomy( 'test_taxonomy' ); + \unregister_taxonomy( 'test_taxonomy' ); } } diff --git a/tests/phpunit/test-class-terms-without-posts-data-collector.php b/tests/phpunit/test-class-terms-without-posts-data-collector.php index ed1209ebe..470530944 100644 --- a/tests/phpunit/test-class-terms-without-posts-data-collector.php +++ b/tests/phpunit/test-class-terms-without-posts-data-collector.php @@ -36,7 +36,7 @@ public function setUp(): void { */ public function test_collect_returns_terms_without_posts() { // Create a category. - $term_result = wp_insert_term( 'Test Category', 'category' ); + $term_result = \wp_insert_term( 'Test Category', 'category' ); $this->assertNotWPError( $term_result ); $term_id = $term_result['term_id']; @@ -56,11 +56,11 @@ public function test_collect_returns_terms_without_posts() { */ public function test_collect_ignores_terms_with_posts() { // Create a category. - $term_result = wp_insert_term( 'Test Category', 'category' ); + $term_result = \wp_insert_term( 'Test Category', 'category' ); $this->assertNotWPError( $term_result ); // Create two posts and assign them to the category. - $post_id1 = wp_insert_post( + $post_id1 = \wp_insert_post( [ 'post_title' => 'Test Post 1', 'post_status' => 'publish', @@ -69,7 +69,7 @@ public function test_collect_ignores_terms_with_posts() { ); $this->assertNotWPError( $post_id1 ); - $post_id2 = wp_insert_post( + $post_id2 = \wp_insert_post( [ 'post_title' => 'Test Post 2', 'post_status' => 'publish', @@ -78,10 +78,10 @@ public function test_collect_ignores_terms_with_posts() { ); $this->assertNotWPError( $post_id2 ); - $set_terms = wp_set_object_terms( $post_id1, $term_result['term_id'], 'category' ); + $set_terms = \wp_set_object_terms( $post_id1, $term_result['term_id'], 'category' ); $this->assertNotWPError( $set_terms ); - $set_terms = wp_set_object_terms( $post_id2, $term_result['term_id'], 'category' ); + $set_terms = \wp_set_object_terms( $post_id2, $term_result['term_id'], 'category' ); $this->assertNotWPError( $set_terms ); // Get the data. @@ -97,12 +97,12 @@ public function test_collect_ignores_terms_with_posts() { */ public function test_collect_respects_excluded_terms() { // Create a category. - $term_result = wp_insert_term( 'Test Category', 'category' ); + $term_result = \wp_insert_term( 'Test Category', 'category' ); $this->assertNotWPError( $term_result ); $term_id = $term_result['term_id']; // Add filter to exclude the term. - add_filter( + \add_filter( 'progress_planner_terms_without_posts_exclude_term_ids', function () use ( $term_id ) { return [ $term_id ]; @@ -122,7 +122,7 @@ function () use ( $term_id ) { */ public function test_cache_is_updated_when_term_is_deleted() { // Create a category. - $term_result = wp_insert_term( 'Test Category', 'category' ); + $term_result = \wp_insert_term( 'Test Category', 'category' ); $this->assertNotWPError( $term_result ); $term_id = $term_result['term_id']; @@ -132,7 +132,7 @@ public function test_cache_is_updated_when_term_is_deleted() { $this->assertIsArray( $initial_result ); // Delete the term. - wp_delete_term( $term_id, 'category' ); + \wp_delete_term( $term_id, 'category' ); // Get data again. $this->data_collector->update_cache(); @@ -147,7 +147,7 @@ public function test_cache_is_updated_when_term_is_deleted() { */ public function test_collect_ignores_non_public_taxonomies() { // Register a non-public taxonomy. - register_taxonomy( + \register_taxonomy( 'test_taxonomy', 'post', [ @@ -156,7 +156,7 @@ public function test_collect_ignores_non_public_taxonomies() { ); // Create a term in the non-public taxonomy. - $term_result = wp_insert_term( 'Test Term', 'test_taxonomy' ); + $term_result = \wp_insert_term( 'Test Term', 'test_taxonomy' ); $this->assertNotWPError( $term_result ); // Get the data. @@ -167,7 +167,7 @@ public function test_collect_ignores_non_public_taxonomies() { $this->assertNull( $result ); // Clean up. - unregister_taxonomy( 'test_taxonomy' ); + \unregister_taxonomy( 'test_taxonomy' ); } /** @@ -175,12 +175,12 @@ public function test_collect_ignores_non_public_taxonomies() { */ public function test_cache_is_updated_when_terms_are_changed() { // Create a category. - $term_result = wp_insert_term( 'Test Category', 'category' ); + $term_result = \wp_insert_term( 'Test Category', 'category' ); $this->assertNotWPError( $term_result ); $term_id = $term_result['term_id']; // Create two posts. - $post_id1 = wp_insert_post( + $post_id1 = \wp_insert_post( [ 'post_title' => 'Test Post 1', 'post_status' => 'publish', @@ -189,7 +189,7 @@ public function test_cache_is_updated_when_terms_are_changed() { ); $this->assertNotWPError( $post_id1 ); - $post_id2 = wp_insert_post( + $post_id2 = \wp_insert_post( [ 'post_title' => 'Test Post 2', 'post_status' => 'publish', @@ -204,10 +204,10 @@ public function test_cache_is_updated_when_terms_are_changed() { $this->assertIsArray( $initial_result ); // Assign both terms to the posts. - $set_terms = wp_set_object_terms( $post_id1, $term_id, 'category' ); + $set_terms = \wp_set_object_terms( $post_id1, $term_id, 'category' ); $this->assertNotWPError( $set_terms ); - $set_terms = wp_set_object_terms( $post_id2, $term_id, 'category' ); + $set_terms = \wp_set_object_terms( $post_id2, $term_id, 'category' ); $this->assertNotWPError( $set_terms ); // Get data again. diff --git a/tests/phpunit/test-class-todo.php b/tests/phpunit/test-class-todo.php deleted file mode 100644 index a18af887d..000000000 --- a/tests/phpunit/test-class-todo.php +++ /dev/null @@ -1,28 +0,0 @@ -<?php // phpcs:disable Generic.Commenting.Todo -/** - * Class Todo_Test - * - * @package Progress_Planner\Tests - */ - -namespace Progress_Planner\Tests; - -use Progress_Planner\Todo; - -/** - * Todo test case. - */ -class Todo_Test extends \WP_UnitTestCase { - - /** - * Test get_items method. - * - * @return void - */ - public function test_get_items() { - $items = \progress_planner()->get_todo()->get_items(); - $this->assertIsArray( $items ); - $this->assertEmpty( $items ); - } -} -// phpcs:enable Generic.Commenting.Todo diff --git a/tests/phpunit/test-class-uninstall.php b/tests/phpunit/test-class-uninstall.php index 60d648fbc..1b3a0e20b 100644 --- a/tests/phpunit/test-class-uninstall.php +++ b/tests/phpunit/test-class-uninstall.php @@ -18,13 +18,13 @@ class Uninstall_Test extends \WP_UnitTestCase { * @return void */ public function test_uninstall_file_runs_without_fatal_error() { - $uninstall_file = plugin_dir_path( __DIR__ ) . '../uninstall.php'; + $uninstall_file = \plugin_dir_path( __DIR__ ) . '../uninstall.php'; $this->assertFileExists( $uninstall_file, 'Uninstall file does not exist.' ); // Catch fatal errors. $errors = []; - set_error_handler( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler + \set_error_handler( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler function ( $errno, $errstr ) use ( &$errors ) { $errors[] = "$errno: $errstr"; return true; // prevent default error handler. @@ -32,7 +32,7 @@ function ( $errno, $errstr ) use ( &$errors ) { ); // Needed to simulate WordPress uninstall context. - define( 'WP_UNINSTALL_PLUGIN', true ); + \define( 'WP_UNINSTALL_PLUGIN', true ); // Include the uninstall script. try { @@ -41,8 +41,8 @@ function ( $errno, $errstr ) use ( &$errors ) { $this->fail( 'Fatal error during uninstall.php: ' . $e->getMessage() ); } - restore_error_handler(); + \restore_error_handler(); - $this->assertEmpty( $errors, 'Uninstall file caused PHP warnings or errors: ' . implode( ', ', $errors ) ); + $this->assertEmpty( $errors, 'Uninstall file caused PHP warnings or errors: ' . \implode( ', ', $errors ) ); } } diff --git a/tests/phpunit/test-class-upgrade-migartion-130.php b/tests/phpunit/test-class-upgrade-migration-130.php similarity index 94% rename from tests/phpunit/test-class-upgrade-migartion-130.php rename to tests/phpunit/test-class-upgrade-migration-130.php index 0d06ccd9b..83646e969 100644 --- a/tests/phpunit/test-class-upgrade-migartion-130.php +++ b/tests/phpunit/test-class-upgrade-migration-130.php @@ -19,7 +19,6 @@ class Upgrade_Migrations_130_Test extends \WP_UnitTestCase { * @return void */ public function test_recreating_tasks_from_activities() { - // Delete all activities. \progress_planner()->get_activities__query()->delete_activities( \progress_planner()->get_activities__query()->query_activities( @@ -92,7 +91,6 @@ public function test_recreating_tasks_from_activities() { // Create a new activity for each item. foreach ( $activity_ids as $activity_id ) { - $activity = new \Progress_Planner\Activities\Suggested_Task(); $activity->type = 'completed'; $activity->data_id = $activity_id; @@ -109,7 +107,7 @@ public function test_recreating_tasks_from_activities() { // Verify that every value in the $activity_ids array is present in the $tasks array and has completed status. foreach ( $activity_ids as $activity_id ) { - $matching_tasks = array_filter( + $matching_tasks = \array_filter( $tasks, function ( $task ) use ( $activity_id ) { return isset( $task['task_id'] ) && @@ -119,14 +117,14 @@ function ( $task ) use ( $activity_id ) { $this->assertNotEmpty( $matching_tasks, - sprintf( 'Task ID "%s" not found in tasks', $activity_id ) + \sprintf( 'Task ID "%s" not found in tasks', $activity_id ) ); - $task = reset( $matching_tasks ); + $task = \reset( $matching_tasks ); $this->assertEquals( 'completed', $task['status'], - sprintf( 'Task ID "%s" status is not "completed"', $activity_id ) + \sprintf( 'Task ID "%s" status is not "completed"', $activity_id ) ); } } diff --git a/tests/phpunit/test-class-upgrade-migrations-111.php b/tests/phpunit/test-class-upgrade-migrations-111.php index b17e390fa..af7d8f93a 100644 --- a/tests/phpunit/test-class-upgrade-migrations-111.php +++ b/tests/phpunit/test-class-upgrade-migrations-111.php @@ -18,7 +18,6 @@ class Upgrade_Migrations_111_Test extends \WP_UnitTestCase { * @return void */ public function test_dataset_1() { - // Delete all activities. \progress_planner()->get_activities__query()->delete_activities( \progress_planner()->get_activities__query()->query_activities( @@ -215,7 +214,7 @@ public function test_dataset_1() { ]; // Add the suggested tasks to the database. - \update_option( 'progress_planner_suggested_tasks', [ 'completed' => array_keys( $migration_map ) ] ); + \update_option( 'progress_planner_suggested_tasks', [ 'completed' => \array_keys( $migration_map ) ] ); // Create a new activity for each item. foreach ( $migration_map as $old_task_id => $item ) { @@ -245,7 +244,7 @@ public function test_dataset_1() { // Verify that every value in the $items array is present in the $tasks array and has completed status. foreach ( $migration_map as $item ) { - $matching_tasks = array_filter( + $matching_tasks = \array_filter( $tasks, function ( $task ) use ( $item ) { return isset( $task['task_id'] ) && @@ -256,14 +255,14 @@ function ( $task ) use ( $item ) { $this->assertNotEmpty( $matching_tasks, - sprintf( 'Task ID "%s" not found in tasks', $item['task_id'] ) + \sprintf( 'Task ID "%s" not found in tasks', $item['task_id'] ) ); - $task = reset( $matching_tasks ); + $task = \reset( $matching_tasks ); $this->assertEquals( 'completed', $task['status'], - sprintf( 'Task ID "%s" status is not "completed"', $item['task_id'] ) + \sprintf( 'Task ID "%s" status is not "completed"', $item['task_id'] ) ); } diff --git a/uninstall.php b/uninstall.php index edecdca6c..2bbe96295 100644 --- a/uninstall.php +++ b/uninstall.php @@ -8,7 +8,7 @@ */ // If uninstall not called from WordPress, then exit. -if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { +if ( ! \defined( 'WP_UNINSTALL_PLUGIN' ) ) { exit; } @@ -23,16 +23,16 @@ * @return void */ function progress_planner_cleanup_options() { - $value = get_option( \Progress_Planner\Settings::OPTION_NAME, [] ); + $value = \get_option( \Progress_Planner\Settings::OPTION_NAME, [] ); $keep = [ 'badges', 'activation_date' ]; - foreach ( array_keys( $value ) as $key ) { // @phpstan-ignore-line argument.type - if ( ! in_array( $key, $keep, true ) ) { + foreach ( \array_keys( $value ) as $key ) { // @phpstan-ignore-line argument.type + if ( ! \in_array( $key, $keep, true ) ) { unset( $value[ $key ] ); // @phpstan-ignore-line offsetAccess.nonOffsetAccessible } } - update_option( \Progress_Planner\Settings::OPTION_NAME, $value ); + \update_option( \Progress_Planner\Settings::OPTION_NAME, $value ); } -progress_planner_cleanup_options(); +\progress_planner_cleanup_options(); // Delete the custom database tables. global $wpdb; diff --git a/views/admin-page-header.php b/views/admin-page-header.php index 435c59b2e..68fc59b8f 100644 --- a/views/admin-page-header.php +++ b/views/admin-page-header.php @@ -6,7 +6,7 @@ */ // Exit if accessed directly. -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -58,7 +58,7 @@ '-18 months' => \esc_html__( 'Activity over the past 18 months', 'progress-planner' ), '-24 months' => \esc_html__( 'Activity over the past 24 months', 'progress-planner' ), ] as $progress_planner_range => $progress_planner_label ) { - printf( + \printf( '<option value="%1$s" %2$s>%3$s</option>', \esc_attr( $progress_planner_range ), \selected( $progress_planner_active_range, $progress_planner_range, false ), @@ -76,7 +76,7 @@ 'weekly' => \esc_html__( 'Weekly', 'progress-planner' ), 'monthly' => \esc_html__( 'Monthly', 'progress-planner' ), ] as $progress_planner_frequency => $progress_planner_label ) { - printf( + \printf( '<option value="%1$s" %2$s>%3$s</option>', \esc_attr( $progress_planner_frequency ), \selected( $progress_planner_active_frequency, $progress_planner_frequency, false ), diff --git a/views/admin-page-settings.php b/views/admin-page-settings.php index 9dee69448..ab6c23d09 100644 --- a/views/admin-page-settings.php +++ b/views/admin-page-settings.php @@ -6,7 +6,7 @@ */ // Exit if accessed directly. -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } ?> @@ -28,7 +28,7 @@ <?php \progress_planner()->the_asset( 'images/icon_settings.svg' ); ?> </span> <span> - <?php esc_html_e( 'Your Progress Planner settings', 'progress-planner' ); ?> + <?php \esc_html_e( 'Your Progress Planner settings', 'progress-planner' ); ?> </span> </h1> @@ -41,7 +41,7 @@ <?php \progress_planner()->the_view( 'page-settings/license.php' ); ?> </div> - <?php wp_nonce_field( 'progress_planner' ); ?> + <?php \wp_nonce_field( 'progress_planner' ); ?> <button id="prpl-settings-submit" @@ -49,7 +49,7 @@ class="prpl-button-primary" type="button" style="display:block;width:min-content;" > - <?php esc_attr_e( 'Save', 'progress-planner' ); ?> + <?php \esc_attr_e( 'Save', 'progress-planner' ); ?> </button> </form> </div> diff --git a/views/admin-page.php b/views/admin-page.php index 000b9fd18..05c6172fb 100644 --- a/views/admin-page.php +++ b/views/admin-page.php @@ -6,7 +6,7 @@ */ // Exit if accessed directly. -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -18,7 +18,7 @@ } ?> -<div class="wrap prpl-wrap <?php echo esc_attr( $prpl_wrapper_class ); ?>"> +<div class="wrap prpl-wrap <?php echo \esc_attr( $prpl_wrapper_class ); ?>"> <?php if ( true === $prpl_privacy_policy_accepted ) : ?> <h1 class="screen-reader-text"><?php \esc_html_e( 'Progress Planner', 'progress-planner' ); ?></h1> <?php \progress_planner()->the_view( 'admin-page-header.php' ); ?> @@ -35,10 +35,12 @@ * * @since 1.1.1 */ - do_action( 'progress_planner_admin_page_after_widgets' ); + \do_action( 'progress_planner_admin_page_after_widgets' ); ?> <?php else : ?> <?php \progress_planner()->the_view( 'welcome.php' ); ?> <?php endif; ?> </div> <div class="prpl-overlay" style="display: none;" onclick="document.querySelector('[data-tooltip-visible=true]').removeAttribute('data-tooltip-visible')"></div> + +<?php \progress_planner()->the_view( 'js-templates/suggested-task.html' ); ?> \ No newline at end of file diff --git a/views/dashboard-widgets/score.php b/views/dashboard-widgets/score.php index 7fdef3569..72cd4ab18 100644 --- a/views/dashboard-widgets/score.php +++ b/views/dashboard-widgets/score.php @@ -10,11 +10,21 @@ ?> <div class="prpl-dashboard-widget"> <div> - <prpl-gauge background="#fff" color="var(--prpl-color-accent-orange)" contentFontSize="var(--prpl-font-size-4xl)" contentPadding="var(--prpl-padding)" marginBottom="0"> - <progress max="<?php echo (int) Monthly::TARGET_POINTS; ?>" value="<?php echo (float) \progress_planner()->get_admin__widgets__suggested_tasks()->get_score(); ?>"> + <prpl-gauge + id="prpl-gauge-ravi" + background="#fff" + color="var(--prpl-color-accent-orange)" + contentFontSize="var(--prpl-font-size-4xl)" + contentPadding="var(--prpl-padding)" + marginBottom="0" + data-max="<?php echo (int) Monthly::TARGET_POINTS; ?>" + data-value="<?php echo (float) \progress_planner()->get_admin__widgets__suggested_tasks()->get_score()['target_score']; ?>" + data-badge-id="<?php echo \esc_attr( Monthly::get_badge_id_from_date( new \DateTime() ) ); ?>" + > + <progress max="<?php echo (int) Monthly::TARGET_POINTS; ?>" value="<?php echo (float) \progress_planner()->get_admin__widgets__suggested_tasks()->get_score()['target_score']; ?>"> <prpl-badge complete="true" - badge-id="<?php echo esc_attr( Monthly::get_badge_id_from_date( new \DateTime() ) ); ?>" + badge-id="<?php echo \esc_attr( Monthly::get_badge_id_from_date( new \DateTime() ) ); ?>" ></prpl-badge> </progress> </prpl-gauge> @@ -22,7 +32,7 @@ </div> <div> - <prpl-gauge background="#fff" color="<?php echo esc_attr( \progress_planner()->get_admin__widgets__activity_scores()->get_gauge_color( \progress_planner()->get_admin__widgets__activity_scores()->get_score() ) ); ?>" contentFontSize="var(--prpl-font-size-5xl)" contentPadding="var(--prpl-padding)" marginBottom="0"> + <prpl-gauge background="#fff" color="<?php echo \esc_attr( \progress_planner()->get_admin__widgets__activity_scores()->get_gauge_color( \progress_planner()->get_admin__widgets__activity_scores()->get_score() ) ); ?>" contentFontSize="var(--prpl-font-size-5xl)" contentPadding="var(--prpl-padding)" marginBottom="0"> <progress max="100" value="<?php echo (float) \progress_planner()->get_admin__widgets__activity_scores()->get_score(); ?>"> <?php echo \esc_html( \progress_planner()->get_admin__widgets__activity_scores()->get_score() ); ?> </progress> @@ -35,20 +45,23 @@ <h3><?php \esc_html_e( 'Ravi\'s Recommendations', 'progress-planner' ); ?></h3> <ul style="display:none"></ul> -<ul class="prpl-suggested-tasks-list"></ul> +<p class="prpl-suggested-tasks-loading"> + <?php \esc_html_e( 'Loading tasks...', 'progress-planner' ); ?> +</p> +<ul id="prpl-suggested-tasks-list" class="prpl-suggested-tasks-list"></ul> <?php if ( \current_user_can( 'manage_options' ) ) : ?> <div class="prpl-dashboard-widget-footer"> - <img src="<?php echo \esc_attr( constant( 'PROGRESS_PLANNER_URL' ) . '/assets/images/icon_progress_planner.svg' ); ?>" style="width:1.85em;" alt="" /> + <img src="<?php echo \esc_attr( \constant( 'PROGRESS_PLANNER_URL' ) . '/assets/images/icon_progress_planner.svg' ); ?>" style="width:1.85em;" alt="" /> <div> - <?php $prpl_pending_celebration_tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'status', 'pending_celebration' ); ?> + <?php $prpl_pending_celebration_tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'post_status' => 'pending' ] ); ?> <?php if ( $prpl_pending_celebration_tasks ) : ?> <?php $prpl_notification_count = \count( $prpl_pending_celebration_tasks ); - printf( + \printf( /* translators: %s: Number of pending celebration tasks. */ - esc_html( _n( 'Good job! You have successfully finished %s task!', 'Good job! You have successfully finished %s tasks!', $prpl_notification_count, 'progress-planner' ) ), - esc_html( number_format_i18n( $prpl_notification_count ) ) + \esc_html( \_n( 'Good job! You have successfully finished %s task!', 'Good job! You have successfully finished %s tasks!', $prpl_notification_count, 'progress-planner' ) ), + \esc_html( \number_format_i18n( $prpl_notification_count ) ) ); ?> <a class="prpl-button-primary" href="<?php echo \esc_url( \get_admin_url( null, 'admin.php?page=progress-planner' ) ); ?>"> diff --git a/views/dashboard-widgets/todo.php b/views/dashboard-widgets/todo.php index 48ab5ae78..0fa0ff1a7 100644 --- a/views/dashboard-widgets/todo.php +++ b/views/dashboard-widgets/todo.php @@ -7,7 +7,7 @@ ?> <div id="prpl-dashboard-widget-todo-header"> - <img src="<?php echo \esc_attr( constant( 'PROGRESS_PLANNER_URL' ) . '/assets/images/icon_progress_planner.svg' ); ?>" style="width:2.5em;" alt="" /> + <img src="<?php echo \esc_attr( \constant( 'PROGRESS_PLANNER_URL' ) . '/assets/images/icon_progress_planner.svg' ); ?>" style="width:2.5em;" alt="" /> <p><?php \esc_html_e( 'Keep track of all your tasks and make sure your site is up-to-date!', 'progress-planner' ); ?></p> </div> <?php diff --git a/views/js-templates/suggested-task.html b/views/js-templates/suggested-task.html new file mode 100644 index 000000000..bc95f8a0a --- /dev/null +++ b/views/js-templates/suggested-task.html @@ -0,0 +1,113 @@ +<script type="text/html" id="tmpl-prpl-suggested-task"> + <# + var providerSlug = prplTerms.getTerm( data.post[ prplTerms.provider ][0], prplTerms.provider ).slug; + var categorySlug = prplTerms.getTerm( data.post[ prplTerms.category ][0], prplTerms.category ).slug; + #> + <li class="prpl-suggested-task" data-task-id="{{ data.post.meta.prpl_task_id || data.post.id }}" data-post-id="{{ data.post.id }}" data-task-action="{{ data.action }}" data-task-url="{{ data.post.meta.prpl_url }}" data-task-provider-id="{{ providerSlug }}" data-task-points="{{ data.post.meta.prpl_points }}" data-task-category="{{ categorySlug }}" data-task-order="{{ data.post.menu_order }}"> + <# if ( data.useCheckbox ) { #> + <# if ( ! data.post.meta.prpl_dismissable ) { #> + <prpl-tooltip class="prpl-suggested-task-disabled-checkbox-tooltip"> + <slot name="open-icon"> + <input type="checkbox" class="prpl-suggested-task-checkbox" style="margin-top: 2px; pointer-events: none;" <# if ( ! data.post.meta.prpl_dismissable ) { #>disabled<# } #> <# if ( 'trash' === data.post.status || 'pending' === data.post.status ) { #>checked<# } #>> + </slot> + <slot name="content">{{{ data.l10n.disabledRRCheckboxTooltip }}}</slot> + </prpl-tooltip> + <# } else { #> + <label> + <input type="checkbox" class="prpl-suggested-task-checkbox" onchange="prplSuggestedTask.maybeComplete( {{ data.post.id }} );" style="margin-top: 2px; pointer-events: none;" <# if ( ! data.post.meta.prpl_dismissable || 'user' !== categorySlug && ( 'trash' === data.post.status || 'pending' === data.post.status ) ) { #>disabled<# } #> <# if ( 'trash' === data.post.status || 'pending' === data.post.status ) { #>checked<# } #>> + <span class="screen-reader-text">{{{ data.post.title.rendered }}}: {{ data.l10n.markAsComplete }}</span> + </label> + <# } #> + <# } #> + + <h3 style="width: 100%;"> + <span <# if ( 'user' === categorySlug ) { #>contenteditable="plaintext-only" onkeydown="prplSuggestedTask.updateTaskTitle( this );" data-post-id="{{ data.post.id }}"<# } #>><# if ( data.post.meta.prpl_url ) { #><a href="{{{ data.post.meta.prpl_url }}}" target="{{{ data.post.meta.prpl_url_target }}}">{{{ data.post.title.rendered }}}</a><# } else if ( data.post.meta.prpl_popover_id ) { #><a href="#" role="button" onclick="document.getElementById('{{{ data.post.meta.prpl_popover_id }}}')?.showPopover()">{{{ data.post.title.rendered }}}</a><# } else { #>{{{ data.post.title.rendered }}}<# } #></span> + </h3> + + <div class="prpl-suggested-task-actions"> + <div class="tooltip-actions"> + <# if ( data.post.content.rendered !== '' ) { #> + <prpl-tooltip> + <slot name="open-icon"> + <button type="button" class="prpl-suggested-task-button" data-task-id="{{ data.post.meta.prpl_task_id }}" data-task-title="{{ data.post.title.rendered }}" data-action="info" data-target="info" title="{{ data.l10n.info }}" onclick="prplSuggestedTask.runButtonAction( this );"> + <img src="{{ data.assets.infoIcon }}" alt="{{ data.l10n.info }}" class="icon"> + <span class="screen-reader-text">{{ data.l10n.info }}</span> + </button> + </slot> + <slot name="content">{{{ data.post.content.rendered }}}</slot> + </prpl-tooltip> + <# } #> + <# if ( 'user' === categorySlug ) { #> + <span class="prpl-move-buttons"> + <button type="button" class="prpl-suggested-task-button move-up" data-task-id="{{ data.post.meta.prpl_task_id }}" data-task-title="{{ data.post.title.rendered }}" data-action="move-up" data-target="move-up" title="{{ data.l10n.moveUp }}" onclick="prplSuggestedTask.runButtonAction( this );"> + <span class="dashicons dashicons-arrow-up-alt2"></span> + <span class="screen-reader-text">{{ data.l10n.moveUp }}</span> + </button> + <button type="button" class="prpl-suggested-task-button move-down" data-task-id="{{ data.post.meta.prpl_task_id }}" data-task-title="{{ data.post.title.rendered }}" data-action="move-down" data-target="move-down" title="{{ data.l10n.moveDown }}" onclick="prplSuggestedTask.runButtonAction( this );"> + <span class="dashicons dashicons-arrow-down-alt2"></span> + <span class="screen-reader-text">{{ data.l10n.moveDown }}</span> + </button> + </span> + <# } #> + + <# if ( data.post.meta.prpl_snoozable ) { #> + <prpl-tooltip class="prpl-suggested-task-snooze"> + <slot name="open-icon"> + <button type="button" class="prpl-suggested-task-button" data-task-id="{{ data.post.meta.prpl_task_id }}" data-task-title="{{ data.post.title.rendered }}" data-action="snooze" data-target="snooze" title="{{ data.l10n.snooze }}" onclick="prplSuggestedTask.runButtonAction( this );"> + <img src="{{ data.assets.snoozeIcon }}" alt="{{ data.l10n.snooze }}" class="icon"> + <span class="screen-reader-text">{{ data.l10n.snooze }}</span> + </button> + </slot> + <slot name="content"> + <fieldset> + <legend> + <span>{{ data.l10n.snoozeThisTask }}</span> + <button type="button" class="prpl-toggle-radio-group" onclick="this.closest( '.prpl-suggested-task-snooze' ).classList.toggle( 'prpl-toggle-radio-group-open' );"> + <span class="prpl-toggle-radio-group-text">{{ data.l10n.howLong }}</span> + <span class="prpl-toggle-radio-group-arrow">›</span> + </button> + </legend> + <div class="prpl-snooze-duration-radio-group"> + <# _.each( { + '1-week': data.l10n.snoozeDurationOneWeek, + '1-month': data.l10n.snoozeDurationOneMonth, + '3-months': data.l10n.snoozeDurationThreeMonths, + '6-months': data.l10n.snoozeDurationSixMonths, + '1-year': data.l10n.snoozeDurationOneYear, + 'forever': data.l10n.snoozeDurationForever, + }, function( value, key ) { #> + <label> + <input type="radio" name="snooze-duration-{{ data.post.meta.prpl_task_id }}" value="{{ key }}" onchange="prplSuggestedTask.snooze( {{ data.post.id }}, '{{ key }}' );"> + {{ value }} + </label> + <# }); #> + </div> + </fieldset> + </slot> + </prpl-tooltip> + <# } #> + + <# if ( data.post.meta.prpl_dismissable && ! data.useCheckbox ) { #> + <button type="button" class="prpl-suggested-task-button" data-task-id="{{ data.post.meta.prpl_task_id }}" data-task-title="{{ data.post.title.rendered }}" data-action="complete" data-target="complete" title="{{ data.l10n.markAsComplete }}" onclick="prplSuggestedTask.runButtonAction( this );"> + <span class="dashicons dashicons-saved"></span> + <span class="screen-reader-text">{{ data.l10n.markAsComplete }}</span> + </button> + <# } #> + + <# if ( 'user' === categorySlug ) { #> + <button type="button" class="prpl-suggested-task-button trash" data-post-id="{{ data.post.id }}" title="{{ data.l10n.delete }}" onclick="prplSuggestedTask.trash( {{ data.post.id }} );"> + <svg role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path fill="#9ca3af" d="M32.99 47.88H15.01c-3.46 0-6.38-2.7-6.64-6.15L6.04 11.49l-.72.12c-.82.14-1.59-.41-1.73-1.22-.14-.82.41-1.59 1.22-1.73.79-.14 1.57-.26 2.37-.38h.02c2.21-.33 4.46-.6 6.69-.81v-.72c0-3.56 2.74-6.44 6.25-6.55 2.56-.08 5.15-.08 7.71 0 3.5.11 6.25 2.99 6.25 6.55v.72c2.24.2 4.48.47 6.7.81.79.12 1.59.25 2.38.39.82.14 1.36.92 1.22 1.73-.14.82-.92 1.36-1.73 1.22l-.72-.12-2.33 30.24c-.27 3.45-3.18 6.15-6.64 6.15Zm-17.98-3h17.97c1.9 0 3.51-1.48 3.65-3.38l2.34-30.46c-2.15-.3-4.33-.53-6.48-.7h-.03c-5.62-.43-11.32-.43-16.95 0h-.03c-2.15.17-4.33.4-6.48.7l2.34 30.46c.15 1.9 1.75 3.38 3.65 3.38ZM24 7.01c2.37 0 4.74.07 7.11.22v-.49c0-1.93-1.47-3.49-3.34-3.55-2.5-.08-5.03-.08-7.52 0-1.88.06-3.34 1.62-3.34 3.55v.49c2.36-.15 4.73-.22 7.11-.22Zm5.49 32.26h-.06c-.83-.03-1.47-.73-1.44-1.56l.79-20.65c.03-.83.75-1.45 1.56-1.44.83.03 1.47.73 1.44 1.56l-.79 20.65c-.03.81-.7 1.44-1.5 1.44Zm-10.98 0c-.8 0-1.47-.63-1.5-1.44l-.79-20.65c-.03-.83.61-1.52 1.44-1.56.84 0 1.52.61 1.56 1.44l.79 20.65c.03.83-.61 1.52-1.44 1.56h-.06Z"></path></svg> + <span class="screen-reader-text">{{ data.l10n.delete }}</span> + </button> + <# } #> + </div> + + <# if ( data.post.meta.prpl_points ) { #> + <span class="prpl-suggested-task-points"> + +{{ data.post.meta.prpl_points }} + </span> + <# } #> + </div> + <# document.dispatchEvent( new CustomEvent( 'prpl/suggestedTask/itemInjected', { detail: data } ) ); #> + </li> +</script> \ No newline at end of file diff --git a/views/page-settings/license.php b/views/page-settings/license.php index 07dcc8ceb..0012f44c8 100644 --- a/views/page-settings/license.php +++ b/views/page-settings/license.php @@ -6,7 +6,7 @@ */ // Exit if accessed directly. -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -29,7 +29,7 @@ <?php if ( empty( $prpl_pro_license ) || 'valid' !== $prpl_pro_license_status ) : ?> <p> <?php - printf( + \printf( // translators: %s is a link to the Pro page, with the text "Progress Planner Pro". \esc_html__( 'Take part in interactive challenges to solve website problems like broken links and sharpen your skills with in-context mini courses. Upgrade to %s!', 'progress-planner' ), '<a href="https://progressplanner.com/pro/" target="_blank">Progress Planner Pro</a>' @@ -50,11 +50,11 @@ <?php if ( ! empty( $prpl_pro_license ) ) : ?> <span class="prpl-license-status prpl-license-status-<?php echo ( 'valid' === $prpl_pro_license_status ) ? 'valid' : 'invalid'; ?>"> <?php if ( 'valid' === $prpl_pro_license_status ) : ?> - <span class="prpl-license-status-valid" title="<?php esc_attr_e( 'Valid', 'progress-planner' ); ?>"> + <span class="prpl-license-status-valid" title="<?php \esc_attr_e( 'Valid', 'progress-planner' ); ?>"> <?php \progress_planner()->the_asset( 'images/icon_check_circle.svg' ); ?> </span> <?php else : ?> - <span class="prpl-license-status-invalid" title="<?php esc_attr_e( 'Invalid', 'progress-planner' ); ?>"> + <span class="prpl-license-status-invalid" title="<?php \esc_attr_e( 'Invalid', 'progress-planner' ); ?>"> <?php \progress_planner()->the_asset( 'images/icon_exclamation_circle.svg' ); ?> </span> <?php endif; ?> diff --git a/views/page-settings/pages.php b/views/page-settings/pages.php index 68284f703..dee0d2f65 100644 --- a/views/page-settings/pages.php +++ b/views/page-settings/pages.php @@ -6,7 +6,7 @@ */ // Exit if accessed directly. -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } ?> @@ -18,11 +18,11 @@ <?php \progress_planner()->the_asset( 'images/icon_pages.svg' ); ?> </span> <span> - <?php esc_html_e( 'Your pages', 'progress-planner' ); ?> + <?php \esc_html_e( 'Your pages', 'progress-planner' ); ?> </span> </h2> <p> - <?php esc_html_e( 'Let us know if you have following pages.', 'progress-planner' ); ?> + <?php \esc_html_e( 'Let us know if you have following pages.', 'progress-planner' ); ?> </p> <div class="prpl-pages-list"> <?php diff --git a/views/page-settings/post-types.php b/views/page-settings/post-types.php index 34737db34..e76b79d18 100644 --- a/views/page-settings/post-types.php +++ b/views/page-settings/post-types.php @@ -6,7 +6,7 @@ */ // Exit if accessed directly. -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -19,7 +19,7 @@ } // We use it in order to change grid layout when there are more than 5 valuable post types. -$prpl_data_attributes = 5 < count( $prpl_post_types ) ? 'data-has-many-valuable-post-types' : ''; +$prpl_data_attributes = 5 < \count( $prpl_post_types ) ? 'data-has-many-valuable-post-types' : ''; ?> <div class="prpl-column prpl-column-post-types" <?php echo \esc_attr( $prpl_data_attributes ); ?>> @@ -29,11 +29,11 @@ <?php \progress_planner()->the_asset( 'images/icon_copywriting.svg' ); ?> </span> <span> - <?php esc_html_e( 'Valuable post types', 'progress-planner' ); ?> + <?php \esc_html_e( 'Valuable post types', 'progress-planner' ); ?> </span> </h2> <p> - <?php esc_html_e( 'You\'re in control of what counts as valuable content. We\'ll track and reward activity only for the post types you select here.', 'progress-planner' ); ?> + <?php \esc_html_e( 'You\'re in control of what counts as valuable content. We\'ll track and reward activity only for the post types you select here.', 'progress-planner' ); ?> </p> <div id="prpl-post-types-include-wrapper"> <?php foreach ( $prpl_post_types as $prpl_post_type ) : ?> @@ -42,7 +42,7 @@ type="checkbox" name="prpl-post-types-include[]" value="<?php echo \esc_attr( $prpl_post_type ); ?>" - <?php checked( \in_array( $prpl_post_type, $prpl_saved_settings, true ) ); ?> + <?php \checked( \in_array( $prpl_post_type, $prpl_saved_settings, true ) ); ?> /> <?php echo \esc_html( \get_post_type_object( $prpl_post_type )->labels->name ); // @phpstan-ignore-line property.nonObject ?> </label> diff --git a/views/page-settings/settings.php b/views/page-settings/settings.php index f5038cda4..e80038127 100644 --- a/views/page-settings/settings.php +++ b/views/page-settings/settings.php @@ -6,7 +6,7 @@ */ // Exit if accessed directly. -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -29,7 +29,7 @@ id="prpl-setting-redirect-on-login" name="prpl-redirect-on-login" type="checkbox" - <?php checked( $prpl_redirect_on_login ); ?> + <?php \checked( $prpl_redirect_on_login ); ?> /> <span><?php \esc_html_e( 'Show the Progress Planner dashboard after login.', 'progress-planner' ); ?></span> </label> diff --git a/views/page-widgets/activity-scores.php b/views/page-widgets/activity-scores.php index ce08362fe..5e4395c68 100644 --- a/views/page-widgets/activity-scores.php +++ b/views/page-widgets/activity-scores.php @@ -7,7 +7,7 @@ use Progress_Planner\Base; -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -34,7 +34,7 @@ </h2> <div style="--background: var(--prpl-background-orange)"> - <prpl-gauge background="var(--prpl-background-green)" color="<?php echo esc_attr( $prpl_widget->get_gauge_color( $prpl_widget->get_score() ) ); ?>" contentFontSize="var(--prpl-font-size-6xl)"> + <prpl-gauge background="var(--prpl-background-green)" color="<?php echo \esc_attr( $prpl_widget->get_gauge_color( $prpl_widget->get_score() ) ); ?>" contentFontSize="var(--prpl-font-size-6xl)"> <progress max="100" value="<?php echo (float) $prpl_widget->get_score(); ?>"> <?php echo \esc_html( $prpl_widget->get_score() ); ?> </progress> @@ -91,7 +91,7 @@ if ( (int) $prpl_record['max_streak'] === 0 ) { \esc_html_e( 'This is the start of your first streak! Add content to your site every week and set a personal record!', 'progress-planner' ); } elseif ( (int) $prpl_record['max_streak'] <= (int) $prpl_record['current_streak'] ) { - printf( + \printf( \esc_html( /* translators: %s: number of weeks. */ \_n( @@ -104,7 +104,7 @@ \esc_html( \number_format_i18n( $prpl_record['current_streak'] ) ) ); } elseif ( 1 <= $prpl_record['current_streak'] ) { - printf( + \printf( \esc_html( /* translators: %1$s: number of weeks for the current streak. %2$s: number of weeks for the maximum streak. %3$s: The number of weeks to go in order to break the record. */ \_n( @@ -119,7 +119,7 @@ \esc_html( \number_format_i18n( $prpl_record['max_streak'] - $prpl_record['current_streak'] ) ) ); } else { - printf( + \printf( \esc_html( /* translators: %1$s: number of weeks for the maximum streak. */ \_n( diff --git a/views/page-widgets/badge-streak.php b/views/page-widgets/badge-streak.php index fa47c79ee..c48dc466c 100644 --- a/views/page-widgets/badge-streak.php +++ b/views/page-widgets/badge-streak.php @@ -5,7 +5,7 @@ * @package Progress_Planner */ -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -13,7 +13,7 @@ $prpl_widget_context_details = []; if ( $prpl_widget->get_details( 'content' ) ) { $prpl_widget_context_details['content'] = [ - 'text' => sprintf( + 'text' => \sprintf( \esc_html( /* translators: %s: The remaining number of posts or pages to write. */ \_n( @@ -29,7 +29,7 @@ } if ( $prpl_widget->get_details( 'maintenance' ) ) { $prpl_widget_context_details['maintenance'] = [ - 'text' => sprintf( + 'text' => \sprintf( \esc_html( /* translators: %s: The remaining number of weeks. */ \_n( @@ -60,13 +60,13 @@ <div class="prpl-latest-badges-wrapper"> <?php $prpl_current_context = 0; - $prpl_contexts_count = count( array_keys( $prpl_widget_context_details ) ); + $prpl_contexts_count = \count( \array_keys( $prpl_widget_context_details ) ); ?> <?php foreach ( $prpl_widget_context_details as $prpl_context => $prpl_details ) : ?> <?php ++$prpl_current_context; ?> <prpl-gauge background="<?php echo \esc_attr( $prpl_widget->get_details( $prpl_context )->get_background() ); ?>" color="var(--prpl-color-accent-orange)"> <progress max="100" value="<?php echo (float) $prpl_widget->get_details( $prpl_context )->get_progress()['progress']; ?>"> - <prpl-badge complete="true" badge-id="<?php echo esc_attr( $prpl_widget->get_details( $prpl_context )->get_id() ); ?>"></prpl-badge> + <prpl-badge complete="true" badge-id="<?php echo \esc_attr( $prpl_widget->get_details( $prpl_context )->get_id() ); ?>"></prpl-badge> </progress> </prpl-gauge> <div class="prpl-badge-content-wrapper"> @@ -79,7 +79,7 @@ <?php endforeach; ?> </div> -<h3><?php esc_html_e( 'Your achievements', 'progress-planner' ); ?></h3> +<h3><?php \esc_html_e( 'Your achievements', 'progress-planner' ); ?></h3> <div class="prpl-badges-container-achievements"> <?php foreach ( [ 'content', 'maintenance' ] as $prpl_badge_group ) : @@ -97,7 +97,7 @@ class="prpl-badge" > <prpl-badge complete="<?php echo $prpl_badge_completed ? 'true' : 'false'; ?>" - badge-id="<?php echo esc_attr( $prpl_badge->get_id() ); ?>" + badge-id="<?php echo \esc_attr( $prpl_badge->get_id() ); ?>" ></prpl-badge> <p><?php echo \esc_html( $prpl_badge->get_name() ); ?></p> </span> diff --git a/views/page-widgets/challenge.php b/views/page-widgets/challenge.php index d85b4d622..00990b38e 100644 --- a/views/page-widgets/challenge.php +++ b/views/page-widgets/challenge.php @@ -5,7 +5,7 @@ * @package Progress_Planner */ -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -19,5 +19,5 @@ </h2> <div class="prpl-challenge-content"> - <?php echo \wp_kses_post( str_replace( '{{admin_url}}', \admin_url(), $prpl_challenge['content'] ) ); ?> + <?php echo \wp_kses_post( \str_replace( '{{admin_url}}', \admin_url(), $prpl_challenge['content'] ) ); ?> </div> diff --git a/views/page-widgets/content-activity.php b/views/page-widgets/content-activity.php index 8763c7711..298178eff 100644 --- a/views/page-widgets/content-activity.php +++ b/views/page-widgets/content-activity.php @@ -5,7 +5,7 @@ * @package Progress_Planner */ -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -13,15 +13,15 @@ $prpl_activity_types = [ 'publish' => [ - 'label' => __( 'Content published', 'progress-planner' ), + 'label' => \__( 'Content published', 'progress-planner' ), 'color' => 'var(--prpl-color-accent-green)', ], 'update' => [ - 'label' => __( 'Content updated', 'progress-planner' ), + 'label' => \__( 'Content updated', 'progress-planner' ), 'color' => 'var(--prpl-color-accent-purple)', ], 'delete' => [ - 'label' => __( 'Content deleted', 'progress-planner' ), + 'label' => \__( 'Content deleted', 'progress-planner' ), 'color' => 'var(--prpl-color-accent-red)', ], ]; @@ -31,8 +31,7 @@ 'all' => 0, ]; -foreach ( array_keys( $prpl_activity_types ) as $prpl_activity_type ) { - +foreach ( \array_keys( $prpl_activity_types ) as $prpl_activity_type ) { // Default count. $prpl_activities_count[ $prpl_activity_type ] = 0; @@ -47,19 +46,18 @@ ); if ( $prpl_activities ) { - if ( 'delete' !== $prpl_activity_type ) { // Filter the activities to only include the tracked post types. - $prpl_activities = array_filter( + $prpl_activities = \array_filter( $prpl_activities, function ( $activity ) use ( $prpl_tracked_post_types ) { - return in_array( get_post_type( $activity->data_id ), $prpl_tracked_post_types, true ); + return \in_array( \get_post_type( $activity->data_id ), $prpl_tracked_post_types, true ); } ); } // Update the count. - $prpl_activities_count[ $prpl_activity_type ] = count( $prpl_activities ); + $prpl_activities_count[ $prpl_activity_type ] = \count( $prpl_activities ); } $prpl_activities_count['all'] += $prpl_activities_count[ $prpl_activity_type ]; diff --git a/views/page-widgets/latest-badge.php b/views/page-widgets/latest-badge.php index d03be0b76..a6cf61954 100644 --- a/views/page-widgets/latest-badge.php +++ b/views/page-widgets/latest-badge.php @@ -5,7 +5,7 @@ * @package Progress_Planner */ -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -20,11 +20,11 @@ <p><?php \esc_html_e( 'You haven\'t unlocked any badges yet. Hang on, you\'ll get there!', 'progress-planner' ); ?></p> <?php else : ?> <p> - <?php if ( str_starts_with( $prpl_latest_badge->get_id(), 'monthly-' ) ) : ?> + <?php if ( \str_starts_with( $prpl_latest_badge->get_id(), 'monthly-' ) ) : ?> <?php \esc_html_e( 'Wooohoooo! Congratulations! You have earned a new badge.', 'progress-planner' ); ?> <?php else : ?> <?php - printf( + \printf( /* translators: %s: The badge name. */ \esc_html__( 'Wooohoooo! Congratulations! You have earned a new badge. You are now a %s.', 'progress-planner' ), '<strong>' . \esc_html( $prpl_latest_badge->get_name() ) . '</strong>' diff --git a/views/page-widgets/parts/monthly-badges.php b/views/page-widgets/parts/monthly-badges.php index 67dcb3e8d..80b14e6df 100644 --- a/views/page-widgets/parts/monthly-badges.php +++ b/views/page-widgets/parts/monthly-badges.php @@ -7,7 +7,7 @@ use Progress_Planner\Badges\Monthly; -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } $prpl_location = ''; @@ -17,13 +17,13 @@ $prpl_css_class = \esc_attr( $args['css_class'] ); } -$prpl_location = false !== strpos( $prpl_css_class, 'in-popover' ) ? 'popover' : 'suggested-tasks'; -$prpl_badges_year = (int) isset( $args['badges_year'] ) ? $args['badges_year'] : gmdate( 'Y' ); +$prpl_location = false !== \strpos( $prpl_css_class, 'in-popover' ) ? 'popover' : 'suggested-tasks'; +$prpl_badges_year = (int) isset( $args['badges_year'] ) ? $args['badges_year'] : \gmdate( 'Y' ); ?> <div class="prpl-widget-wrapper <?php echo \esc_attr( $prpl_css_class ); ?>"> <h3 class="prpl-widget-title"> <?php - printf( + \printf( /* translators: %d: year */ \esc_html__( 'Monthly badges %d', 'progress-planner' ), \esc_html( (string) $prpl_badges_year ) @@ -35,12 +35,11 @@ <?php if ( $prpl_badges ) : ?> <?php $prpl_badges_per_row = 3; - $prpl_badges_count = count( $prpl_badges ); + $prpl_badges_count = \count( $prpl_badges ); $prpl_scroll_to_row = 1; $prpl_current_month_badge_id = Monthly::get_badge_id_from_date( new \DateTime() ); if ( 'popover' !== $prpl_location ) { - - $prpl_total_rows = (int) ceil( $prpl_badges_count / $prpl_badges_per_row ); + $prpl_total_rows = (int) \ceil( $prpl_badges_count / $prpl_badges_per_row ); // We need to know current month badge position. $prpl_current_month_position = 1; @@ -52,7 +51,7 @@ } } - $prpl_scroll_to_row = (int) ceil( $prpl_current_month_position / $prpl_badges_per_row ); + $prpl_scroll_to_row = (int) \ceil( $prpl_current_month_position / $prpl_badges_per_row ); // Always display the previous row, so user can see already completed badges. if ( 1 < $prpl_scroll_to_row ) { @@ -85,7 +84,7 @@ class="prpl-badge prpl-badge-<?php echo \esc_attr( $prpl_badge->get_id() ); ?>" > <prpl-badge complete="<?php echo 100 === (int) $prpl_badge->progress_callback()['progress'] ? 'true' : 'false'; ?>" - badge-id="<?php echo esc_attr( $prpl_badge->get_id() ); ?>" + badge-id="<?php echo \esc_attr( $prpl_badge->get_id() ); ?>" ></prpl-badge> <p><?php echo \esc_html( $prpl_badge->get_name() ); ?></p> </span> diff --git a/views/page-widgets/suggested-tasks.php b/views/page-widgets/suggested-tasks.php index d2a1e0e6a..7f0f38193 100644 --- a/views/page-widgets/suggested-tasks.php +++ b/views/page-widgets/suggested-tasks.php @@ -7,7 +7,7 @@ use Progress_Planner\Badges\Monthly; -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -15,7 +15,6 @@ $prpl_badge = \progress_planner()->get_badges()->get_badge( Monthly::get_badge_id_from_date( new \DateTime() ) ); ?> - <div class="prpl-dashboard-widget-suggested-tasks"> <h2 class="prpl-widget-title"> <?php \esc_html_e( 'Ravi\'s Recommendations', 'progress-planner' ); ?> @@ -25,7 +24,10 @@ </p> <ul style="display:none"></ul> - <ul class="prpl-suggested-tasks-list"></ul> + <ul id="prpl-suggested-tasks-list" class="prpl-suggested-tasks-list"></ul> + <p class="prpl-suggested-tasks-loading"> + <?php \esc_html_e( 'Loading tasks...', 'progress-planner' ); ?> + </p> <p class="prpl-no-suggested-tasks"> <?php \esc_html_e( 'You have completed all recommended tasks.', 'progress-planner' ); ?> <br> @@ -44,21 +46,64 @@ background="var(--prpl-background-orange)" color="var(--prpl-color-accent-orange)" data-max="<?php echo (int) Monthly::TARGET_POINTS; ?>" - data-value="<?php echo (float) $prpl_widget->get_score(); ?>" - data-badge-id="<?php echo esc_attr( $prpl_badge->get_id() ); ?>" + data-value="<?php echo (float) $prpl_widget->get_score()['target_score']; ?>" + data-badge-id="<?php echo \esc_attr( $prpl_badge->get_id() ); ?>" > - <progress max="<?php echo (int) Monthly::TARGET_POINTS; ?>" value="<?php echo (float) $prpl_widget->get_score(); ?>"> - <prpl-badge complete="true" badge-id="<?php echo esc_attr( $prpl_badge->get_id() ); ?>"></prpl-badge> + <progress max="<?php echo (int) Monthly::TARGET_POINTS; ?>" value="<?php echo (float) $prpl_widget->get_score()['target_score']; ?>"> + <prpl-badge complete="true" badge-id="<?php echo \esc_attr( $prpl_badge->get_id() ); ?>"></prpl-badge> </progress> </prpl-gauge> <div class="prpl-widget-content-points"> <span><?php \esc_html_e( 'Progress monthly badge', 'progress-planner' ); ?></span> <span id="prpl-widget-content-ravi-points-number" class="prpl-widget-content-points-number"> - <?php echo (int) $prpl_widget->get_score(); ?>pt + <?php echo (int) $prpl_widget->get_score()['target_score']; ?>pt </span> </div> + <?php if ( ! empty( $prpl_widget->get_previous_incomplete_months_badges() ) ) : ?> + <hr> + + <?php + $prpl_remaining_points = $prpl_widget->get_score()['target'] - $prpl_widget->get_score()['target_score']; + $prpl_days_remaining = (int) \gmdate( 't' ) - (int) \gmdate( 'j' ); + ?> + <div class="prpl-previous-month-badge-progress-bars-wrapper"> + <h3><?php \esc_html_e( 'Oh no! You missed the previous monthly badge!', 'progress-planner' ); ?></h3> + <p><?php echo \wp_kses( \__( 'No worries though! <strong>Collect the surplus of points</strong> you earn, and get your badge!', 'progress-planner' ), [ 'strong' => [] ] ); ?></p> + <?php foreach ( $prpl_widget->get_previous_incomplete_months_badges() as $prpl_previous_incomplete_month_badge ) : ?> + <?php $prpl_remaining_points += $prpl_previous_incomplete_month_badge->progress_callback()['remaining']; ?> + <div + class="prpl-previous-month-badge-progress-bar-wrapper" + style="padding: 1rem 0; background-color: var(--prpl-background-orange); border-radius: 0.5rem; padding: 1rem;" + data-badge-id="<?php echo \esc_attr( $prpl_previous_incomplete_month_badge->get_id() ); ?>" + > + <prpl-badge-progress-bar + data-badge-id="<?php echo \esc_attr( $prpl_previous_incomplete_month_badge->get_id() ); ?>" + data-points="<?php echo (int) $prpl_previous_incomplete_month_badge->progress_callback()['points']; ?>" + data-max-points="<?php echo (int) Monthly::TARGET_POINTS; ?>" + ></prpl-badge-progress-bar> + + <div class="prpl-widget-content-points"> + <span class="prpl-widget-previous-ravi-points-number" class="prpl-widget-content-points-number"> + <?php echo (int) $prpl_previous_incomplete_month_badge->progress_callback()['points']; ?>pt + </span> + <span class="prpl-previous-month-badge-progress-bar-remaining" data-remaining="<?php echo (int) $prpl_previous_incomplete_month_badge->progress_callback()['remaining']; ?>"> + <?php + \printf( + /* translators: %d: The number of points. */ + \esc_html__( '%1$d more points to go in the next %2$d days', 'progress-planner' ), + (int) $prpl_remaining_points, + (int) $prpl_days_remaining + ); + ?> + </span> + </div> + </div> + <?php endforeach; ?> + </div> + <?php endif; ?> + <hr> <?php endif; ?> diff --git a/views/page-widgets/todo.php b/views/page-widgets/todo.php index 63af50435..406c28e69 100644 --- a/views/page-widgets/todo.php +++ b/views/page-widgets/todo.php @@ -5,7 +5,7 @@ * @package Progress_Planner */ -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -40,4 +40,5 @@ </prpl-tooltip> </span> </p> + <?php \progress_planner()->get_admin__widgets__todo()->the_todo_list(); ?> diff --git a/views/page-widgets/whats-new.php b/views/page-widgets/whats-new.php index a66ec2b96..986b765a7 100644 --- a/views/page-widgets/whats-new.php +++ b/views/page-widgets/whats-new.php @@ -5,7 +5,7 @@ * @package Progress_Planner */ -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } diff --git a/views/popovers/badge-streak.php b/views/popovers/badge-streak.php index df000a4cc..3de14c55b 100644 --- a/views/popovers/badge-streak.php +++ b/views/popovers/badge-streak.php @@ -6,7 +6,7 @@ */ // Exit if accessed directly. -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } ?> diff --git a/views/popovers/email-sending.php b/views/popovers/email-sending.php new file mode 100644 index 000000000..8f0678522 --- /dev/null +++ b/views/popovers/email-sending.php @@ -0,0 +1,211 @@ +<?php +/** + * Popover for the email-sending task. + * + * @package Progress_Planner + */ + +// Exit if accessed directly. +if ( ! \defined( 'ABSPATH' ) ) { + exit; +} + +?> + +<prpl-email-test-popup + popover-id="<?php echo \esc_attr( 'prpl-popover-' . $prpl_popover_id ); ?>" + provider-id="<?php echo \esc_attr( $prpl_provider_id ); ?>" +> + <?php /* First step */ ?> + <div class="prpl-columns-wrapper-flex prpl-sending-email-step" id="prpl-sending-email-form-step"> + <div class="prpl-column prpl-column-content"> + <h2 class="prpl-interactive-task-title"><?php \esc_html_e( 'Test if your site can send emails', 'progress-planner' ); ?></h2> + <p class="prpl-interactive-task-description"><?php \esc_html_e( 'Your WordPress site sometimes needs to send emails. For example, to reset a password, send a comment notification, or warn you when something breaks. Contact forms also use email.', 'progress-planner' ); ?></p> + <p class="prpl-interactive-task-description"><?php \esc_html_e( 'It’s important to check if these emails are actually sent. Enter your email address on the right to get a test email.', 'progress-planner' ); ?></p> + </div> + <div class="prpl-column"> + <p><?php \esc_html_e( 'Where should we send the test email?', 'progress-planner' ); ?></p> + <div class="prpl-note"> + <span class="prpl-note-icon"> + <?php \progress_planner()->the_asset( 'images/icon_exclamation_triangle_solid.svg' ); ?> + </span> + <span class="prpl-note-text"> + <?php \esc_html_e( 'You should get the email in a few minutes. In rare cases, it might take a few hours.', 'progress-planner' ); ?> + </span> + </div> + <form id="prpl-sending-email-form" onsubmit="return false;"> + <input type="email" id="prpl-sending-email-address" placeholder="<?php \esc_html_e( 'Enter your e-mail address', 'progress-planner' ); ?>" value="<?php echo \esc_attr( \wp_get_current_user()->user_email ); ?>" /> + + <div class="prpl-steps-nav-wrapper"> + <button class="prpl-button" data-action="showResults" type="submit"> + <?php + /* translators: %s is a forward arrow icon. */ + \printf( \esc_html__( 'Next step %s', 'progress-planner' ), '<span class="dashicons dashicons-arrow-right-alt2"></span>' ); + ?> + </button> + </div> + </form> + </div> + </div> + + <?php /* We detected an error during sending test email, showing error message */ ?> + <div class="prpl-columns-wrapper-flex prpl-sending-email-step" id="prpl-sending-email-error-occurred-step" style="display: none;"> + <div class="prpl-column prpl-column-content"> + <h2 class="prpl-interactive-task-title"><?php \esc_html_e( 'We tried to send a test email', 'progress-planner' ); ?></h2> + <p class="prpl-interactive-task-description" id="prpl-sending-email-error-occurred-message" data-email-message=" + <?php + \printf( + /* translators: %s is the email subject. */ + \esc_attr__( 'We just tried to send the email "%s" to [EMAIL_ADDRESS], but unfortunately it didn’t work.', 'progress-planner' ), + \esc_attr( $prpl_email_subject ) + ); + ?> + "></p> + + </div> + + <div class="prpl-column"> + <div class="prpl-note prpl-note-error"> + <span class="prpl-note-icon"> + <?php \progress_planner()->the_asset( 'images/icon_exclamation_circle_solid.svg' ); ?> + </span> + <span class="prpl-note-text" data-email-message=" + <?php + /* translators: %s is the error message. */ + \printf( \esc_attr__( 'The test email didn’t work. The error message was: [ERROR_MESSAGE]', 'progress-planner' ), \esc_attr( $prpl_email_error ) ); + ?> + "> + </span> + </div> + + <p> + <?php + \printf( + /* translators: %s is a link to the troubleshooting guide. */ + \esc_html__( 'There are a few common reasons why your email might not be sending. Check the %s to find out what’s causing the issue and how to fix it.', 'progress-planner' ), + '<a href="' . \esc_url( $prpl_troubleshooting_guide_url ) . '" target="_blank">' . \esc_html__( 'troubleshooting guide', 'progress-planner' ) . '</a>' + ); + ?> + </p> + + <div class="prpl-steps-nav-wrapper"> + <button class="prpl-button" data-action="showForm"> + <?php + /* translators: %s is a back arrow icon. */ + \printf( \esc_html__( ' %s Try again', 'progress-planner' ), '<span class="dashicons dashicons-arrow-left-alt2"></span>' ); + ?> + </button> + <button class="prpl-button" data-action="closePopover"><?php \esc_html_e( 'Retry later', 'progress-planner' ); ?></button> + </div> + </div> + </div> + + <?php /* Email sent, asking user if they received it */ ?> + <div class="prpl-columns-wrapper-flex prpl-sending-email-step" id="prpl-sending-email-result-step" style="display: none;"> + <div class="prpl-column prpl-column-content"> + <h2 class="prpl-interactive-task-title"><?php \esc_html_e( 'We sent a test email', 'progress-planner' ); ?></h2> + <p class="prpl-interactive-task-description" id="prpl-sending-email-sent-message" data-email-message=" + <?php + /* translators: %s is the email subject. */ + \printf( \esc_attr__( 'We just sent the email "%s" to [EMAIL_ADDRESS].', 'progress-planner' ), \esc_attr( $prpl_email_subject ) ); + ?> + "></p> + + </div> + + <div class="prpl-column"> + <p><?php \esc_html_e( 'Did you get the test email?', 'progress-planner' ); ?></p> + <div class="prpl-note"> + <span class="prpl-note-icon"> + <?php \progress_planner()->the_asset( 'images/icon_exclamation_triangle_solid.svg' ); ?> + </span> + <span class="prpl-note-text"> + <?php \esc_html_e( 'You should get the email in a few minutes. In rare cases, it might take a few hours.', 'progress-planner' ); ?> + </span> + </div> + <div class="radios"> + <div class="prpl-radio-wrapper"> + <label for="prpl-sending-email-result-yes" class="prpl-custom-radio"> + <input + type="radio" + id="prpl-sending-email-result-yes" + name="prpl-sending-email-result" + data-action="showSuccess" + > + <span class="prpl-custom-control"></span> + <?php \esc_html_e( 'Yes', 'progress-planner' ); ?> + </label> + </div> + <div class="prpl-radio-wrapper"> + <label for="prpl-sending-email-result-no" class="prpl-custom-radio"> + <input + type="radio" + id="prpl-sending-email-result-no" + name="prpl-sending-email-result" + data-action="showTroubleshooting" + > + <span class="prpl-custom-control"></span> + <?php \esc_html_e( 'No', 'progress-planner' ); ?> + </label> + </div> + </div> + + <div class="prpl-steps-nav-wrapper"> + <button class="prpl-button" data-action=""> + <?php + /* translators: %s is an arrow icon. */ + \printf( \esc_html__( 'Next step %s', 'progress-planner' ), '<span class="dashicons dashicons-arrow-right-alt2"></span>' ); + ?> + </button> + </div> + </div> + </div> + + <?php /* Email received, showing success message */ ?> + <div class="prpl-columns-wrapper-flex prpl-sending-email-step" id="prpl-sending-email-success-step" style="display: none;"> + <div class="prpl-column prpl-column-content"> + <h2 class="prpl-interactive-task-title"><?php \esc_html_e( 'Your email is set up properly!', 'progress-planner' ); ?></h2> + <?php \esc_html_e( 'Great, you received the test email! This indicates email is set up properly on your website.', 'progress-planner' ); ?> + </div> + + <div class="prpl-column"> + <p><?php \esc_html_e( 'Celebrate this achievement!', 'progress-planner' ); ?></p> + + <div class="prpl-steps-nav-wrapper"> + <button class="prpl-button" data-action="completeTask"><?php \esc_html_e( 'Collect your point!', 'progress-planner' ); ?></button> + </div> + </div> + </div> + + <?php /* Email not received, showing troubleshooting */ ?> + <div class="prpl-columns-wrapper-flex prpl-sending-email-step" id="prpl-sending-email-troubleshooting-step" style="display: none;"> + <div class="prpl-column prpl-column-content"> + <h2 class="prpl-interactive-task-title"><?php \esc_html_e( 'Your email might not be working well', 'progress-planner' ); ?></h2> + <p class="prpl-interactive-task-description"> + <?php \esc_html_e( 'We\'re sorry to hear you did not receive our confirmation email yet. On some websites, it make take up to a few hours to send email. That\'s why we strongly advise you to check back in a few hours from now.', 'progress-planner' ); ?> + </p> + <p class="prpl-interactive-task-description"><?php \esc_html_e( 'If you already waited a couple of hours and you still didn\'t get our email, your email might not be working well.', 'progress-planner' ); ?> + </p> + </div> + + <div class="prpl-column"> + <?php if ( $prpl_is_there_sending_email_override ) : ?> + <p><?php \esc_html_e( 'What can you do next? Well, it looks like you are already running an SMTP plugin on your website, but it might not be configured correctly.', 'progress-planner' ); ?></p> + <p><?php \esc_html_e( 'You can find more information about running an SMTP plugin in our troubleshooting guide.', 'progress-planner' ); ?></p> + <?php else : ?> + <p><?php \esc_html_e( 'What can you do next? If you haven\'t already, you may need to install a plugin to handle email for you (an SMTP plugin).', 'progress-planner' ); ?></p> + <p><?php \esc_html_e( 'You can find more information about installing an SMTP plugin in our troubleshooting guide.', 'progress-planner' ); ?></p> + <?php endif; ?> + + <div class="prpl-steps-nav-wrapper"> + <button class="prpl-button" data-action="openTroubleshootingGuide"><?php \esc_html_e( 'Take me to your troubleshooting guide', 'progress-planner' ); ?></button> + </div> + </div> + </div> + + <button class="prpl-popover-close" data-action="closePopover"> + <span class="dashicons dashicons-no-alt"></span> + <span class="screen-reader-text"><?php \esc_html_e( 'Close', 'progress-planner' ); ?></span> + </button> + +</prpl-email-test-popup> \ No newline at end of file diff --git a/views/popovers/monthly-badges.php b/views/popovers/monthly-badges.php index d2887e286..47e68a935 100644 --- a/views/popovers/monthly-badges.php +++ b/views/popovers/monthly-badges.php @@ -6,7 +6,7 @@ */ // Exit if accessed directly. -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } ?> @@ -39,8 +39,8 @@ <div class="prpl-popover-column"> <?php $prpl_badges_groups = [ - 'content' => __( 'Writing badges', 'progress-planner' ), - 'maintenance' => __( 'Streak badges', 'progress-planner' ), + 'content' => \__( 'Writing badges', 'progress-planner' ), + 'maintenance' => \__( 'Streak badges', 'progress-planner' ), ]; ?> <?php foreach ( $prpl_badges_groups as $prpl_badge_group => $prpl_widget_title ) : ?> @@ -62,7 +62,7 @@ class="prpl-badge" > <prpl-badge complete="<?php echo $prpl_badge_completed ? 'true' : 'false'; ?>" - badge-id="<?php echo esc_attr( $prpl_badge->get_id() ); ?>" + badge-id="<?php echo \esc_attr( $prpl_badge->get_id() ); ?>" ></prpl-badge> <p><?php echo \esc_html( $prpl_badge->get_name() ); ?></p> </span> diff --git a/views/popovers/parts/badge-streak-badge.php b/views/popovers/parts/badge-streak-badge.php index 7e5886fd2..4ebfdf678 100644 --- a/views/popovers/parts/badge-streak-badge.php +++ b/views/popovers/parts/badge-streak-badge.php @@ -6,7 +6,7 @@ */ // Exit if accessed directly. -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } ?> @@ -19,7 +19,7 @@ class="prpl-badge" <div class="inner"> <prpl-badge complete="<?php echo 100 === (int) $prpl_badge_progress['progress'] ? 'true' : 'false'; ?>" - badge-id="<?php echo esc_attr( $prpl_badge->get_id() ); ?>" + badge-id="<?php echo \esc_attr( $prpl_badge->get_id() ); ?>" ></prpl-badge> <?php echo \esc_html( $prpl_badge->get_name() ); ?> </div> diff --git a/views/popovers/parts/badge-streak-progressbar.php b/views/popovers/parts/badge-streak-progressbar.php index ed51c1f38..936985bde 100644 --- a/views/popovers/parts/badge-streak-progressbar.php +++ b/views/popovers/parts/badge-streak-progressbar.php @@ -6,7 +6,7 @@ */ // Exit if accessed directly. -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -18,7 +18,7 @@ ?> <div class="progress-badges"> <span class="badges-popover-progress-total"> - <span style="width: <?php echo (int) end( $prpl_badges )->get_progress()['progress']; ?>%"></span> + <span style="width: <?php echo (int) \end( $prpl_badges )->get_progress()['progress']; ?>%"></span> </span> <div class="indicators"> <?php foreach ( $prpl_badges as $prpl_badge ) : ?> @@ -29,11 +29,11 @@ ✔️ <?php else : ?> <?php - printf( + \printf( 'content' === $prpl_context // @phpstan-ignore-line variable.undefined ? \esc_html( /* translators: The number of weeks remaining to complete the badge. */ - _n( + \_n( '%s post to go', '%s posts to go', (int) $prpl_badge_progress['remaining'], @@ -41,7 +41,7 @@ ) ) : \esc_html( /* translators: The number of weeks remaining to complete the badge. */ - _n( + \_n( '%s week to go', '%s weeks to go', (int) $prpl_badge_progress['remaining'], diff --git a/views/popovers/parts/icon.php b/views/popovers/parts/icon.php index 357283534..fc08dafa4 100644 --- a/views/popovers/parts/icon.php +++ b/views/popovers/parts/icon.php @@ -5,7 +5,7 @@ * @package Progress_Planner */ -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } diff --git a/views/popovers/parts/upgrade-tasks.php b/views/popovers/parts/upgrade-tasks.php index bbc9920a5..70265b906 100644 --- a/views/popovers/parts/upgrade-tasks.php +++ b/views/popovers/parts/upgrade-tasks.php @@ -7,7 +7,7 @@ use Progress_Planner\Badges\Monthly; -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -50,27 +50,46 @@ 'category' => $prpl_task_provider->get_provider_category(), ]; - $prpl_task_completed = $prpl_task_provider->evaluate_task( $prpl_task_data['task_id'] ); - $prpl_task_details = $prpl_task_provider->get_task_details(); + // Note: get_post() returns a formatted array (details), not an object. + $prpl_task = \progress_planner()->get_suggested_tasks_db()->get_post( $prpl_task_data['task_id'] ); - // If the task is completed, mark it as pending celebration. - if ( $prpl_task_completed ) { + /** + * Most tasks are already added, but the "completed" tasks are not - since Tasks::should_add_task() returns false for them. + * We need to add them manually. + */ + if ( ! $prpl_task ) { + $prpl_task_post_id = \progress_planner()->get_suggested_tasks_db()->add( $prpl_task_provider->get_task_details( $prpl_task_data ) ); - // Add the task to the pending tasks. - \progress_planner()->get_suggested_tasks()->get_tasks_manager()->add_pending_task( $prpl_task_data ); + // Something went wrong, skip this task. + if ( ! $prpl_task_post_id ) { + continue; + } - // Change the task status to pending celebration. - \progress_planner()->get_suggested_tasks()->mark_task_as( 'pending_celebration', $prpl_task_data['task_id'] ); + // Note: get_post() returns a formatted array (details), not an object. + $prpl_task = \progress_planner()->get_suggested_tasks_db()->get_post( $prpl_task_post_id ); + } + + // Something went wrong, skip this task. + if ( ! $prpl_task ) { + continue; + } + + $prpl_task_completed = $prpl_task_provider->evaluate_task( $prpl_task_data['task_id'] ); + + // If the task is completed, mark it as pending. + if ( $prpl_task_completed ) { + // Change the task status to pending. + \progress_planner()->get_suggested_tasks_db()->update_recommendation( $prpl_task->ID, [ 'post_status' => 'pending' ] ); // Insert an activity. \progress_planner()->get_suggested_tasks()->insert_activity( $prpl_task_data['task_id'] ); } ?> <li class="prpl-onboarding-task" data-prpl-task-completed="<?php echo $prpl_task_completed ? 'true' : 'false'; ?>"> - <h3><?php echo \esc_html( $prpl_task_details['title'] ); ?></h3> + <h3><?php echo \esc_html( $prpl_task->post_title ); ?></h3> <span class="prpl-onboarding-task-status"> <span class="prpl-suggested-task-points"> - +<?php echo \esc_html( $prpl_task_details['points'] ); ?> + +<?php echo \esc_html( (string) $prpl_task->points ); ?> </span> <span class="prpl-suggested-task-loader"></span> <span class="icon icon-check-circle"> @@ -92,7 +111,7 @@ <img src="<?php echo \esc_url( \progress_planner()->get_remote_server_root_url() . '/wp-json/progress-planner-saas/v1/badge-svg/?badge_id=' . \esc_attr( $prpl_badge->get_id() ) ); ?>" alt="<?php \esc_attr_e( 'Badge', 'progress-planner' ); ?>" - onerror="this.onerror=null;this.src='<?php echo esc_url( \progress_planner()->get_placeholder_svg() ); ?>';" + onerror="this.onerror=null;this.src='<?php echo \esc_url( \progress_planner()->get_placeholder_svg() ); ?>';" /> </span> <?php \esc_html_e( 'These tasks contribute to your monthly badge—every check completed brings you closer!', 'progress-planner' ); ?> diff --git a/views/popovers/popover.php b/views/popovers/popover.php index d280255e4..d5b542fad 100644 --- a/views/popovers/popover.php +++ b/views/popovers/popover.php @@ -6,7 +6,7 @@ */ // Exit if accessed directly. -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } ?> diff --git a/views/popovers/subscribe-form.php b/views/popovers/subscribe-form.php index 1cd1228f2..c59abb16f 100644 --- a/views/popovers/subscribe-form.php +++ b/views/popovers/subscribe-form.php @@ -6,7 +6,7 @@ */ // Exit if accessed directly. -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -18,7 +18,7 @@ <form id="prpl-settings-license-form"> <p> <?php - printf( + \printf( /* translators: %s: progressplanner.com link */ \esc_html__( 'We can send you weekly emails with your own to-do’s, your activity stats and nudges to keep you working on your site. To do this, we’ll create an account for you on %s.', 'progress-planner' ), '<a href="https://prpl.fyi/home" target="_blank">progressplanner.com</a>' diff --git a/views/popovers/upgrade-tasks.php b/views/popovers/upgrade-tasks.php index 159b95a94..85a6396c5 100644 --- a/views/popovers/upgrade-tasks.php +++ b/views/popovers/upgrade-tasks.php @@ -8,7 +8,7 @@ use Progress_Planner\Badges\Monthly; // Exit if accessed directly. -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } diff --git a/views/setting/page-select.php b/views/setting/page-select.php index 94644d4a9..5196ffcf4 100644 --- a/views/setting/page-select.php +++ b/views/setting/page-select.php @@ -6,7 +6,7 @@ */ // Exit if accessed directly. -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -16,15 +16,15 @@ $prpl_select_value = 0; $prpl_radio_value = ( '_no_page_needed' === $prpl_setting_value ) ? 'not-applicable' : 'no'; -if ( is_numeric( $prpl_setting_value ) && 0 < $prpl_setting_value ) { +if ( \is_numeric( $prpl_setting_value ) && 0 < $prpl_setting_value ) { $prpl_radio_value = 'yes'; $prpl_select_value = (int) $prpl_setting_value; } ?> <div - class="prpl-pages-item prpl-pages-item-<?php echo esc_attr( $prpl_setting['page'] ); ?>" - data-page-item="<?php echo esc_attr( $prpl_setting['page'] ); ?>" + class="prpl-pages-item prpl-pages-item-<?php echo \esc_attr( $prpl_setting['page'] ); ?>" + data-page-item="<?php echo \esc_attr( $prpl_setting['page'] ); ?>" > <div class="item-description"> <h3> @@ -35,19 +35,19 @@ class="prpl-pages-item prpl-pages-item-<?php echo esc_attr( $prpl_setting['page' <?php \progress_planner()->the_asset( 'images/icon_exclamation_circle.svg' ); ?> </span> <span> - <?php echo esc_html( $prpl_setting['title'] ); ?> + <?php echo \esc_html( $prpl_setting['title'] ); ?> </span> </h3> - <p><?php echo esc_html( $prpl_setting['description'] ); ?></p> + <p><?php echo \esc_html( $prpl_setting['description'] ); ?></p> </div> <div> - <fieldset id="prpl-setting-fieldset-<?php echo esc_attr( $prpl_setting['id'] ); ?>"> + <fieldset id="prpl-setting-fieldset-<?php echo \esc_attr( $prpl_setting['id'] ); ?>"> <div class="radios"> <?php foreach ( [ - 'yes' => esc_html__( 'I have this page', 'progress-planner' ), - 'no' => esc_html__( 'I don\'t have this page yet', 'progress-planner' ), - 'not-applicable' => esc_html__( 'My site doesn\'t need this page', 'progress-planner' ), + 'yes' => \esc_html__( 'I have this page', 'progress-planner' ), + 'no' => \esc_html__( 'I don\'t have this page yet', 'progress-planner' ), + 'not-applicable' => \esc_html__( 'My site doesn\'t need this page', 'progress-planner' ), ] as $prpl_r_value => $prpl_r_label ) : $prpl_radio_checked = ( 'no' === $prpl_r_value && 'no' === $prpl_setting['isset'] ); if ( ! $prpl_radio_checked ) { @@ -58,24 +58,24 @@ class="prpl-pages-item prpl-pages-item-<?php echo esc_attr( $prpl_setting['page' <label> <input type="radio" - id="<?php echo esc_attr( 'pages[' . esc_attr( $prpl_setting['id'] ) . '][have_page]' ); ?>" - name="<?php echo esc_attr( 'pages[' . esc_attr( $prpl_setting['id'] ) . '][have_page]' ); ?>" - value="<?php echo esc_attr( $prpl_r_value ); ?>" - data-page="<?php echo esc_attr( $prpl_setting['page'] ); ?>" + id="<?php echo \esc_attr( 'pages[' . \esc_attr( $prpl_setting['id'] ) . '][have_page]' ); ?>" + name="<?php echo \esc_attr( 'pages[' . \esc_attr( $prpl_setting['id'] ) . '][have_page]' ); ?>" + value="<?php echo \esc_attr( $prpl_r_value ); ?>" + data-page="<?php echo \esc_attr( $prpl_setting['page'] ); ?>" <?php echo $prpl_radio_checked ? ' checked' : ''; ?> > - <?php echo esc_html( $prpl_r_label ); ?> + <?php echo \esc_html( $prpl_r_label ); ?> </label> <?php if ( 'yes' === $prpl_r_value ) : ?> <div class="prpl-select-page"> <div data-action="select"> <?php - wp_dropdown_pages( + \wp_dropdown_pages( [ - 'name' => 'pages[' . esc_attr( $prpl_setting['id'] ) . '][id]', - 'show_option_none' => '— ' . esc_html__( 'Select page', 'progress-planner' ) . ' —', - 'selected' => esc_attr( $prpl_setting['value'] ), + 'name' => 'pages[' . \esc_attr( $prpl_setting['id'] ) . '][id]', + 'show_option_none' => '— ' . \esc_html__( 'Select page', 'progress-planner' ) . ' —', + 'selected' => \esc_attr( $prpl_setting['value'] ), ] ); ?> @@ -88,9 +88,9 @@ class="prpl-pages-item prpl-pages-item-<?php echo esc_attr( $prpl_setting['page' <a target="_blank" class="prpl-button" - href="<?php echo esc_url( \admin_url( 'post-new.php?post_type=page&prpl_page_type=' . esc_attr( $prpl_setting['page'] ) ) ); ?>" + href="<?php echo \esc_url( \admin_url( 'post-new.php?post_type=page&prpl_page_type=' . \esc_attr( $prpl_setting['page'] ) ) ); ?>" > - <?php esc_html_e( 'Create this page', 'progress-planner' ); ?> + <?php \esc_html_e( 'Create this page', 'progress-planner' ); ?> </a> </div> <?php endif; ?> diff --git a/views/setting/radio.php b/views/setting/radio.php index 7a3b7e2bc..0ee1f5550 100644 --- a/views/setting/radio.php +++ b/views/setting/radio.php @@ -6,26 +6,26 @@ */ // Exit if accessed directly. -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } ?> -<fieldset id="prpl-setting-fieldset-<?php echo esc_attr( $prpl_setting['id'] ); ?>"> - <legend><?php echo wp_kses_post( $prpl_setting['title'] ); ?></legend> +<fieldset id="prpl-setting-fieldset-<?php echo \esc_attr( $prpl_setting['id'] ); ?>"> + <legend><?php echo \wp_kses_post( $prpl_setting['title'] ); ?></legend> <div class="radios"> <?php foreach ( $prpl_setting['options'] as $prpl_option_value => $prpl_option_label ) : ?> <label> <input type="radio" - id="prpl-setting-<?php echo esc_attr( $prpl_setting['id'] ); ?>" - name="<?php echo esc_attr( $prpl_setting['id'] ); ?>" - value="<?php echo esc_attr( $prpl_option_value ); ?>" + id="prpl-setting-<?php echo \esc_attr( $prpl_setting['id'] ); ?>" + name="<?php echo \esc_attr( $prpl_setting['id'] ); ?>" + value="<?php echo \esc_attr( $prpl_option_value ); ?>" <?php if ( isset( $prpl_setting['page'] ) && $prpl_setting['page'] ) : ?> - data-page="<?php echo esc_attr( $prpl_setting['page'] ); ?>" + data-page="<?php echo \esc_attr( $prpl_setting['page'] ); ?>" <?php endif; ?> <?php echo ( (string) $prpl_option_value === (string) $prpl_setting['value'] ) ? ' checked' : ''; ?> > - <?php echo wp_kses_post( $prpl_option_label ); ?> + <?php echo \wp_kses_post( $prpl_option_label ); ?> </label> <?php endforeach; ?> </div> diff --git a/views/welcome.php b/views/welcome.php index 731d63bb3..29612ef83 100644 --- a/views/welcome.php +++ b/views/welcome.php @@ -8,7 +8,7 @@ namespace Progress_Planner; // Exit if accessed directly. -if ( ! defined( 'ABSPATH' ) ) { +if ( ! \defined( 'ABSPATH' ) ) { exit; } @@ -21,7 +21,6 @@ \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/onboard' ); \progress_planner()->get_admin__enqueue()->enqueue_style( 'progress-planner/upgrade-tasks' ); - ?> <div class="prpl-welcome"> <div class="welcome-header"> @@ -40,24 +39,24 @@ <li> <?php /* translators: %s: <strong> tag */ - printf( \esc_html__( '%1$s Personalized to-do’s %2$s to keep your site in great shape.', 'progress-planner' ), '<strong>', '</strong>' ); + \printf( \esc_html__( '%1$s Personalized to-do’s %2$s to keep your site in great shape.', 'progress-planner' ), '<strong>', '</strong>' ); ?> </li> <li> <?php /* translators: %s: <strong> tag */ - printf( \esc_html__( '%1$s Activity stats %2$s so you can track your progress.', 'progress-planner' ), '<strong>', '</strong>' ); + \printf( \esc_html__( '%1$s Activity stats %2$s so you can track your progress.', 'progress-planner' ), '<strong>', '</strong>' ); ?> </li> <li> <?php /* translators: %s: <strong> tag */ - printf( \esc_html__( '%1$s Helpful nudges %2$s to stay consistent with your website goals.', 'progress-planner' ), '<strong>', '</strong>' ); + \printf( \esc_html__( '%1$s Helpful nudges %2$s to stay consistent with your website goals.', 'progress-planner' ), '<strong>', '</strong>' ); ?> </li> </ul> <?php - printf( + \printf( /* translators: %s: progressplanner.com link */ \esc_html__( 'To send these updates, we’ll create an account for you on %s.', 'progress-planner' ), '<a href="https://prpl.fyi/home" target="_blank">progressplanner.com</a>' @@ -128,7 +127,7 @@ class="prpl-input" value="1" > <?php - printf( + \printf( /* translators: %s: progressplanner.com/privacy-policy link */ \esc_html__( 'I agree to the %s.', 'progress-planner' ), '<a href="https://progressplanner.com/privacy-policy/#h-plugin-privacy-policy" target="_blank">Privacy policy</a>' @@ -158,7 +157,7 @@ class="prpl-button-secondary prpl-button-secondary--no-email prpl-hidden" <div> <p id="prpl-account-created-message" style="display:none;"> <?php - printf( + \printf( /* translators: %s: progressplanner.com link */ \esc_html__( 'Success! We saved your data on %s so we can email you every week.', 'progress-planner' ), '<a href="https://prpl.fyi/home">ProgressPlanner.com</a>' @@ -167,7 +166,7 @@ class="prpl-button-secondary prpl-button-secondary--no-email prpl-hidden" </p> <p id="prpl-account-not-created-message" style="display:none;"> <?php - printf( + \printf( \esc_html__( 'Success! Enjoy using the Progress Planner plugin!', 'progress-planner' ), ); ?> @@ -182,7 +181,7 @@ class="prpl-button-secondary prpl-button-secondary--no-email prpl-hidden" </div> <div class="right"> <img - src="<?php echo \esc_url( constant( 'PROGRESS_PLANNER_URL' ) . '/assets/images/image_onboaring_block.png' ); ?>" + src="<?php echo \esc_url( \constant( 'PROGRESS_PLANNER_URL' ) . '/assets/images/image_onboaring_block.png' ); ?>" alt="" class="onboarding" />