From 5684433e2a7d126b312de47ed05d21022483374c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Perona?= Date: Wed, 6 Mar 2024 09:54:17 -0500 Subject: [PATCH 1/9] Closes #504 AVIF support (#801) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gaël Robin Co-authored-by: Opeyemi Ibrahim Co-authored-by: Michael Lee <38788055+jeawhanlee@users.noreply.github.com> Co-authored-by: WordPress Fan <146129302+wordpressfan@users.noreply.github.com> --- assets/js/options.js | 12 +- assets/js/options.min.js | 2 +- classes/Avif/Apache.php | 31 + classes/Avif/Display.php | 169 ++++ classes/Avif/IIS.php | 32 + classes/Avif/RewriteRules/Apache.php | 59 ++ classes/Avif/RewriteRules/Display.php | 224 ++++++ classes/Avif/RewriteRules/IIS.php | 58 ++ classes/Avif/RewriteRules/Nginx.php | 30 + classes/Avif/ServiceProvider.php | 51 ++ classes/Bulk/AbstractBulk.php | 12 +- classes/Bulk/Bulk.php | 188 +++-- classes/Bulk/BulkInterface.php | 16 +- classes/Bulk/CustomFolders.php | 32 +- classes/Bulk/Noop.php | 9 +- classes/Bulk/WP.php | 65 +- classes/CDN/CDN.php | 123 +++ classes/CDN/ServiceProvider.php | 47 ++ ....php => GenerateMissingNextgenCommand.php} | 14 +- .../Argument/ArgumentResolverInterface.php | 28 + .../Argument/ArgumentResolverTrait.php | 120 +++ .../League/Container/Argument/ClassName.php | 29 + .../Container/Argument/ClassNameInterface.php | 13 + .../Argument/ClassNameWithOptionalValue.php | 39 + .../League/Container/Argument/RawArgument.php | 29 + .../Argument/RawArgumentInterface.php | 13 + .../League/Container/Container.php | 248 ++++++ .../Container/ContainerAwareInterface.php | 40 + .../League/Container/ContainerAwareTrait.php | 76 ++ .../Container/Definition/Definition.php | 278 +++++++ .../Definition/DefinitionAggregate.php | 124 +++ .../DefinitionAggregateInterface.php | 67 ++ .../Definition/DefinitionInterface.php | 120 +++ .../Exception/ContainerException.php | 10 + .../Container/Exception/NotFoundException.php | 10 + .../League/Container/Inflector/Inflector.php | 123 +++ .../Inflector/InflectorAggregate.php | 58 ++ .../Inflector/InflectorAggregateInterface.php | 27 + .../Inflector/InflectorInterface.php | 60 ++ .../League/Container/ReflectionContainer.php | 131 +++ .../AbstractServiceProvider.php | 46 ++ .../BootableServiceProviderInterface.php | 14 + .../ServiceProviderAggregate.php | 106 +++ .../ServiceProviderAggregateInterface.php | 36 + .../ServiceProviderInterface.php | 46 ++ .../Container/ContainerExceptionInterface.php | 12 + .../Psr/Container/ContainerInterface.php | 36 + .../Container/NotFoundExceptionInterface.php | 10 + classes/Imagifybeat/Actions.php | 10 +- classes/Job/MediaOptimization.php | 4 +- classes/Optimization/File.php | 81 +- .../Optimization/Process/AbstractProcess.php | 747 +++++++++++------- classes/Optimization/Process/Noop.php | 246 +++--- .../Optimization/Process/ProcessInterface.php | 216 ++--- classes/{Webp => }/Picture/Display.php | 404 +++++----- classes/Picture/ServiceProvider.php | 48 ++ classes/Plugin.php | 107 ++- ...p.php => OptimizedMediaWithoutNextGen.php} | 98 ++- classes/Stats/ServiceProvider.php | 47 ++ classes/Webp/Apache.php | 18 +- classes/Webp/Display.php | 92 +-- classes/Webp/IIS.php | 18 +- classes/Webp/RewriteRules/Apache.php | 17 +- classes/Webp/RewriteRules/Display.php | 108 +-- classes/Webp/RewriteRules/IIS.php | 19 +- classes/Webp/RewriteRules/Nginx.php | 60 +- classes/Webp/ServiceProvider.php | 51 ++ composer.json | 6 +- config/providers.php | 8 + imagify.php | 8 +- .../amazon-s3-and-cloudfront/classes/Main.php | 4 +- .../nextgen-gallery/classes/Bulk/NGG.php | 32 +- .../inc/common/attachments.php | 3 +- .../regenerate-thumbnails/classes/Main.php | 2 +- .../ActionScheduler/action-scheduler.php | 19 +- .../ActionScheduler/changelog.txt | 60 ++ .../classes/ActionScheduler_ActionFactory.php | 108 ++- .../classes/ActionScheduler_Compatibility.php | 18 +- .../classes/ActionScheduler_ListTable.php | 19 +- .../classes/ActionScheduler_OptionLock.php | 90 ++- .../classes/ActionScheduler_QueueCleaner.php | 127 ++- .../classes/ActionScheduler_QueueRunner.php | 19 +- .../ActionScheduler_WPCLI_Clean_Command.php | 125 +++ .../ActionScheduler_WPCLI_QueueRunner.php | 2 +- ...ctionScheduler_WPCLI_Scheduler_command.php | 50 +- .../classes/abstracts/ActionScheduler.php | 41 +- .../ActionScheduler_Abstract_ListTable.php | 20 +- .../ActionScheduler_Abstract_QueueRunner.php | 117 ++- .../ActionScheduler_Abstract_Schema.php | 31 +- .../abstracts/ActionScheduler_Lock.php | 2 + .../abstracts/ActionScheduler_Store.php | 4 +- .../actions/ActionScheduler_Action.php | 39 + .../data-stores/ActionScheduler_DBLogger.php | 2 +- .../data-stores/ActionScheduler_DBStore.php | 159 +++- .../ActionScheduler_wpPostStore.php | 15 +- .../classes/migration/Runner.php | 2 +- .../schema/ActionScheduler_StoreSchema.php | 8 +- .../ActionScheduler/functions.php | 107 ++- inc/Dependencies/ActionScheduler/readme.txt | 66 +- inc/admin/upgrader.php | 2 +- .../classes/wp-async-request.php | 57 +- .../classes/wp-background-process.php | 428 +++++++--- .../wp-media/event-manager/EventManager.php | 135 ++++ .../EventManagerAwareSubscriberInterface.php | 14 + .../event-manager/SubscriberInterface.php | 32 + inc/classes/class-imagify-admin-ajax-post.php | 36 +- .../class-imagify-files-list-table.php | 22 +- inc/classes/class-imagify-options.php | 43 +- inc/common/attachments.php | 51 +- inc/functions/admin-ui.php | 54 +- inc/functions/admin.php | 8 +- inc/functions/api.php | 6 +- inc/functions/common.php | 26 + inc/functions/i18n.php | 38 +- inc/main.php | 6 +- readme.txt | 29 +- uninstall.php | 2 +- views/button/delete-webp.php | 2 +- views/button/generate-webp.php | 2 +- views/part-settings-webp-missing-message.php | 4 +- views/part-settings-webp.php | 42 +- 121 files changed, 6234 insertions(+), 1632 deletions(-) create mode 100644 classes/Avif/Apache.php create mode 100644 classes/Avif/Display.php create mode 100644 classes/Avif/IIS.php create mode 100644 classes/Avif/RewriteRules/Apache.php create mode 100644 classes/Avif/RewriteRules/Display.php create mode 100644 classes/Avif/RewriteRules/IIS.php create mode 100644 classes/Avif/RewriteRules/Nginx.php create mode 100644 classes/Avif/ServiceProvider.php create mode 100644 classes/CDN/CDN.php create mode 100644 classes/CDN/ServiceProvider.php rename classes/CLI/{GenerateMissingWebpCommand.php => GenerateMissingNextgenCommand.php} (57%) create mode 100644 classes/Dependencies/League/Container/Argument/ArgumentResolverInterface.php create mode 100644 classes/Dependencies/League/Container/Argument/ArgumentResolverTrait.php create mode 100644 classes/Dependencies/League/Container/Argument/ClassName.php create mode 100644 classes/Dependencies/League/Container/Argument/ClassNameInterface.php create mode 100644 classes/Dependencies/League/Container/Argument/ClassNameWithOptionalValue.php create mode 100644 classes/Dependencies/League/Container/Argument/RawArgument.php create mode 100644 classes/Dependencies/League/Container/Argument/RawArgumentInterface.php create mode 100644 classes/Dependencies/League/Container/Container.php create mode 100644 classes/Dependencies/League/Container/ContainerAwareInterface.php create mode 100644 classes/Dependencies/League/Container/ContainerAwareTrait.php create mode 100644 classes/Dependencies/League/Container/Definition/Definition.php create mode 100644 classes/Dependencies/League/Container/Definition/DefinitionAggregate.php create mode 100644 classes/Dependencies/League/Container/Definition/DefinitionAggregateInterface.php create mode 100644 classes/Dependencies/League/Container/Definition/DefinitionInterface.php create mode 100644 classes/Dependencies/League/Container/Exception/ContainerException.php create mode 100644 classes/Dependencies/League/Container/Exception/NotFoundException.php create mode 100644 classes/Dependencies/League/Container/Inflector/Inflector.php create mode 100644 classes/Dependencies/League/Container/Inflector/InflectorAggregate.php create mode 100644 classes/Dependencies/League/Container/Inflector/InflectorAggregateInterface.php create mode 100644 classes/Dependencies/League/Container/Inflector/InflectorInterface.php create mode 100644 classes/Dependencies/League/Container/ReflectionContainer.php create mode 100644 classes/Dependencies/League/Container/ServiceProvider/AbstractServiceProvider.php create mode 100644 classes/Dependencies/League/Container/ServiceProvider/BootableServiceProviderInterface.php create mode 100644 classes/Dependencies/League/Container/ServiceProvider/ServiceProviderAggregate.php create mode 100644 classes/Dependencies/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php create mode 100644 classes/Dependencies/League/Container/ServiceProvider/ServiceProviderInterface.php create mode 100644 classes/Dependencies/Psr/Container/ContainerExceptionInterface.php create mode 100644 classes/Dependencies/Psr/Container/ContainerInterface.php create mode 100644 classes/Dependencies/Psr/Container/NotFoundExceptionInterface.php rename classes/{Webp => }/Picture/Display.php (71%) create mode 100644 classes/Picture/ServiceProvider.php rename classes/Stats/{OptimizedMediaWithoutWebp.php => OptimizedMediaWithoutNextGen.php} (63%) create mode 100644 classes/Stats/ServiceProvider.php create mode 100644 classes/Webp/ServiceProvider.php create mode 100644 config/providers.php create mode 100644 inc/Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Clean_Command.php create mode 100644 inc/classes/Dependencies/wp-media/event-manager/EventManager.php create mode 100644 inc/classes/Dependencies/wp-media/event-manager/EventManagerAwareSubscriberInterface.php create mode 100644 inc/classes/Dependencies/wp-media/event-manager/SubscriberInterface.php diff --git a/assets/js/options.js b/assets/js/options.js index b4d08b6b8..657065e8c 100755 --- a/assets/js/options.js +++ b/assets/js/options.js @@ -138,7 +138,7 @@ window.imagify = window.imagify || {}; /** * Fade CDN URL field. */ - $( '[name="imagify_settings[display_webp_method]"]' ).on( 'change.imagify init.imagify', function( e ) { + $( '[name="imagify_settings[display_nextgen_method]"]' ).on( 'change.imagify init.imagify', function( e ) { if ( 'picture' === e.target.value ) { $( e.target ).closest( '.imagify-radio-group' ).next( '.imagify-options-line' ).removeClass( 'imagify-faded' ); } else { @@ -496,7 +496,7 @@ window.imagify = window.imagify || {}; .on( 'imagifybeat-send', this.addRequirementsImagifybeat ) .on( 'imagifybeat-tick', { imagifyOptionsBulk: this }, this.processRequirementsImagifybeat ); - if ( false !== imagifyOptions.bulk.progress_webp.total && false !== imagifyOptions.bulk.progress_webp.remaining ) { + if ( false !== imagifyOptions.bulk.progress_next_gen.total && false !== imagifyOptions.bulk.progress_next_gen.remaining ) { // Reset properties. w.imagify.optionsBulk.error = false; w.imagify.optionsBulk.working = true; @@ -511,10 +511,10 @@ window.imagify = window.imagify || {}; this.$missingWebpMessage.hide().attr('aria-hidden', 'true'); - processed = imagifyOptions.bulk.progress_webp.total - imagifyOptions.bulk.progress_webp.remaining; - progress = Math.floor( processed / imagifyOptions.bulk.progress_webp.total * 100 ); + processed = imagifyOptions.bulk.progress_next_gen.total - imagifyOptions.bulk.progress_next_gen.remaining; + progress = Math.floor( processed / imagifyOptions.bulk.progress_next_gen.total * 100 ); this.$progressBar.css( 'width', progress + '%' ); - this.$progressText.text( processed + '/' + imagifyOptions.bulk.progress_webp.total ); + this.$progressText.text( processed + '/' + imagifyOptions.bulk.progress_next_gen.total ); this.$progressWrap.slideDown().attr( 'aria-hidden', 'false' ).removeClass( 'hidden' ); } @@ -655,7 +655,7 @@ window.imagify = window.imagify || {}; _this = this; - $.get( this.getAjaxUrl( 'MissingWebp', imagifyOptions.bulk.contexts ) ) + $.get( this.getAjaxUrl( 'MissingNextGen', imagifyOptions.bulk.contexts ) ) .done( function( response ) { var errorMessage; diff --git a/assets/js/options.min.js b/assets/js/options.min.js index f61a911b6..34c492b4c 100755 --- a/assets/js/options.min.js +++ b/assets/js/options.min.js @@ -1 +1 @@ -window.imagify=window.imagify||{},function(t,a){var s=!1,o=!1;t("#imagify-settings #api_key").on("blur.imagify",function(){var i=t(this),e=i.val();return""!==t.trim(e)&&(t("#check_api_key").val()===e?(t("#imagify-check-api-container").html(' '+imagifyOptions.labels.ValidApiKeyText),!1):(!0===s?o.abort():(t("#imagify-check-api-container").remove(),i.after(''+imagifyOptions.labels.waitApiKeyCheckText+"")),s=!0,void(o=t.get(ajaxurl+a.imagify.concat+"action=imagify_check_api_key_validity&api_key="+i.val()+"&imagifycheckapikeynonce="+t("#imagifycheckapikeynonce").val()).done(function(i){i.success?(t("#imagify-check-api-container").remove(),swal({title:imagifyOptions.labels.ApiKeyCheckSuccessTitle,html:imagifyOptions.labels.ApiKeyCheckSuccessText,type:"success",padding:0,customClass:"imagify-sweet-alert"}).then(function(){location.reload()})):t("#imagify-check-api-container").html(' '+i.data),s=!1}))))}),t(".imagify-options-line").css("cursor","pointer").on("click.imagify",function(i){"INPUT"!==i.target.nodeName&&t('input[aria-describedby="'+t(this).attr("id")+'"]').trigger("click.imagify")}),t(".imagify-settings th span").on("click.imagify",function(){var i=t(this).parent().next("td").find(":checkbox");1===i.length&&i.trigger("click.imagify")}),t(".imagify-options-line").find("input").on("change.imagify focus.imagify",function(){var i;"checkbox"===this.type&&!this.checked||!(i=t(this).closest(".imagify-options-line").prev("label").prev(":checkbox")).length||i[0].checked||i.prop("checked",!0)}),t('[name="imagify_settings[backup]"]').on("change.imagify",function(){var i=t(this),e=i.siblings("#backup-dir-is-writable"),a={action:"imagify_check_backup_dir_is_writable",_wpnonce:e.data("nonce")};i.is(":checked")?t.getJSON(ajaxurl,a).done(function(i){t.isPlainObject(i)&&i.success&&(i.data.is_writable?e.addClass("hidden"):e.removeClass("hidden"))}):swal({title:imagifyOptions.labels.noBackupTitle,html:imagifyOptions.labels.noBackupText,type:"warning",customClass:"imagify-sweet-alert",padding:0,showCancelButton:!0,cancelButtonText:imagifySwal.labels.cancelButtonText,reverseButtons:!0}).then(function(){e.addClass("hidden")},function(){i.prop("checked",!0)})}),t('[name="imagify_settings[display_webp_method]"]').on("change.imagify init.imagify",function(i){"picture"===i.target.value?t(i.target).closest(".imagify-radio-group").next(".imagify-options-line").removeClass("imagify-faded"):t(i.target).closest(".imagify-radio-group").next(".imagify-options-line").addClass("imagify-faded")}).filter(":checked").trigger("init.imagify")}(jQuery,(document,window)),function(i,s){i.imagifyUser&&s.getJSON(ajaxurl,i.imagifyUser).done(function(t){s.isPlainObject(t)&&t.success&&(t.data.id=null,t.data.plan_id=null,t.data.is=[],s.each(t.data,function(i,e){var a=".imagify-user-"+i.replace(/_/g,"-");0===i.indexOf("is_")?e&&t.data.is.push(a):"is"!==i&&s(a).text(e)}),t.data.is.push("best-plan"),s(t.data.is.join(",")).removeClass("hidden"))})}(window,(document,jQuery)),function(e,i,g){function a(a){var t,s,o,n,l=!1,r=null;a&&(o=g("#imagify-custom-folders-selected"),(n=o.find(".imagify-custom-folder-line")).find('[value="'+a+'"]').length||(a=a.split("#///#"),t=a[1].replace(/\/+$/,"").toLowerCase(),s=e.imagify.template("imagify-custom-folder"),n.each(function(){var i=g(this),e=i.data("path").replace(/\/+$/,"").toLowerCase();return""!==e&&0===t.indexOf(e)?!(l=!0):t'+imagifyOptions.labels.filesTreeSubTitle+'

'+imagifyOptions.labels.cleaningInfo+'

    '+i.data+"
",type:"",customClass:"imagify-sweet-alert imagify-swal-has-subtitle imagify-folders-selection",showCancelButton:!0,padding:0,confirmButtonText:imagifyOptions.labels.confirmFilesTreeBtn,cancelButtonText:imagifySwal.labels.cancelButtonText,reverseButtons:!0}).then(function(){var i=g("#imagify-folders-tree input").serializeArray();i.length&&g.each(i,function(i,e){a(e.value)})}).catch(swal.noop):swal({title:imagifyOptions.labels.error,html:i.data||"",type:"error",padding:0,customClass:"imagify-sweet-alert"})}).fail(function(){swal({title:imagifyOptions.labels.error,type:"error",customClass:"imagify-sweet-alert",padding:0})}).always(function(){i.prop("disabled",!1).next("img").attr("aria-hidden","true")}))}),g(i).on("click.imagify","#imagify-folders-tree [data-folder]",function(){var e=g(this),i=e.nextAll(".imagify-folders-sub-tree"),a=[];e.prop("disabled")||e.siblings(":checkbox").is(":checked")||(e.prop("disabled",!0).addClass("imagify-loading"),i.length?(e.hasClass("imagify-is-open")?(i.addClass("hidden"),e.removeClass(" imagify-is-open")):(i.removeClass("hidden"),e.addClass("imagify-is-open")),e.prop("disabled",!1).removeClass("imagify-loading")):(g("#imagify-custom-folders-selected").find("input").each(function(){a.push(this.value)}),g.post(imagifyOptions.getFilesTree,{folder:e.data("folder"),selected:a},null,"json").done(function(i){i.success?e.addClass("imagify-is-open").parent().append('
    '+i.data+"
"):swal({title:imagifyOptions.labels.error,html:i.data||"",type:"error",padding:0,customClass:"imagify-sweet-alert"})}).fail(function(){swal({title:imagifyOptions.labels.error,type:"error",padding:0,customClass:"imagify-sweet-alert"})}).always(function(){e.prop("disabled",!1).removeClass("imagify-loading")})))}),g("#imagify-custom-folders").on("click.imagify",".imagify-custom-folders-remove",function(){var i=g(this).closest(".imagify-custom-folder-line").addClass("imagify-will-remove");e.setTimeout(function(){i.remove(),g("#imagify-custom-folders-selected").siblings(".imagify-success.hidden").removeClass("hidden")},750)}),g("#imagify-add-themes-to-custom-folder").on("click.imagify",function(){var i=g(this);a(i.data("theme")),a(i.data("theme-parent")),i.replaceWith("

"+imagifyOptions.labels.themesAdded+"

")}))}(window,document,jQuery),function(t,a,s){imagifyOptions.bulk&&(t.imagify.optionsBulk={error:!1,working:!1,processIsStopped:!1,$button:null,$progressWrap:null,$progressBar:null,$progressText:null,init:function(){var i,e;this.$missingWebpElement=s(".generate-missing-webp"),this.$missingWebpMessage=s(".generate-missing-webp p"),this.$button=s("#imagify-generate-webp-versions"),this.$progressWrap=this.$button.siblings(".imagify-progress"),this.$progressBar=this.$progressWrap.find(".bar"),this.$progressText=this.$progressBar.find(".percent"),s("#imagify_convert_to_webp").on("change.imagify init.imagify",{imagifyOptionsBulk:this},this.toggleButton).trigger("init.imagify"),this.$button.on("click.imagify",{imagifyOptionsBulk:this},this.maybeLaunchMissingWebpProcess),s(a).on("imagifybeat-send",{imagifyOptionsBulk:this},this.addQueueImagifybeat).on("imagifybeat-tick",{imagifyOptionsBulk:this},this.processQueueImagifybeat).on("imagifybeat-send",this.addRequirementsImagifybeat).on("imagifybeat-tick",{imagifyOptionsBulk:this},this.processRequirementsImagifybeat),!1!==imagifyOptions.bulk.progress_webp.total&&!1!==imagifyOptions.bulk.progress_webp.remaining&&(t.imagify.optionsBulk.error=!1,t.imagify.optionsBulk.working=!0,t.imagify.optionsBulk.processIsStopped=!1,this.$button.prop("disabled",!0).find(".dashicons").addClass("rotate"),t.imagify.beat.interval(15),t.imagify.beat.disableSuspend(),this.$missingWebpMessage.hide().attr("aria-hidden","true"),i=imagifyOptions.bulk.progress_webp.total-imagifyOptions.bulk.progress_webp.remaining,e=Math.floor(i/imagifyOptions.bulk.progress_webp.total*100),this.$progressBar.css("width",e+"%"),this.$progressText.text(i+"/"+imagifyOptions.bulk.progress_webp.total),this.$progressWrap.slideDown().attr("aria-hidden","false").removeClass("hidden"))},toggleButton:function(i){this.checked?i.data.imagifyOptionsBulk.$button.prop("disabled",!1):i.data.imagifyOptionsBulk.$button.prop("disabled",!0)},maybeLaunchMissingWebpProcess:function(i){!i.data.imagifyOptionsBulk||i.data.imagifyOptionsBulk.working||i.data.imagifyOptionsBulk.hasBlockingError(!0)||(i.data.imagifyOptionsBulk.error=!1,i.data.imagifyOptionsBulk.working=!0,i.data.imagifyOptionsBulk.processIsStopped=!1,i.data.imagifyOptionsBulk.$button.prop("disabled",!0).find(".dashicons").addClass("rotate"),t.imagify.beat.interval(15),t.imagify.beat.disableSuspend(),i.data.imagifyOptionsBulk.launchProcess())},addQueueImagifybeat:function(i,e){e[imagifyOptions.bulk.imagifybeatIDs.progress]=imagifyOptions.bulk.contexts},processQueueImagifybeat:function(i,e){var a,t;i.data.imagifyOptionsBulk&&void 0===e[imagifyOptions.bulk.imagifybeatIDs.progress]||(i.data.imagifyOptionsBulk.processIsStopped||0===(e=e[imagifyOptions.bulk.imagifybeatIDs.progress]).remaining?i.data.imagifyOptionsBulk.processFinished():(a=e.total-e.remaining,t=Math.floor(a/e.total*100),i.data.imagifyOptionsBulk.$progressBar.css("width",t+"%"),i.data.imagifyOptionsBulk.$progressText.text(a+"/"+e.total)))},addRequirementsImagifybeat:function(i,e){e[imagifyOptions.bulk.imagifybeatIDs.requirements]=1},processRequirementsImagifybeat:function(i,e){i.data.imagifyOptionsBulk&&void 0===e[imagifyOptions.bulk.imagifybeatIDs.requirements]||(e=e[imagifyOptions.bulk.imagifybeatIDs.requirements],imagifyOptions.bulk.curlMissing=e.curl_missing,imagifyOptions.bulk.editorMissing=e.editor_missing,imagifyOptions.bulk.extHttpBlocked=e.external_http_blocked,imagifyOptions.bulk.apiDown=e.api_down,imagifyOptions.bulk.keyIsValid=e.key_is_valid,imagifyOptions.bulk.isOverQuota=e.is_over_quota)},launchProcess:function(){var a;this.processIsStopped||s.get((a=this).getAjaxUrl("MissingWebp",imagifyOptions.bulk.contexts)).done(function(i){var e;a.processIsStopped||(e=i.data&&i.data.message?i.data.message:imagifyOptions.bulk.ajaxErrorText,i.success?0===i.data.total?a.stopProcess("no-images"):(a.$missingWebpMessage.hide().attr("aria-hidden","true"),a.$progressText.text("0"+(i.data.total?"/"+i.data.total:"")),a.$progressWrap.slideDown().attr("aria-hidden","false").removeClass("hidden")):a.error||a.stopProcess(e))}).fail(function(){a.error||a.stopProcess("get-unoptimized-images")})},processFinished:function(){var i={};!1!==this.error&&(i="invalid-api-key"===this.error?{title:imagifyOptions.bulk.labels.invalidAPIKeyTitle,type:"info"}:"over-quota"===this.error?{title:imagifyOptions.bulk.labels.overQuotaTitle,html:s("#tmpl-imagify-overquota-alert").html(),type:"info",customClass:"imagify-swal-has-subtitle imagify-swal-error-header",showConfirmButton:!1}:"get-unoptimized-images"===this.error?{title:imagifyOptions.bulk.labels.getUnoptimizedImagesErrorTitle,html:imagifyOptions.bulk.labels.getUnoptimizedImagesErrorText,type:"info"}:"no-images"===this.error?{title:imagifyOptions.bulk.labels.nothingToDoTitle,html:imagifyOptions.bulk.labels.nothingToDoText,type:"info"}:"no-backup"===this.error?{title:imagifyOptions.bulk.labels.nothingToDoTitle,html:imagifyOptions.bulk.labels.nothingToDoNoBackupText,type:"info"}:{title:imagifyOptions.bulk.labels.error,html:this.error,type:"info"},this.displayError(i),this.error=!1),this.working=!1,this.processIsStopped=!1,t.imagify.beat.resetInterval(),t.imagify.beat.enableSuspend(),this.$progressWrap.slideUp().attr("aria-hidden","true").addClass("hidden"),this.$progressText.text("0"),this.$missingWebpElement.hide().attr("aria-hidden","true"),this.$button.find(".dashicons").removeClass("rotate")},hasBlockingError:function(i){return i=void 0!==i&&i,imagifyOptions.bulk.curlMissing?(i&&this.displayError({html:imagifyOptions.bulk.labels.curlMissing}),!0):imagifyOptions.bulk.editorMissing?(i&&this.displayError({html:imagifyOptions.bulk.labels.editorMissing}),!0):imagifyOptions.bulk.extHttpBlocked?(i&&this.displayError({html:imagifyOptions.bulk.labels.extHttpBlocked}),!0):imagifyOptions.bulk.apiDown?(i&&this.displayError({html:imagifyOptions.bulk.labels.apiDown}),!0):imagifyOptions.bulk.keyIsValid?!!imagifyOptions.bulk.isOverQuota&&(i&&this.displayError({title:imagifyOptions.bulk.labels.overQuotaTitle,html:s("#tmpl-imagify-overquota-alert").html(),type:"info",customClass:"imagify-swal-has-subtitle imagify-swal-error-header",showConfirmButton:!1}),!0):(i&&this.displayError({title:imagifyOptions.bulk.labels.invalidAPIKeyTitle,type:"info"}),!0)},displayError:function(i,e,a){var t={title:"",html:"",type:"error",customClass:"",width:620,padding:0,showCloseButton:!0,showConfirmButton:!0};(a=s.isPlainObject(i)?s.extend({},t,i):s.extend({},t,{title:i||"",html:e||""},a=a||{})).title=a.title||imagifyOptions.bulk.labels.error,a.customClass+=" imagify-sweet-alert",swal(a).catch(swal.noop)},getAjaxUrl:function(i,e){var a=ajaxurl+t.imagify.concat+"_wpnonce="+imagifyOptions.bulk.ajaxNonce;return(a+="&action="+imagifyOptions.bulk.ajaxActions[i])+("&context="+e.join("_"))},stopProcess:function(i){this.processIsStopped=!0,this.error=i,this.processFinished()}},t.imagify.optionsBulk.init())}(window,document,jQuery),function(o){var t=o.propHooks.checked;o.propHooks.checked={set:function(i,e,a){e=void 0===t?i[a]=e:t(i,e,a);return o(i).trigger("change.imagify"),e}},o(".imagify-select-all").on("click.imagify",function(){var i=o(this),e=i.data("action"),a=i.closest(".imagify-select-all-buttons"),t=a.prev(".imagify-check-group"),s="imagify-is-inactive";if(i.hasClass(s))return!1;a.find(".imagify-select-all").removeClass(s).attr("aria-disabled","false"),i.addClass(s).attr("aria-disabled","true"),t.find(".imagify-row-check").prop("checked",function(){return!o(this).is(":hidden,:disabled")&&"select"===e})}),o(".imagify-check-group .imagify-row-check").on("change.imagify",function(){var i=o(this).closest(".imagify-check-group"),e=i.find(".imagify-row-check"),a=e.filter(":visible:enabled").length,e=e.filter(":visible:enabled:checked").length,i=i.next(".imagify-select-all-buttons"),t="imagify-is-inactive";0===e&&i.find('[data-action="unselect"]').addClass(t).attr("aria-disabled","true"),e===a&&i.find('[data-action="select"]').addClass(t).attr("aria-disabled","true"),e!==a&&0 '+imagifyOptions.labels.ValidApiKeyText),!1):(!0===s?n.abort():(t("#imagify-check-api-container").remove(),i.after(''+imagifyOptions.labels.waitApiKeyCheckText+"")),s=!0,void(n=t.get(ajaxurl+a.imagify.concat+"action=imagify_check_api_key_validity&api_key="+i.val()+"&imagifycheckapikeynonce="+t("#imagifycheckapikeynonce").val()).done(function(i){i.success?(t("#imagify-check-api-container").remove(),swal({title:imagifyOptions.labels.ApiKeyCheckSuccessTitle,html:imagifyOptions.labels.ApiKeyCheckSuccessText,type:"success",padding:0,customClass:"imagify-sweet-alert"}).then(function(){location.reload()})):t("#imagify-check-api-container").html(' '+i.data),s=!1}))))}),t(".imagify-options-line").css("cursor","pointer").on("click.imagify",function(i){"INPUT"!==i.target.nodeName&&t('input[aria-describedby="'+t(this).attr("id")+'"]').trigger("click.imagify")}),t(".imagify-settings th span").on("click.imagify",function(){var i=t(this).parent().next("td").find(":checkbox");1===i.length&&i.trigger("click.imagify")}),t(".imagify-options-line").find("input").on("change.imagify focus.imagify",function(){var i;"checkbox"===this.type&&!this.checked||!(i=t(this).closest(".imagify-options-line").prev("label").prev(":checkbox")).length||i[0].checked||i.prop("checked",!0)}),t('[name="imagify_settings[backup]"]').on("change.imagify",function(){var i=t(this),e=i.siblings("#backup-dir-is-writable"),a={action:"imagify_check_backup_dir_is_writable",_wpnonce:e.data("nonce")};i.is(":checked")?t.getJSON(ajaxurl,a).done(function(i){t.isPlainObject(i)&&i.success&&(i.data.is_writable?e.addClass("hidden"):e.removeClass("hidden"))}):swal({title:imagifyOptions.labels.noBackupTitle,html:imagifyOptions.labels.noBackupText,type:"warning",customClass:"imagify-sweet-alert",padding:0,showCancelButton:!0,cancelButtonText:imagifySwal.labels.cancelButtonText,reverseButtons:!0}).then(function(){e.addClass("hidden")},function(){i.prop("checked",!0)})}),t('[name="imagify_settings[display_nextgen_method]"]').on("change.imagify init.imagify",function(i){"picture"===i.target.value?t(i.target).closest(".imagify-radio-group").next(".imagify-options-line").removeClass("imagify-faded"):t(i.target).closest(".imagify-radio-group").next(".imagify-options-line").addClass("imagify-faded")}).filter(":checked").trigger("init.imagify")}(jQuery,(document,window)),function(i,s){i.imagifyUser&&s.getJSON(ajaxurl,i.imagifyUser).done(function(t){s.isPlainObject(t)&&t.success&&(t.data.id=null,t.data.plan_id=null,t.data.is=[],s.each(t.data,function(i,e){var a=".imagify-user-"+i.replace(/_/g,"-");0===i.indexOf("is_")?e&&t.data.is.push(a):"is"!==i&&s(a).text(e)}),t.data.is.push("best-plan"),s(t.data.is.join(",")).removeClass("hidden"))})}(window,(document,jQuery)),function(e,i,g){function a(a){var t,s,n,o,l=!1,r=null;a&&(n=g("#imagify-custom-folders-selected"),(o=n.find(".imagify-custom-folder-line")).find('[value="'+a+'"]').length||(a=a.split("#///#"),t=a[1].replace(/\/+$/,"").toLowerCase(),s=e.imagify.template("imagify-custom-folder"),o.each(function(){var i=g(this),e=i.data("path").replace(/\/+$/,"").toLowerCase();return""!==e&&0===t.indexOf(e)?!(l=!0):t'+imagifyOptions.labels.filesTreeSubTitle+'

'+imagifyOptions.labels.cleaningInfo+'

    '+i.data+"
",type:"",customClass:"imagify-sweet-alert imagify-swal-has-subtitle imagify-folders-selection",showCancelButton:!0,padding:0,confirmButtonText:imagifyOptions.labels.confirmFilesTreeBtn,cancelButtonText:imagifySwal.labels.cancelButtonText,reverseButtons:!0}).then(function(){var i=g("#imagify-folders-tree input").serializeArray();i.length&&g.each(i,function(i,e){a(e.value)})}).catch(swal.noop):swal({title:imagifyOptions.labels.error,html:i.data||"",type:"error",padding:0,customClass:"imagify-sweet-alert"})}).fail(function(){swal({title:imagifyOptions.labels.error,type:"error",customClass:"imagify-sweet-alert",padding:0})}).always(function(){i.prop("disabled",!1).next("img").attr("aria-hidden","true")}))}),g(i).on("click.imagify","#imagify-folders-tree [data-folder]",function(){var e=g(this),i=e.nextAll(".imagify-folders-sub-tree"),a=[];e.prop("disabled")||e.siblings(":checkbox").is(":checked")||(e.prop("disabled",!0).addClass("imagify-loading"),i.length?(e.hasClass("imagify-is-open")?(i.addClass("hidden"),e.removeClass(" imagify-is-open")):(i.removeClass("hidden"),e.addClass("imagify-is-open")),e.prop("disabled",!1).removeClass("imagify-loading")):(g("#imagify-custom-folders-selected").find("input").each(function(){a.push(this.value)}),g.post(imagifyOptions.getFilesTree,{folder:e.data("folder"),selected:a},null,"json").done(function(i){i.success?e.addClass("imagify-is-open").parent().append('
    '+i.data+"
"):swal({title:imagifyOptions.labels.error,html:i.data||"",type:"error",padding:0,customClass:"imagify-sweet-alert"})}).fail(function(){swal({title:imagifyOptions.labels.error,type:"error",padding:0,customClass:"imagify-sweet-alert"})}).always(function(){e.prop("disabled",!1).removeClass("imagify-loading")})))}),g("#imagify-custom-folders").on("click.imagify",".imagify-custom-folders-remove",function(){var i=g(this).closest(".imagify-custom-folder-line").addClass("imagify-will-remove");e.setTimeout(function(){i.remove(),g("#imagify-custom-folders-selected").siblings(".imagify-success.hidden").removeClass("hidden")},750)}),g("#imagify-add-themes-to-custom-folder").on("click.imagify",function(){var i=g(this);a(i.data("theme")),a(i.data("theme-parent")),i.replaceWith("

"+imagifyOptions.labels.themesAdded+"

")}))}(window,document,jQuery),function(t,a,s){imagifyOptions.bulk&&(t.imagify.optionsBulk={error:!1,working:!1,processIsStopped:!1,$button:null,$progressWrap:null,$progressBar:null,$progressText:null,init:function(){var i,e;this.$missingWebpElement=s(".generate-missing-webp"),this.$missingWebpMessage=s(".generate-missing-webp p"),this.$button=s("#imagify-generate-webp-versions"),this.$progressWrap=this.$button.siblings(".imagify-progress"),this.$progressBar=this.$progressWrap.find(".bar"),this.$progressText=this.$progressBar.find(".percent"),s("#imagify_convert_to_webp").on("change.imagify init.imagify",{imagifyOptionsBulk:this},this.toggleButton).trigger("init.imagify"),this.$button.on("click.imagify",{imagifyOptionsBulk:this},this.maybeLaunchMissingWebpProcess),s(a).on("imagifybeat-send",{imagifyOptionsBulk:this},this.addQueueImagifybeat).on("imagifybeat-tick",{imagifyOptionsBulk:this},this.processQueueImagifybeat).on("imagifybeat-send",this.addRequirementsImagifybeat).on("imagifybeat-tick",{imagifyOptionsBulk:this},this.processRequirementsImagifybeat),!1!==imagifyOptions.bulk.progress_next_gen.total&&!1!==imagifyOptions.bulk.progress_next_gen.remaining&&(t.imagify.optionsBulk.error=!1,t.imagify.optionsBulk.working=!0,t.imagify.optionsBulk.processIsStopped=!1,this.$button.prop("disabled",!0).find(".dashicons").addClass("rotate"),t.imagify.beat.interval(15),t.imagify.beat.disableSuspend(),this.$missingWebpMessage.hide().attr("aria-hidden","true"),i=imagifyOptions.bulk.progress_next_gen.total-imagifyOptions.bulk.progress_next_gen.remaining,e=Math.floor(i/imagifyOptions.bulk.progress_next_gen.total*100),this.$progressBar.css("width",e+"%"),this.$progressText.text(i+"/"+imagifyOptions.bulk.progress_next_gen.total),this.$progressWrap.slideDown().attr("aria-hidden","false").removeClass("hidden"))},toggleButton:function(i){this.checked?i.data.imagifyOptionsBulk.$button.prop("disabled",!1):i.data.imagifyOptionsBulk.$button.prop("disabled",!0)},maybeLaunchMissingWebpProcess:function(i){!i.data.imagifyOptionsBulk||i.data.imagifyOptionsBulk.working||i.data.imagifyOptionsBulk.hasBlockingError(!0)||(i.data.imagifyOptionsBulk.error=!1,i.data.imagifyOptionsBulk.working=!0,i.data.imagifyOptionsBulk.processIsStopped=!1,i.data.imagifyOptionsBulk.$button.prop("disabled",!0).find(".dashicons").addClass("rotate"),t.imagify.beat.interval(15),t.imagify.beat.disableSuspend(),i.data.imagifyOptionsBulk.launchProcess())},addQueueImagifybeat:function(i,e){e[imagifyOptions.bulk.imagifybeatIDs.progress]=imagifyOptions.bulk.contexts},processQueueImagifybeat:function(i,e){var a,t;i.data.imagifyOptionsBulk&&void 0===e[imagifyOptions.bulk.imagifybeatIDs.progress]||(i.data.imagifyOptionsBulk.processIsStopped||0===(e=e[imagifyOptions.bulk.imagifybeatIDs.progress]).remaining?i.data.imagifyOptionsBulk.processFinished():(a=e.total-e.remaining,t=Math.floor(a/e.total*100),i.data.imagifyOptionsBulk.$progressBar.css("width",t+"%"),i.data.imagifyOptionsBulk.$progressText.text(a+"/"+e.total)))},addRequirementsImagifybeat:function(i,e){e[imagifyOptions.bulk.imagifybeatIDs.requirements]=1},processRequirementsImagifybeat:function(i,e){i.data.imagifyOptionsBulk&&void 0===e[imagifyOptions.bulk.imagifybeatIDs.requirements]||(e=e[imagifyOptions.bulk.imagifybeatIDs.requirements],imagifyOptions.bulk.curlMissing=e.curl_missing,imagifyOptions.bulk.editorMissing=e.editor_missing,imagifyOptions.bulk.extHttpBlocked=e.external_http_blocked,imagifyOptions.bulk.apiDown=e.api_down,imagifyOptions.bulk.keyIsValid=e.key_is_valid,imagifyOptions.bulk.isOverQuota=e.is_over_quota)},launchProcess:function(){var a;this.processIsStopped||s.get((a=this).getAjaxUrl("MissingNextGen",imagifyOptions.bulk.contexts)).done(function(i){var e;a.processIsStopped||(e=i.data&&i.data.message?i.data.message:imagifyOptions.bulk.ajaxErrorText,i.success?0===i.data.total?a.stopProcess("no-images"):(a.$missingWebpMessage.hide().attr("aria-hidden","true"),a.$progressText.text("0"+(i.data.total?"/"+i.data.total:"")),a.$progressWrap.slideDown().attr("aria-hidden","false").removeClass("hidden")):a.error||a.stopProcess(e))}).fail(function(){a.error||a.stopProcess("get-unoptimized-images")})},processFinished:function(){var i={};!1!==this.error&&(i="invalid-api-key"===this.error?{title:imagifyOptions.bulk.labels.invalidAPIKeyTitle,type:"info"}:"over-quota"===this.error?{title:imagifyOptions.bulk.labels.overQuotaTitle,html:s("#tmpl-imagify-overquota-alert").html(),type:"info",customClass:"imagify-swal-has-subtitle imagify-swal-error-header",showConfirmButton:!1}:"get-unoptimized-images"===this.error?{title:imagifyOptions.bulk.labels.getUnoptimizedImagesErrorTitle,html:imagifyOptions.bulk.labels.getUnoptimizedImagesErrorText,type:"info"}:"no-images"===this.error?{title:imagifyOptions.bulk.labels.nothingToDoTitle,html:imagifyOptions.bulk.labels.nothingToDoText,type:"info"}:"no-backup"===this.error?{title:imagifyOptions.bulk.labels.nothingToDoTitle,html:imagifyOptions.bulk.labels.nothingToDoNoBackupText,type:"info"}:{title:imagifyOptions.bulk.labels.error,html:this.error,type:"info"},this.displayError(i),this.error=!1),this.working=!1,this.processIsStopped=!1,t.imagify.beat.resetInterval(),t.imagify.beat.enableSuspend(),this.$progressWrap.slideUp().attr("aria-hidden","true").addClass("hidden"),this.$progressText.text("0"),this.$missingWebpElement.hide().attr("aria-hidden","true"),this.$button.find(".dashicons").removeClass("rotate")},hasBlockingError:function(i){return i=void 0!==i&&i,imagifyOptions.bulk.curlMissing?(i&&this.displayError({html:imagifyOptions.bulk.labels.curlMissing}),!0):imagifyOptions.bulk.editorMissing?(i&&this.displayError({html:imagifyOptions.bulk.labels.editorMissing}),!0):imagifyOptions.bulk.extHttpBlocked?(i&&this.displayError({html:imagifyOptions.bulk.labels.extHttpBlocked}),!0):imagifyOptions.bulk.apiDown?(i&&this.displayError({html:imagifyOptions.bulk.labels.apiDown}),!0):imagifyOptions.bulk.keyIsValid?!!imagifyOptions.bulk.isOverQuota&&(i&&this.displayError({title:imagifyOptions.bulk.labels.overQuotaTitle,html:s("#tmpl-imagify-overquota-alert").html(),type:"info",customClass:"imagify-swal-has-subtitle imagify-swal-error-header",showConfirmButton:!1}),!0):(i&&this.displayError({title:imagifyOptions.bulk.labels.invalidAPIKeyTitle,type:"info"}),!0)},displayError:function(i,e,a){var t={title:"",html:"",type:"error",customClass:"",width:620,padding:0,showCloseButton:!0,showConfirmButton:!0};(a=s.isPlainObject(i)?s.extend({},t,i):s.extend({},t,{title:i||"",html:e||""},a=a||{})).title=a.title||imagifyOptions.bulk.labels.error,a.customClass+=" imagify-sweet-alert",swal(a).catch(swal.noop)},getAjaxUrl:function(i,e){var a=ajaxurl+t.imagify.concat+"_wpnonce="+imagifyOptions.bulk.ajaxNonce;return(a+="&action="+imagifyOptions.bulk.ajaxActions[i])+("&context="+e.join("_"))},stopProcess:function(i){this.processIsStopped=!0,this.error=i,this.processFinished()}},t.imagify.optionsBulk.init())}(window,document,jQuery),function(n){var t=n.propHooks.checked;n.propHooks.checked={set:function(i,e,a){e=void 0===t?i[a]=e:t(i,e,a);return n(i).trigger("change.imagify"),e}},n(".imagify-select-all").on("click.imagify",function(){var i=n(this),e=i.data("action"),a=i.closest(".imagify-select-all-buttons"),t=a.prev(".imagify-check-group"),s="imagify-is-inactive";if(i.hasClass(s))return!1;a.find(".imagify-select-all").removeClass(s).attr("aria-disabled","false"),i.addClass(s).attr("aria-disabled","true"),t.find(".imagify-row-check").prop("checked",function(){return!n(this).is(":hidden,:disabled")&&"select"===e})}),n(".imagify-check-group .imagify-row-check").on("change.imagify",function(){var i=n(this).closest(".imagify-check-group"),e=i.find(".imagify-row-check"),a=e.filter(":visible:enabled").length,e=e.filter(":visible:enabled:checked").length,i=i.next(".imagify-select-all-buttons"),t="imagify-is-inactive";0===e&&i.find('[data-action="unselect"]').addClass(t).attr("aria-disabled","true"),e===a&&i.find('[data-action="select"]').addClass(t).attr("aria-disabled","true"),e!==a&&0 + AddType image/avif .avif +' ); + } +} diff --git a/classes/Avif/Display.php b/classes/Avif/Display.php new file mode 100644 index 000000000..0fba80617 --- /dev/null +++ b/classes/Avif/Display.php @@ -0,0 +1,169 @@ + [ 'maybe_add_rewrite_rules', 12 ], + 'imagify_activation' => 'activate', + 'imagify_deactivation' => 'deactivate', + ]; + } + + /** + * If display Next-Gen images, add the AVIF type to the .htaccess/etc file. + * + * @since 1.9 + * + * @param array $values The option values. + * + * @return array + */ + public function maybe_add_rewrite_rules( $values ) { + if ( ! $this->get_server_conf() ) { + return $values; + } + + $enabled = isset( $values['display_nextgen'] ) ? true : false; + $result = false; + + if ( $enabled ) { + // Add the AVIF file type. + $result = $this->get_server_conf()->add(); + } elseif ( ! $enabled ) { + // Remove the AVIF file type. + $result = $this->get_server_conf()->remove(); + } + + if ( ! is_wp_error( $result ) ) { + return $values; + } + + // Display an error message. + if ( is_multisite() && strpos( wp_get_referer(), network_admin_url( '/' ) ) === 0 ) { + Notices::get_instance()->add_network_temporary_notice( $result->get_error_message() ); + + return $values; + } + + Notices::get_instance()->add_site_temporary_notice( $result->get_error_message() ); + + return $values; + } + + /** + * Add rules on plugin activation. + * + * @since 1.9 + */ + public function activate() { + $conf = $this->get_server_conf(); + + if ( ! $conf ) { + return; + } + + if ( ! get_imagify_option( 'display_nextgen' ) ) { + return; + } + + if ( is_wp_error( $conf->is_file_writable() ) ) { + return; + } + + $conf->add(); + } + + /** + * Remove rules on plugin deactivation. + * + * @since 1.9 + */ + public function deactivate() { + $conf = $this->get_server_conf(); + + if ( ! $conf ) { + return; + } + + $file_path = $conf->get_file_path(); + $filesystem = \Imagify_Filesystem::get_instance(); + + if ( ! $filesystem->exists( $file_path ) ) { + return; + } + if ( ! $filesystem->is_writable( $file_path ) ) { + return; + } + + $conf->remove(); + } + + /** + * Get the path to the directory conf file. + * + * @since 1.9 + * + * @param bool $relative True to get a path relative to the site’s root. + * @return string|bool The file path. False on failure. + */ + public function get_file_path( $relative = false ) { + if ( ! $this->get_server_conf() ) { + return false; + } + + $file_path = $this->get_server_conf()->get_file_path(); + + if ( $relative ) { + return \Imagify_Filesystem::get_instance()->make_path_relative( $file_path ); + } + + return $file_path; + } + + /** + * Get the server conf instance. + * Note: nothing needed for nginx. + * + * @since 1.9 + * + * @return WriteFileInterface + */ + protected function get_server_conf() { + global $is_apache, $is_iis7; + + if ( isset( $this->server_conf ) ) { + return $this->server_conf; + } + + if ( $is_apache ) { + $this->server_conf = new Apache(); + } elseif ( $is_iis7 ) { + $this->server_conf = new IIS(); + } + + return $this->server_conf; + } +} diff --git a/classes/Avif/IIS.php b/classes/Avif/IIS.php new file mode 100644 index 000000000..cc8dcd36c --- /dev/null +++ b/classes/Avif/IIS.php @@ -0,0 +1,32 @@ + + + +' ); + } +} diff --git a/classes/Avif/RewriteRules/Apache.php b/classes/Avif/RewriteRules/Apache.php new file mode 100644 index 000000000..c970aeb87 --- /dev/null +++ b/classes/Avif/RewriteRules/Apache.php @@ -0,0 +1,59 @@ +get_extensions_pattern(); + $extensions = str_replace( '|avif', '', $extensions ); + $home_root = wp_parse_url( home_url( '/' ) ); + $home_root = $home_root['path']; + + return trim( ' + + # Vary: Accept for all the requests to jpeg, png, and gif. + SetEnvIf Request_URI "\.(' . $extensions . ')$" REQUEST_image + + + + RewriteEngine On + RewriteBase ' . $home_root . ' + + # Check if browser supports AVIF images. + # Update the MIME type accordingly. + RewriteCond %{HTTP_ACCEPT} image/avif + + # Check if AVIF replacement image exists. + RewriteCond %{REQUEST_FILENAME}.avif -f + + # Serve AVIF image instead. + RewriteRule (.+)\.(' . $extensions . ')$ $1.$2.avif [T=image/avif,NC] + + + + # Update the MIME type accordingly. + Header append Vary Accept env=REQUEST_image +' ); + } +} diff --git a/classes/Avif/RewriteRules/Display.php b/classes/Avif/RewriteRules/Display.php new file mode 100644 index 000000000..0d54ccbe0 --- /dev/null +++ b/classes/Avif/RewriteRules/Display.php @@ -0,0 +1,224 @@ + [ 'maybe_add_rewrite_rules', 11 ], + 'imagify_settings_webp_info' => 'maybe_add_avif_info', + 'imagify_activation' => 'activate', + 'imagify_deactivation' => 'deactivate', + ]; + } + + /** + * If display AVIF images via rewrite rules, add the rules to the .htaccess/etc file. + * + * @since 1.9 + * + * @param array $values The option values. + * + * @return array + */ + public function maybe_add_rewrite_rules( $values ) { + $was_enabled = (bool) get_imagify_option( 'display_nextgen' ); + $is_enabled = ! empty( $values['display_nextgen'] ); + + // Which method? + $old_value = get_imagify_option( 'display_nextgen_method' ); + $new_value = ! empty( $values['display_nextgen_method'] ) ? $values['display_nextgen_method'] : ''; + + // Decide when to add or remove rules. + $is_rewrite = self::OPTION_VALUE === $new_value; + $was_rewrite = self::OPTION_VALUE === $old_value; + + if ( ! $this->get_server_conf() ) { + return $values; + } + + $result = false; + + if ( $is_enabled && $is_rewrite && ( ! $was_enabled || ! $was_rewrite ) ) { + // Add the rewrite rules. + $result = $this->get_server_conf()->add(); + } elseif ( $was_enabled && $was_rewrite && ( ! $is_enabled || ! $is_rewrite ) ) { + // Remove the rewrite rules. + $result = $this->get_server_conf()->remove(); + } + + if ( ! is_wp_error( $result ) ) { + return $values; + } + + // Display an error message. + if ( is_multisite() && strpos( wp_get_referer(), network_admin_url( '/' ) ) === 0 ) { + Notices::get_instance()->add_network_temporary_notice( $result->get_error_message() ); + + return $values; + } + + Notices::get_instance()->add_site_temporary_notice( $result->get_error_message() ); + + return $values; + } + + /** + * If the conf file is not writable, add a warning. + */ + public function maybe_add_avif_info() { + $conf = $this->get_server_conf(); + + if ( ! $conf ) { + return; + } + + $writable = $conf->is_file_writable(); + + if ( is_wp_error( $writable ) ) { + $rules = $conf->get_new_contents(); + + if ( ! $rules ) { + // Uh? + return; + } + + printf( + /* translators: %s is a file name. */ + esc_html__( 'If you choose to use rewrite rules, you will have to add the following lines manually to the %s file:', 'imagify' ), + '' . $this->get_file_path( true ) . '' + ); + + echo '
' . esc_html( $rules ) . '
'; + } + } + + /** + * Add rules on plugin activation. + */ + public function activate() { + $conf = $this->get_server_conf(); + + if ( ! $conf ) { + return; + } + + if ( ! get_imagify_option( 'display_nextgen' ) ) { + return; + } + + if ( self::OPTION_VALUE !== get_imagify_option( 'display_nextgen_method' ) ) { + return; + } + + if ( is_wp_error( $conf->is_file_writable() ) ) { + return; + } + + $conf->add(); + } + + /** + * Remove rules on plugin deactivation. + * + * @since 1.9 + */ + public function deactivate() { + $conf = $this->get_server_conf(); + + if ( ! $conf ) { + return; + } + + if ( ! get_imagify_option( 'display_nextgen' ) ) { + return; + } + + if ( self::OPTION_VALUE !== get_imagify_option( 'display_nextgen_method' ) ) { + return; + } + + $file_path = $conf->get_file_path(); + $filesystem = \Imagify_Filesystem::get_instance(); + + if ( ! $filesystem->exists( $file_path ) ) { + return; + } + if ( ! $filesystem->is_writable( $file_path ) ) { + return; + } + + $conf->remove(); + } + + /** + * Get the path to the directory conf file. + * + * @param bool $relative True to get a path relative to the site’s root. + * + * @return string|bool The file path. False on failure. + */ + public function get_file_path( $relative = false ) { + if ( ! $this->get_server_conf() ) { + return false; + } + + $file_path = $this->get_server_conf()->get_file_path(); + + if ( $relative ) { + return \Imagify_Filesystem::get_instance()->make_path_relative( $file_path ); + } + + return $file_path; + } + + /** + * Get the server conf instance. + * + * @return WriteFileInterface + */ + protected function get_server_conf() { + global $is_apache, $is_iis7, $is_nginx; + + if ( isset( $this->server_conf ) ) { + return $this->server_conf; + } + + if ( $is_apache ) { + $this->server_conf = new Apache(); + } elseif ( $is_iis7 ) { + $this->server_conf = new IIS(); + } elseif ( $is_nginx ) { + $this->server_conf = new Nginx(); + } + + return $this->server_conf; + } +} diff --git a/classes/Avif/RewriteRules/IIS.php b/classes/Avif/RewriteRules/IIS.php new file mode 100644 index 000000000..8c4295979 --- /dev/null +++ b/classes/Avif/RewriteRules/IIS.php @@ -0,0 +1,58 @@ +get_extensions_pattern(); + $extensions = str_replace( '|avif', '', $extensions ); + $home_root = wp_parse_url( home_url( '/' ) ); + $home_root = $home_root['path']; + + return trim( ' + + + + + + + + + + + + + + + + + + + + + + +' ); + } +} diff --git a/classes/Avif/RewriteRules/Nginx.php b/classes/Avif/RewriteRules/Nginx.php new file mode 100644 index 000000000..7855cb1b5 --- /dev/null +++ b/classes/Avif/RewriteRules/Nginx.php @@ -0,0 +1,30 @@ +getContainer()->share( 'avif_display', Display::class ); + $this->getContainer()->share( 'avif_rewrite_rules', RewriteRules::class ); + } + + /** + * Returns the subscribers array + * + * @return array + */ + public function get_subscribers() { + return $this->subscribers; + } +} diff --git a/classes/Bulk/AbstractBulk.php b/classes/Bulk/AbstractBulk.php index 4b5b881c8..8bd71f16e 100644 --- a/classes/Bulk/AbstractBulk.php +++ b/classes/Bulk/AbstractBulk.php @@ -105,13 +105,19 @@ function_exists( 'set_time_limit' ) } /** - * Tell if there are optimized media without WebP versions. + * Tell if there are optimized media without next-gen versions. * * @since 1.9 * * @return int The number of media. */ - public function has_optimized_media_without_webp() { - return count( $this->get_optimized_media_ids_without_webp()['ids'] ); + public function has_optimized_media_without_nextgen() { + $format = 'webp'; + + if ( get_imagify_option( 'convert_to_avif' ) ) { + $format = 'avif'; + } + + return count( $this->get_optimized_media_ids_without_format( $format )['ids'] ); } } diff --git a/classes/Bulk/Bulk.php b/classes/Bulk/Bulk.php index ef13f783a..941201750 100644 --- a/classes/Bulk/Bulk.php +++ b/classes/Bulk/Bulk.php @@ -17,15 +17,15 @@ class Bulk { */ public function init() { add_action( 'imagify_optimize_media', [ $this, 'optimize_media' ], 10, 3 ); - add_action( 'imagify_convert_webp', [ $this, 'generate_webp_versions' ], 10, 2 ); - add_action( 'imagify_convert_webp_finished', [ $this, 'clear_webp_transients' ], 10, 2 ); + add_action( 'imagify_convert_next_gen', [ $this, 'generate_nextgen_versions' ], 10, 2 ); add_action( 'wp_ajax_imagify_bulk_optimize', [ $this, 'bulk_optimize_callback' ] ); - add_action( 'wp_ajax_imagify_missing_webp_generation', [ $this, 'missing_webp_callback' ] ); + add_action( 'wp_ajax_imagify_missing_nextgen_generation', [ $this, 'missing_nextgen_callback' ] ); add_action( 'wp_ajax_imagify_get_folder_type_data', [ $this, 'get_folder_type_data_callback' ] ); add_action( 'wp_ajax_imagify_bulk_info_seen', [ $this, 'bulk_info_seen_callback' ] ); add_action( 'wp_ajax_imagify_bulk_get_stats', [ $this, 'bulk_get_stats_callback' ] ); add_action( 'imagify_after_optimize', [ $this, 'check_optimization_status' ], 10, 2 ); add_action( 'imagify_deactivation', [ $this, 'delete_transients_data' ] ); + add_action( 'update_option_imagify_settings', [ $this, 'maybe_generate_missing_nextgen' ], 10, 2 ); } /** @@ -37,7 +37,7 @@ public function delete_transients_data() { delete_transient( 'imagify_custom-folders_optimize_running' ); delete_transient( 'imagify_wp_optimize_running' ); delete_transient( 'imagify_bulk_optimization_complete' ); - delete_transient( 'imagify_missing_webp_total' ); + delete_transient( 'imagify_missing_next_gen_total' ); } /** @@ -173,17 +173,31 @@ public function run_optimize( string $context, int $optimization_level ) { 'message' => 'over-quota', ]; } + $formats = imagify_nextgen_images_formats(); + $media_ids = [ + 'ids' => [], + 'errors' => [ + 'no_file_path' => [], + 'no_backup' => [], + ], + ]; + foreach ( $formats as $format ) { + $result = $this->get_bulk_instance( $context )->get_optimized_media_ids_without_format( $format ); + $media_ids['ids'] = array_merge( $media_ids['ids'], $result['ids'] ); + } + $get_unoptimized_media_ids = $this->get_bulk_instance( $context )->get_unoptimized_media_ids( $optimization_level ); - $media_ids = $this->get_bulk_instance( $context )->get_unoptimized_media_ids( $optimization_level ); + $media_ids['ids'] = array_merge( $media_ids['ids'], $get_unoptimized_media_ids ); - if ( empty( $media_ids ) ) { + if ( empty( $media_ids['ids'] ) ) { return [ 'success' => false, 'message' => 'no-images', ]; } + $media_ids['ids'] = array_unique( $media_ids['ids'] ); - foreach ( $media_ids as $media_id ) { + foreach ( $media_ids['ids'] as $media_id ) { try { as_enqueue_async_action( 'imagify_optimize_media', @@ -213,13 +227,14 @@ public function run_optimize( string $context, int $optimization_level ) { } /** - * Runs the WebP generation + * Runs the next-gen generation * * @param array $contexts An array of contexts (WP/Custom folders). + * @param array $formats An array of format to generate. * * @return array */ - public function run_generate_webp( array $contexts ) { + public function run_generate_nextgen( array $contexts, array $formats ) { if ( ! $this->can_optimize() ) { return [ 'success' => false, @@ -227,28 +242,29 @@ public function run_generate_webp( array $contexts ) { ]; } - delete_transient( 'imagify_stat_without_webp' ); + delete_transient( 'imagify_stat_without_next_gen' ); $medias = []; foreach ( $contexts as $context ) { - $media = $this->get_bulk_instance( $context )->get_optimized_media_ids_without_webp(); + foreach ( $formats as $format ) { + $media = $this->get_bulk_instance( $context )->get_optimized_media_ids_without_format( $format ); + if ( ! $media['ids'] && $media['errors']['no_backup'] ) { + // No backup, no next-gen. + return [ + 'success' => false, + 'message' => 'no-backup', + ]; + } elseif ( ! $media['ids'] && $media['errors']['no_file_path'] ) { + // Error. + return [ + 'success' => false, + 'message' => __( 'The path to the selected files could not be retrieved.', 'imagify' ), + ]; + } - if ( ! $media['ids'] && $media['errors']['no_backup'] ) { - // No backup, no WebP. - return [ - 'success' => false, - 'message' => 'no-backup', - ]; - } elseif ( ! $media['ids'] && $media['errors']['no_file_path'] ) { - // Error. - return [ - 'success' => false, - 'message' => __( 'The path to the selected files could not be retrieved.', 'imagify' ), - ]; + $medias[ $context ] = $media['ids']; } - - $medias[ $context ] = $media['ids']; } if ( empty( $medias ) ) { @@ -266,12 +282,12 @@ public function run_generate_webp( array $contexts ) { foreach ( $media_ids as $media_id ) { try { as_enqueue_async_action( - 'imagify_convert_webp', + 'imagify_convert_next_gen', [ 'id' => $media_id, 'context' => $context, ], - "imagify-{$context}-convert-webp" + "imagify-{$context}-convert-nextgen" ); } catch ( Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // nothing to do. @@ -279,7 +295,7 @@ public function run_generate_webp( array $contexts ) { } } - set_transient( 'imagify_missing_webp_total', $total, HOUR_IN_SECONDS ); + set_transient( 'imagify_missing_next_gen_total', $total, HOUR_IN_SECONDS ); return [ 'success' => true, @@ -310,13 +326,13 @@ private function get_bulk_class_name( string $context ): string { } /** - * Filter the name of the class to use for bulk process. - * - * @since 1.9 - * - * @param int $class_name The class name. - * @param string $context The context name. - */ + * Filter the name of the class to use for bulk process. + * + * @since 1.9 + * + * @param int $class_name The class name. + * @param string $context The context name. + */ $class_name = apply_filters( 'imagify_bulk_class_name', $class_name, $context ); return '\\' . ltrim( $class_name, '\\' ); @@ -374,7 +390,7 @@ private function force_optimize( int $media_id, string $context, int $level ) { } /** - * Generate WebP images if they are missing. + * Generate next-gen images if they are missing. * * @since 2.1 * @@ -383,12 +399,12 @@ private function force_optimize( int $media_id, string $context, int $level ) { * * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ - public function generate_webp_versions( int $media_id, string $context ) { + public function generate_nextgen_versions( int $media_id, string $context ) { if ( ! $this->can_optimize() ) { return false; } - return imagify_get_optimization_process( $media_id, $context )->generate_webp_versions(); + return imagify_get_optimization_process( $media_id, $context )->generate_nextgen_versions(); } /** @@ -451,10 +467,6 @@ public function get_optimization_level( $method = 'GET', $parameter = 'optimizat return (int) $level; } - /** ----------------------------------------------------------------------------------------- */ - /** BULK OPTIMIZATION CALLBACKS ============================================================= */ - /** ----------------------------------------------------------------------------------------- */ - /** * Launch the bulk optimization action * @@ -480,14 +492,14 @@ public function bulk_optimize_callback() { } /** - * Launch the missing WebP versions generation + * Launch the missing Next-gen versions generation * * @return void */ - public function missing_webp_callback() { + public function missing_nextgen_callback() { imagify_check_nonce( 'imagify-bulk-optimize' ); - $contexts = explode( '_', sanitize_key( wp_unslash( $_GET['context'] ) ) ); + $contexts = $this->get_contexts(); foreach ( $contexts as $context ) { if ( ! imagify_get_context( $context )->current_user_can( 'bulk-optimize' ) ) { @@ -495,8 +507,9 @@ public function missing_webp_callback() { } } - $data = $this->run_generate_webp( $contexts ); + $formats = imagify_nextgen_images_formats(); + $data = $this->run_generate_nextgen( $contexts, $formats ); if ( false === $data['success'] ) { wp_send_json_error( [ 'message' => $data['message'] ] ); } @@ -575,4 +588,87 @@ public function bulk_get_stats_callback() { wp_send_json_success( imagify_get_bulk_stats( array_flip( $folder_types ) ) ); } + + /** + * Update Options callback to start bulk optimization. + * + * @since 2.2 + * + * @param array $old_value The old option value. + * @param array $value The new option value. + * + * Please note that the convert_to_avif new value is a checkbox, + * so it equals 1 when it's set otherwise it's not set. + * That's why we need to use empty function when checking its value. + * + * @return void + */ + public function maybe_generate_missing_nextgen( $old_value, $value ) { + if ( empty( $old_value['convert_to_avif'] ) === empty( $value['convert_to_avif'] ) ) { + // Old value = new value so do nothing. + return; + } + + if ( empty( $value['convert_to_avif'] ) ) { + // new value is disabled, do nothing. + return; + } + + $contexts = $this->get_contexts(); + $formats = imagify_nextgen_images_formats(); + + $this->run_generate_nextgen( $contexts, $formats ); + } + + /** + * Get the context for the bulk optimization page. + * + * @since 2.2 + * + * @return array The array of unique contexts ('wp' or 'custom-folders'). + */ + public function get_contexts() { + $contexts = []; + $types = []; + + // Library: in each site. + if ( ! is_network_admin() ) { + $types['library|wp'] = 1; + } + + // Custom folders: in network admin only if network activated, in each site otherwise. + if ( imagify_can_optimize_custom_folders() && ( imagify_is_active_for_network() && is_network_admin() || ! imagify_is_active_for_network() ) ) { + $types['custom-folders|custom-folders'] = 1; + } + + /** + * Filter the types to display in the bulk optimization page. + * + * @since 1.7.1 + * + * @param array $types The folder types displayed on the page. If a folder type is "library", the context should be suffixed after a pipe character. They are passed as array keys. + */ + $types = apply_filters( 'imagify_bulk_page_types', $types ); + $types = array_filter( (array) $types ); + + if ( isset( $types['library|wp'] ) && ! in_array( 'wp', $contexts, true ) ) { + $contexts[] = 'wp'; + } + + if ( isset( $types['custom-folders|custom-folders'] ) ) { + $folders_instance = \Imagify_Folders_DB::get_instance(); + + if ( ! $folders_instance->has_items() ) { + // New Feature! + if ( ! in_array( 'wp', $contexts, true ) ) { + $contexts[] = 'wp'; + } + } elseif ( $folders_instance->has_active_folders() && ! in_array( 'custom-folders', $contexts, true ) ) { + $contexts[] = 'custom-folders'; + } + } + + return $contexts; + } + } diff --git a/classes/Bulk/BulkInterface.php b/classes/Bulk/BulkInterface.php index 5e4adeba8..b599f6b32 100644 --- a/classes/Bulk/BulkInterface.php +++ b/classes/Bulk/BulkInterface.php @@ -18,10 +18,11 @@ interface BulkInterface { public function get_unoptimized_media_ids( $optimization_level ); /** - * Get ids of all optimized media without WebP versions. + * Get ids of all optimized media without Next gen versions. * - * @since 1.9 - * @since 1.9.5 The method doesn't return the IDs directly anymore. + * @since 2.2 + * + * @param string $format Format we are looking for. (webp|avif). * * @return array { * @type array $ids A list of media IDs. @@ -31,16 +32,17 @@ public function get_unoptimized_media_ids( $optimization_level ); * } * } */ - public function get_optimized_media_ids_without_webp(); + public function get_optimized_media_ids_without_format( $format ); + /** - * Tell if there are optimized media without WebP versions. + * Tell if there are optimized media without next-gen versions. * - * @since 1.9 + * @since 2.2 * * @return int The number of media. */ - public function has_optimized_media_without_webp(); + public function has_optimized_media_without_nextgen(); /** * Get the context data. diff --git a/classes/Bulk/CustomFolders.php b/classes/Bulk/CustomFolders.php index 3717aaf31..617df0e5b 100644 --- a/classes/Bulk/CustomFolders.php +++ b/classes/Bulk/CustomFolders.php @@ -74,10 +74,11 @@ public function get_unoptimized_media_ids( $optimization_level ) { } /** - * Get ids of all optimized media without WebP versions. + * Get ids of all optimized media without Next gen versions. * - * @since 1.9 - * @since 1.9.5 The method doesn't return the IDs directly anymore. + * @since 2.2 + * + * @param string $format Format we are looking for. (webp|avif). * * @return array { * @type array $ids A list of media IDs. @@ -87,7 +88,7 @@ public function get_unoptimized_media_ids( $optimization_level ) { * } * } */ - public function get_optimized_media_ids_without_webp() { + public function get_optimized_media_ids_without_format( $format ) { global $wpdb; $this->set_no_time_limit(); @@ -95,9 +96,22 @@ public function get_optimized_media_ids_without_webp() { $files_table = Imagify_Files_DB::get_instance()->get_table_name(); $folders_table = Imagify_Folders_DB::get_instance()->get_table_name(); $mime_types = Imagify_DB::get_mime_types( 'image' ); - $mime_types = str_replace( ",'image/webp'", '', $mime_types ); - $webp_suffix = constant( imagify_get_optimization_process_class_name( 'custom-folders' ) . '::WEBP_SUFFIX' ); - $files = $wpdb->get_results( $wpdb->prepare( // WPCS: unprepared SQL ok. + // Remove single quotes and explode string into array. + $mime_types_array = explode( ',', str_replace( "'", '', $mime_types ) ); + + // Iterate over array and check if string contains input. + foreach ( $mime_types_array as $item ) { + if ( strpos( $item, $format ) !== false ) { + $mime = $item; + break; + } + } + if ( ! isset( $mime ) && empty( $mime ) ) { + $mime = 'image/webp'; + } + $mime_types = str_replace( ",'" . $mime . "'", '', $mime_types ); + $nextgen_suffix = constant( imagify_get_optimization_process_class_name( 'custom-folders' ) . '::' . strtoupper( $format ) . '_SUFFIX' ); + $files = $wpdb->get_results( $wpdb->prepare( // WPCS: unprepared SQL ok. " SELECT fi.file_id, fi.path FROM $files_table as fi @@ -108,11 +122,11 @@ public function get_optimized_media_ids_without_webp() { AND ( fi.status = 'success' OR fi.status = 'already_optimized' ) AND ( fi.data NOT LIKE %s OR fi.data IS NULL ) ORDER BY fi.file_id DESC", - '%' . $wpdb->esc_like( $webp_suffix . '";a:4:{s:7:"success";b:1;' ) . '%' + '%' . $wpdb->esc_like( $nextgen_suffix . '";a:4:{s:7:"success";b:1;' ) . '%' ) ); $wpdb->flush(); - unset( $mime_types, $files_table, $folders_table, $webp_suffix ); + unset( $mime_types, $files_table, $folders_table, $nextgen_suffix, $mime ); $data = [ 'ids' => [], diff --git a/classes/Bulk/Noop.php b/classes/Bulk/Noop.php index e43bc03b8..971967c65 100644 --- a/classes/Bulk/Noop.php +++ b/classes/Bulk/Noop.php @@ -20,10 +20,11 @@ public function get_unoptimized_media_ids( $optimization_level ) { } /** - * Get ids of all optimized media without WebP versions. + * * Get ids of all optimized media without Next gen versions. * - * @since 1.9 - * @since 1.9.5 The method doesn't return the IDs directly anymore. + * @since 2.2 + * + * @param string $format Format we are looking for. (webp|avif). * * @return array { * @type array $ids A list of media IDs. @@ -33,7 +34,7 @@ public function get_unoptimized_media_ids( $optimization_level ) { * } * } */ - public function get_optimized_media_ids_without_webp() { + public function get_optimized_media_ids_without_format( $format ) { return [ 'ids' => [], 'errors' => [ diff --git a/classes/Bulk/WP.php b/classes/Bulk/WP.php index bb5e309bf..ddbddeefd 100644 --- a/classes/Bulk/WP.php +++ b/classes/Bulk/WP.php @@ -165,10 +165,11 @@ public function get_unoptimized_media_ids( $optimization_level ) { } /** - * Get ids of all optimized media without WebP versions. + * Get ids of all optimized media without Next gen versions. * - * @since 1.9 - * @since 1.9.5 The method doesn't return the IDs directly anymore. + * @since 2.2 + * + * @param string $format Format we are looking for. (webp|avif). * * @return array { * @type array $ids A list of media IDs. @@ -178,45 +179,61 @@ public function get_unoptimized_media_ids( $optimization_level ) { * } * } */ - public function get_optimized_media_ids_without_webp() { + public function get_optimized_media_ids_without_format( $format ) { global $wpdb; $this->set_no_time_limit(); $mime_types = Imagify_DB::get_mime_types( 'image' ); - $mime_types = str_replace( ",'image/webp'", '', $mime_types ); + + // Remove single quotes and explode string into array. + $mime_types_array = explode( ',', str_replace( "'", '', $mime_types ) ); + + // Iterate over array and check if string contains input. + foreach ( $mime_types_array as $item ) { + if ( strpos( $item, $format ) !== false ) { + $mime = $item; + break; + } + } + if ( ! isset( $mime ) && empty( $mime ) ) { + $mime = 'image/webp'; + } + $mime_types = str_replace( ",'" . $mime . "'", '', $mime_types ); $statuses = Imagify_DB::get_post_statuses(); $nodata_join = Imagify_DB::get_required_wp_metadata_join_clause(); $nodata_where = Imagify_DB::get_required_wp_metadata_where_clause( [ 'prepared' => true, ] ); - $webp_suffix = constant( imagify_get_optimization_process_class_name( 'wp' ) . '::WEBP_SUFFIX' ); + $nextgen_suffix = constant( imagify_get_optimization_process_class_name( 'wp' ) . '::' . strtoupper( $format ) . '_SUFFIX' ); + $ids = $wpdb->get_col( $wpdb->prepare( // WPCS: unprepared SQL ok. " - SELECT p.ID - FROM $wpdb->posts AS p - $nodata_join - LEFT JOIN $wpdb->postmeta AS mt1 - ON ( p.ID = mt1.post_id AND mt1.meta_key = '_imagify_status' ) - LEFT JOIN $wpdb->postmeta AS mt2 + SELECT p.ID + FROM $wpdb->posts AS p + $nodata_join + LEFT JOIN $wpdb->postmeta AS mt1 + ON ( p.ID = mt1.post_id AND mt1.meta_key = '_imagify_status' ) + LEFT JOIN $wpdb->postmeta AS mt2 ON ( p.ID = mt2.post_id AND mt2.meta_key = '_imagify_data' ) - WHERE - p.post_mime_type IN ( $mime_types ) - AND ( mt1.meta_value = 'success' OR mt1.meta_value = 'already_optimized' ) - AND mt2.meta_value NOT LIKE %s - AND p.post_type = 'attachment' - AND p.post_status IN ( $statuses ) - $nodata_where - ORDER BY p.ID DESC - LIMIT 0, %d", - '%' . $wpdb->esc_like( $webp_suffix . '";a:4:{s:7:"success";b:1;' ) . '%', + WHERE + p.post_mime_type IN ( $mime_types ) + AND (mt1.meta_key IS NULL OR mt1.meta_value = 'success' OR mt1.meta_value = 'already_optimized' ) + AND mt2.meta_value NOT LIKE %s + AND p.post_type = 'attachment' + AND p.post_status IN ( $statuses ) + $nodata_where + ORDER BY p.ID DESC + LIMIT 0, %d", + '%' . $wpdb->esc_like( $nextgen_suffix . '";a:4:{s:7:"success";b:1;' ) . '%', imagify_get_unoptimized_attachment_limit() ) ); $wpdb->flush(); - unset( $mime_types, $statuses, $webp_suffix ); + unset( $mime_types, $statuses, $nextgen_suffix, $mime ); $ids = array_filter( array_map( 'absint', $ids ) ); + $data = [ 'ids' => [], 'errors' => [ @@ -243,7 +260,7 @@ public function get_optimized_media_ids_without_webp() { * @param array $metas An array of the data fetched from the database. * @param string $context The context. */ - do_action( 'imagify_bulk_generate_webp_before_file_existence_tests', $ids, $metas, 'wp' ); + do_action( 'imagify_bulk_generate_nextgen_before_file_existence_tests', $ids, $metas, 'wp' ); foreach ( $ids as $i => $id ) { if ( empty( $metas['filenames'][ $id ] ) ) { diff --git a/classes/CDN/CDN.php b/classes/CDN/CDN.php new file mode 100644 index 000000000..8c21525f2 --- /dev/null +++ b/classes/CDN/CDN.php @@ -0,0 +1,123 @@ + 'get_cdn_source', + ]; + } + + /** + * Get the CDN "source". + * + * @since 1.9.3 + * + * @param string $option_url An URL to use instead of the one stored in the option. It is used only if no constant/filter. + * + * @return array { + * @type string $source Where does it come from? Possible values are 'constant', 'filter', or 'option'. + * @type string $name Who? Can be a constant name, a plugin name, or an empty string. + * @type string $url The CDN URL, with a trailing slash. An empty string if no URL is set. + * } + */ + public function get_cdn_source( $option_url = '' ) { + if ( defined( 'IMAGIFY_CDN_URL' ) && IMAGIFY_CDN_URL && is_string( IMAGIFY_CDN_URL ) ) { + // Use a constant. + $source = [ + 'source' => 'constant', + 'name' => 'IMAGIFY_CDN_URL', + 'url' => IMAGIFY_CDN_URL, + ]; + } else { + // Maybe use a filter. + $filter_source = [ + 'name' => null, + 'url' => null, + ]; + + /** + * Provide a custom CDN source. + * + * @since 1.9.3 + * + * @param array $filter_source { + * @type $name string The name of which provides the URL (plugin name, etc). + * @type $url string The CDN URL. + * } + */ + $filter_source = apply_filters( 'imagify_cdn_source', $filter_source ); + + if ( ! empty( $filter_source['url'] ) ) { + $source = [ + 'source' => 'filter', + 'name' => ! empty( $filter_source['name'] ) ? $filter_source['name'] : '', + 'url' => $filter_source['url'], + ]; + } + } + + if ( empty( $source['url'] ) ) { + // No constant, no filter: use the option. + $source = [ + 'source' => 'option', + 'name' => '', + 'url' => $option_url && is_string( $option_url ) ? $option_url : get_imagify_option( 'cdn_url' ), + ]; + } + + if ( empty( $source['url'] ) ) { + // Nothing set. + return [ + 'source' => 'option', + 'name' => '', + 'url' => '', + ]; + } + + $source['url'] = $this->sanitize_cdn_url( $source['url'] ); + + if ( empty( $source['url'] ) ) { + // Not an URL. + return [ + 'source' => 'option', + 'name' => '', + 'url' => '', + ]; + } + + return $source; + } + + /** + * Sanitize the CDN URL value. + * + * @since 1.9.3 + * + * @param string $url The URL to sanitize. + * + * @return string + */ + public function sanitize_cdn_url( $url ) { + $url = sanitize_text_field( $url ); + + if ( ! $url || ! preg_match( '@^https?://.+\.[^.]+@i', $url ) ) { + // Not an URL. + return ''; + } + + return trailingslashit( $url ); + } +} diff --git a/classes/CDN/ServiceProvider.php b/classes/CDN/ServiceProvider.php new file mode 100644 index 000000000..094900d35 --- /dev/null +++ b/classes/CDN/ServiceProvider.php @@ -0,0 +1,47 @@ +getContainer()->share( 'cdn', CDN::class ); + } + + /** + * Returns the subscribers array + * + * @return array + */ + public function get_subscribers() { + return $this->subscribers; + } +} diff --git a/classes/CLI/GenerateMissingWebpCommand.php b/classes/CLI/GenerateMissingNextgenCommand.php similarity index 57% rename from classes/CLI/GenerateMissingWebpCommand.php rename to classes/CLI/GenerateMissingNextgenCommand.php index 33cebc0e4..716a1541c 100644 --- a/classes/CLI/GenerateMissingWebpCommand.php +++ b/classes/CLI/GenerateMissingNextgenCommand.php @@ -6,9 +6,9 @@ use Imagify\Bulk\Bulk; /** - * Command class for the missing WebP generation + * Command class for the missing Nextgen generation */ -class GenerateMissingWebpCommand extends AbstractCommand { +class GenerateMissingNextgenCommand extends AbstractCommand { /** * Executes the command. * @@ -16,23 +16,23 @@ class GenerateMissingWebpCommand extends AbstractCommand { * @param array $options Optional arguments. */ public function __invoke( $arguments, $options ) { - Bulk::get_instance()->run_generate_webp( $arguments ); + Bulk::get_instance()->run_generate_nextgen( $arguments ); - \WP_CLI::log( 'Imagify missing WebP generation triggered.' ); + \WP_CLI::log( 'Imagify missing next-gen images generation triggered.' ); } /** * {@inheritdoc} */ protected function get_command_name(): string { - return 'generate-missing-webp'; + return 'generate-missing-nextgen'; } /** * {@inheritdoc} */ public function get_description(): string { - return 'Run the generation of the missing WebP versions'; + return 'Run the generation of the missing next-gen images versions'; } /** @@ -43,7 +43,7 @@ public function get_synopsis(): array { [ 'type' => 'positional', 'name' => 'contexts', - 'description' => 'The context(s) to run the missing WebP generation for. Possible values are wp and custom-folders.', + 'description' => 'The context(s) to run the missing next-gen images generation for. Possible values are wp and custom-folders.', 'optional' => false, 'repeating' => true, ], diff --git a/classes/Dependencies/League/Container/Argument/ArgumentResolverInterface.php b/classes/Dependencies/League/Container/Argument/ArgumentResolverInterface.php new file mode 100644 index 000000000..a56daeaad --- /dev/null +++ b/classes/Dependencies/League/Container/Argument/ArgumentResolverInterface.php @@ -0,0 +1,28 @@ +getValue(); + } elseif ($argument instanceof ClassNameInterface) { + $id = $argument->getClassName(); + } elseif (!is_string($argument)) { + return $argument; + } else { + $justStringValue = true; + $id = $argument; + } + + $container = null; + + try { + $container = $this->getLeagueContainer(); + } catch (ContainerException $e) { + if ($this instanceof ReflectionContainer) { + $container = $this; + } + } + + if ($container !== null) { + try { + return $container->get($id); + } catch (NotFoundException $exception) { + if ($argument instanceof ClassNameWithOptionalValue) { + return $argument->getOptionalValue(); + } + + if ($justStringValue) { + return $id; + } + + throw $exception; + } + } + + if ($argument instanceof ClassNameWithOptionalValue) { + return $argument->getOptionalValue(); + } + + // Just a string value. + return $id; + }, $arguments); + } + + /** + * {@inheritdoc} + */ + public function reflectArguments(ReflectionFunctionAbstract $method, array $args = []) : array + { + $arguments = array_map(function (ReflectionParameter $param) use ($method, $args) { + $name = $param->getName(); + $type = $param->getType(); + + if (array_key_exists($name, $args)) { + return new RawArgument($args[$name]); + } + + if ($type) { + if (PHP_VERSION_ID >= 70100) { + $typeName = $type->getName(); + } else { + $typeName = (string) $type; + } + + $typeName = ltrim($typeName, '?'); + + if ($param->isDefaultValueAvailable()) { + return new ClassNameWithOptionalValue($typeName, $param->getDefaultValue()); + } + + return new ClassName($typeName); + } + + if ($param->isDefaultValueAvailable()) { + return new RawArgument($param->getDefaultValue()); + } + + throw new NotFoundException(sprintf( + 'Unable to resolve a value for parameter (%s) in the function/method (%s)', + $name, + $method->getName() + )); + }, $method->getParameters()); + + return $this->resolveArguments($arguments); + } + + /** + * @return ContainerInterface + */ + abstract public function getContainer() : ContainerInterface; + + /** + * @return Container + */ + abstract public function getLeagueContainer() : Container; +} diff --git a/classes/Dependencies/League/Container/Argument/ClassName.php b/classes/Dependencies/League/Container/Argument/ClassName.php new file mode 100644 index 000000000..4d6ae395b --- /dev/null +++ b/classes/Dependencies/League/Container/Argument/ClassName.php @@ -0,0 +1,29 @@ +value = $value; + } + + /** + * {@inheritdoc} + */ + public function getClassName() : string + { + return $this->value; + } +} diff --git a/classes/Dependencies/League/Container/Argument/ClassNameInterface.php b/classes/Dependencies/League/Container/Argument/ClassNameInterface.php new file mode 100644 index 000000000..5a923e08a --- /dev/null +++ b/classes/Dependencies/League/Container/Argument/ClassNameInterface.php @@ -0,0 +1,13 @@ +className = $className; + $this->optionalValue = $optionalValue; + } + + /** + * @inheritDoc + */ + public function getClassName(): string + { + return $this->className; + } + + public function getOptionalValue() + { + return $this->optionalValue; + } +} diff --git a/classes/Dependencies/League/Container/Argument/RawArgument.php b/classes/Dependencies/League/Container/Argument/RawArgument.php new file mode 100644 index 000000000..e1efd4c70 --- /dev/null +++ b/classes/Dependencies/League/Container/Argument/RawArgument.php @@ -0,0 +1,29 @@ +value = $value; + } + + /** + * {@inheritdoc} + */ + public function getValue() + { + return $this->value; + } +} diff --git a/classes/Dependencies/League/Container/Argument/RawArgumentInterface.php b/classes/Dependencies/League/Container/Argument/RawArgumentInterface.php new file mode 100644 index 000000000..16fe992a9 --- /dev/null +++ b/classes/Dependencies/League/Container/Argument/RawArgumentInterface.php @@ -0,0 +1,13 @@ +definitions = $definitions ?? new DefinitionAggregate; + $this->providers = $providers ?? new ServiceProviderAggregate; + $this->inflectors = $inflectors ?? new InflectorAggregate; + + if ($this->definitions instanceof ContainerAwareInterface) { + $this->definitions->setLeagueContainer($this); + } + + if ($this->providers instanceof ContainerAwareInterface) { + $this->providers->setLeagueContainer($this); + } + + if ($this->inflectors instanceof ContainerAwareInterface) { + $this->inflectors->setLeagueContainer($this); + } + } + + /** + * Add an item to the container. + * + * @param string $id + * @param mixed $concrete + * @param boolean $shared + * + * @return DefinitionInterface + */ + public function add(string $id, $concrete = null, bool $shared = null) : DefinitionInterface + { + $concrete = $concrete ?? $id; + $shared = $shared ?? $this->defaultToShared; + + return $this->definitions->add($id, $concrete, $shared); + } + + /** + * Proxy to add with shared as true. + * + * @param string $id + * @param mixed $concrete + * + * @return DefinitionInterface + */ + public function share(string $id, $concrete = null) : DefinitionInterface + { + return $this->add($id, $concrete, true); + } + + /** + * Whether the container should default to defining shared definitions. + * + * @param boolean $shared + * + * @return self + */ + public function defaultToShared(bool $shared = true) : ContainerInterface + { + $this->defaultToShared = $shared; + + return $this; + } + + /** + * Get a definition to extend. + * + * @param string $id [description] + * + * @return DefinitionInterface + */ + public function extend(string $id) : DefinitionInterface + { + if ($this->providers->provides($id)) { + $this->providers->register($id); + } + + if ($this->definitions->has($id)) { + return $this->definitions->getDefinition($id); + } + + throw new NotFoundException( + sprintf('Unable to extend alias (%s) as it is not being managed as a definition', $id) + ); + } + + /** + * Add a service provider. + * + * @param ServiceProviderInterface|string $provider + * + * @return self + */ + public function addServiceProvider($provider) : self + { + $this->providers->add($provider); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function get($id, bool $new = false) + { + if ($this->definitions->has($id)) { + $resolved = $this->definitions->resolve($id, $new); + return $this->inflectors->inflect($resolved); + } + + if ($this->definitions->hasTag($id)) { + $arrayOf = $this->definitions->resolveTagged($id, $new); + + array_walk($arrayOf, function (&$resolved) { + $resolved = $this->inflectors->inflect($resolved); + }); + + return $arrayOf; + } + + if ($this->providers->provides($id)) { + $this->providers->register($id); + + if (!$this->definitions->has($id) && !$this->definitions->hasTag($id)) { + throw new ContainerException(sprintf('Service provider lied about providing (%s) service', $id)); + } + + return $this->get($id, $new); + } + + foreach ($this->delegates as $delegate) { + if ($delegate->has($id)) { + $resolved = $delegate->get($id); + return $this->inflectors->inflect($resolved); + } + } + + throw new NotFoundException(sprintf('Alias (%s) is not being managed by the container or delegates', $id)); + } + + /** + * {@inheritdoc} + */ + public function has($id) + { + if ($this->definitions->has($id)) { + return true; + } + + if ($this->definitions->hasTag($id)) { + return true; + } + + if ($this->providers->provides($id)) { + return true; + } + + foreach ($this->delegates as $delegate) { + if ($delegate->has($id)) { + return true; + } + } + + return false; + } + + /** + * Allows for manipulation of specific types on resolution. + * + * @param string $type + * @param callable|null $callback + * + * @return InflectorInterface + */ + public function inflector(string $type, callable $callback = null) : InflectorInterface + { + return $this->inflectors->add($type, $callback); + } + + /** + * Delegate a backup container to be checked for services if it + * cannot be resolved via this container. + * + * @param ContainerInterface $container + * + * @return self + */ + public function delegate(ContainerInterface $container) : self + { + $this->delegates[] = $container; + + if ($container instanceof ContainerAwareInterface) { + $container->setLeagueContainer($this); + } + + return $this; + } +} diff --git a/classes/Dependencies/League/Container/ContainerAwareInterface.php b/classes/Dependencies/League/Container/ContainerAwareInterface.php new file mode 100644 index 000000000..f79443177 --- /dev/null +++ b/classes/Dependencies/League/Container/ContainerAwareInterface.php @@ -0,0 +1,40 @@ +container = $container; + + return $this; + } + + /** + * Get the container. + * + * @return ContainerInterface + */ + public function getContainer() : ContainerInterface + { + if ($this->container instanceof ContainerInterface) { + return $this->container; + } + + throw new ContainerException('No container implementation has been set.'); + } + + /** + * Set a container. + * + * @param Container $container + * + * @return self + */ + public function setLeagueContainer(Container $container) : ContainerAwareInterface + { + $this->container = $container; + $this->leagueContainer = $container; + + return $this; + } + + /** + * Get the container. + * + * @return Container + */ + public function getLeagueContainer() : Container + { + if ($this->leagueContainer instanceof Container) { + return $this->leagueContainer; + } + + throw new ContainerException('No container implementation has been set.'); + } +} diff --git a/classes/Dependencies/League/Container/Definition/Definition.php b/classes/Dependencies/League/Container/Definition/Definition.php new file mode 100644 index 000000000..8128d7781 --- /dev/null +++ b/classes/Dependencies/League/Container/Definition/Definition.php @@ -0,0 +1,278 @@ +alias = $id; + $this->concrete = $concrete; + } + + /** + * {@inheritdoc} + */ + public function addTag(string $tag) : DefinitionInterface + { + $this->tags[$tag] = true; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function hasTag(string $tag) : bool + { + return isset($this->tags[$tag]); + } + + /** + * {@inheritdoc} + */ + public function setAlias(string $id) : DefinitionInterface + { + $this->alias = $id; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getAlias() : string + { + return $this->alias; + } + + /** + * {@inheritdoc} + */ + public function setShared(bool $shared = true) : DefinitionInterface + { + $this->shared = $shared; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isShared() : bool + { + return $this->shared; + } + + /** + * {@inheritdoc} + */ + public function getConcrete() + { + return $this->concrete; + } + + /** + * {@inheritdoc} + */ + public function setConcrete($concrete) : DefinitionInterface + { + $this->concrete = $concrete; + $this->resolved = null; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addArgument($arg) : DefinitionInterface + { + $this->arguments[] = $arg; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addArguments(array $args) : DefinitionInterface + { + foreach ($args as $arg) { + $this->addArgument($arg); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addMethodCall(string $method, array $args = []) : DefinitionInterface + { + $this->methods[] = [ + 'method' => $method, + 'arguments' => $args + ]; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addMethodCalls(array $methods = []) : DefinitionInterface + { + foreach ($methods as $method => $args) { + $this->addMethodCall($method, $args); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function resolve(bool $new = false) + { + $concrete = $this->concrete; + + if ($this->isShared() && $this->resolved !== null && $new === false) { + return $this->resolved; + } + + if (is_callable($concrete)) { + $concrete = $this->resolveCallable($concrete); + } + + if ($concrete instanceof RawArgumentInterface) { + $this->resolved = $concrete->getValue(); + + return $concrete->getValue(); + } + + if ($concrete instanceof ClassNameInterface) { + $concrete = $concrete->getClassName(); + } + + if (is_string($concrete) && class_exists($concrete)) { + $concrete = $this->resolveClass($concrete); + } + + if (is_object($concrete)) { + $concrete = $this->invokeMethods($concrete); + } + + if (is_string($concrete) && $this->getContainer()->has($concrete)) { + $concrete = $this->getContainer()->get($concrete); + } + + $this->resolved = $concrete; + + return $concrete; + } + + /** + * Resolve a callable. + * + * @param callable $concrete + * + * @return mixed + */ + protected function resolveCallable(callable $concrete) + { + $resolved = $this->resolveArguments($this->arguments); + + return call_user_func_array($concrete, $resolved); + } + + /** + * Resolve a class. + * + * @param string $concrete + * + * @return object + * + * @throws ReflectionException + */ + protected function resolveClass(string $concrete) + { + $resolved = $this->resolveArguments($this->arguments); + $reflection = new ReflectionClass($concrete); + + return $reflection->newInstanceArgs($resolved); + } + + /** + * Invoke methods on resolved instance. + * + * @param object $instance + * + * @return object + */ + protected function invokeMethods($instance) + { + foreach ($this->methods as $method) { + $args = $this->resolveArguments($method['arguments']); + + /** @var callable $callable */ + $callable = [$instance, $method['method']]; + call_user_func_array($callable, $args); + } + + return $instance; + } +} diff --git a/classes/Dependencies/League/Container/Definition/DefinitionAggregate.php b/classes/Dependencies/League/Container/Definition/DefinitionAggregate.php new file mode 100644 index 000000000..651c8c180 --- /dev/null +++ b/classes/Dependencies/League/Container/Definition/DefinitionAggregate.php @@ -0,0 +1,124 @@ +definitions = array_filter($definitions, function ($definition) { + return ($definition instanceof DefinitionInterface); + }); + } + + /** + * {@inheritdoc} + */ + public function add(string $id, $definition, bool $shared = false) : DefinitionInterface + { + if (!$definition instanceof DefinitionInterface) { + $definition = new Definition($id, $definition); + } + + $this->definitions[] = $definition + ->setAlias($id) + ->setShared($shared) + ; + + return $definition; + } + + /** + * {@inheritdoc} + */ + public function has(string $id) : bool + { + foreach ($this->getIterator() as $definition) { + if ($id === $definition->getAlias()) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function hasTag(string $tag) : bool + { + foreach ($this->getIterator() as $definition) { + if ($definition->hasTag($tag)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getDefinition(string $id) : DefinitionInterface + { + foreach ($this->getIterator() as $definition) { + if ($id === $definition->getAlias()) { + return $definition->setLeagueContainer($this->getLeagueContainer()); + } + } + + throw new NotFoundException(sprintf('Alias (%s) is not being handled as a definition.', $id)); + } + + /** + * {@inheritdoc} + */ + public function resolve(string $id, bool $new = false) + { + return $this->getDefinition($id)->resolve($new); + } + + /** + * {@inheritdoc} + */ + public function resolveTagged(string $tag, bool $new = false) : array + { + $arrayOf = []; + + foreach ($this->getIterator() as $definition) { + if ($definition->hasTag($tag)) { + $arrayOf[] = $definition->setLeagueContainer($this->getLeagueContainer())->resolve($new); + } + } + + return $arrayOf; + } + + /** + * {@inheritdoc} + */ + public function getIterator() : Generator + { + $count = count($this->definitions); + + for ($i = 0; $i < $count; $i++) { + yield $this->definitions[$i]; + } + } +} diff --git a/classes/Dependencies/League/Container/Definition/DefinitionAggregateInterface.php b/classes/Dependencies/League/Container/Definition/DefinitionAggregateInterface.php new file mode 100644 index 000000000..e0c83a923 --- /dev/null +++ b/classes/Dependencies/League/Container/Definition/DefinitionAggregateInterface.php @@ -0,0 +1,67 @@ +type = $type; + $this->callback = $callback; + } + + /** + * {@inheritdoc} + */ + public function getType() : string + { + return $this->type; + } + + /** + * {@inheritdoc} + */ + public function invokeMethod(string $name, array $args) : InflectorInterface + { + $this->methods[$name] = $args; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function invokeMethods(array $methods) : InflectorInterface + { + foreach ($methods as $name => $args) { + $this->invokeMethod($name, $args); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setProperty(string $property, $value) : InflectorInterface + { + $this->properties[$property] = $this->resolveArguments([$value])[0]; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setProperties(array $properties) : InflectorInterface + { + foreach ($properties as $property => $value) { + $this->setProperty($property, $value); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function inflect($object) + { + $properties = $this->resolveArguments(array_values($this->properties)); + $properties = array_combine(array_keys($this->properties), $properties); + + // array_combine() can technically return false + foreach ($properties ?: [] as $property => $value) { + $object->{$property} = $value; + } + + foreach ($this->methods as $method => $args) { + $args = $this->resolveArguments($args); + + /** @var callable $callable */ + $callable = [$object, $method]; + call_user_func_array($callable, $args); + } + + if ($this->callback !== null) { + call_user_func($this->callback, $object); + } + } +} diff --git a/classes/Dependencies/League/Container/Inflector/InflectorAggregate.php b/classes/Dependencies/League/Container/Inflector/InflectorAggregate.php new file mode 100644 index 000000000..f82c6696c --- /dev/null +++ b/classes/Dependencies/League/Container/Inflector/InflectorAggregate.php @@ -0,0 +1,58 @@ +inflectors[] = $inflector; + + return $inflector; + } + + /** + * {@inheritdoc} + */ + public function getIterator() : Generator + { + $count = count($this->inflectors); + + for ($i = 0; $i < $count; $i++) { + yield $this->inflectors[$i]; + } + } + + /** + * {@inheritdoc} + */ + public function inflect($object) + { + foreach ($this->getIterator() as $inflector) { + $type = $inflector->getType(); + + if (! $object instanceof $type) { + continue; + } + + $inflector->setLeagueContainer($this->getLeagueContainer()); + $inflector->inflect($object); + } + + return $object; + } +} diff --git a/classes/Dependencies/League/Container/Inflector/InflectorAggregateInterface.php b/classes/Dependencies/League/Container/Inflector/InflectorAggregateInterface.php new file mode 100644 index 000000000..0c2a57d15 --- /dev/null +++ b/classes/Dependencies/League/Container/Inflector/InflectorAggregateInterface.php @@ -0,0 +1,27 @@ +cacheResolutions === true && array_key_exists($id, $this->cache)) { + return $this->cache[$id]; + } + + if (! $this->has($id)) { + throw new NotFoundException( + sprintf('Alias (%s) is not an existing class and therefore cannot be resolved', $id) + ); + } + + $reflector = new ReflectionClass($id); + $construct = $reflector->getConstructor(); + + if ($construct && !$construct->isPublic()) { + throw new NotFoundException( + sprintf('Alias (%s) has a non-public constructor and therefore cannot be instantiated', $id) + ); + } + + $resolution = $construct === null + ? new $id + : $resolution = $reflector->newInstanceArgs($this->reflectArguments($construct, $args)) + ; + + if ($this->cacheResolutions === true) { + $this->cache[$id] = $resolution; + } + + return $resolution; + } + + /** + * {@inheritdoc} + */ + public function has($id) + { + return class_exists($id); + } + + /** + * Invoke a callable via the container. + * + * @param callable $callable + * @param array $args + * + * @return mixed + * + * @throws ReflectionException + */ + public function call(callable $callable, array $args = []) + { + if (is_string($callable) && strpos($callable, '::') !== false) { + $callable = explode('::', $callable); + } + + if (is_array($callable)) { + if (is_string($callable[0])) { + $callable[0] = $this->getContainer()->get($callable[0]); + } + + $reflection = new ReflectionMethod($callable[0], $callable[1]); + + if ($reflection->isStatic()) { + $callable[0] = null; + } + + return $reflection->invokeArgs($callable[0], $this->reflectArguments($reflection, $args)); + } + + if (is_object($callable)) { + $reflection = new ReflectionMethod($callable, '__invoke'); + + return $reflection->invokeArgs($callable, $this->reflectArguments($reflection, $args)); + } + + $reflection = new ReflectionFunction(\Closure::fromCallable($callable)); + + return $reflection->invokeArgs($this->reflectArguments($reflection, $args)); + } + + /** + * Whether the container should default to caching resolutions and returning + * the cache on following calls. + * + * @param boolean $option + * + * @return self + */ + public function cacheResolutions(bool $option = true) : ContainerInterface + { + $this->cacheResolutions = $option; + + return $this; + } +} diff --git a/classes/Dependencies/League/Container/ServiceProvider/AbstractServiceProvider.php b/classes/Dependencies/League/Container/ServiceProvider/AbstractServiceProvider.php new file mode 100644 index 000000000..64efcb27e --- /dev/null +++ b/classes/Dependencies/League/Container/ServiceProvider/AbstractServiceProvider.php @@ -0,0 +1,46 @@ +provides, true); + } + + /** + * {@inheritdoc} + */ + public function setIdentifier(string $id) : ServiceProviderInterface + { + $this->identifier = $id; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getIdentifier() : string + { + return $this->identifier ?? get_class($this); + } +} diff --git a/classes/Dependencies/League/Container/ServiceProvider/BootableServiceProviderInterface.php b/classes/Dependencies/League/Container/ServiceProvider/BootableServiceProviderInterface.php new file mode 100644 index 000000000..29d147fcd --- /dev/null +++ b/classes/Dependencies/League/Container/ServiceProvider/BootableServiceProviderInterface.php @@ -0,0 +1,14 @@ +getContainer()->has($provider)) { + $provider = $this->getContainer()->get($provider); + } elseif (is_string($provider) && class_exists($provider)) { + $provider = new $provider; + } + + if (in_array($provider, $this->providers, true)) { + return $this; + } + + if ($provider instanceof ContainerAwareInterface) { + $provider->setLeagueContainer($this->getLeagueContainer()); + } + + if ($provider instanceof BootableServiceProviderInterface) { + $provider->boot(); + } + + if ($provider instanceof ServiceProviderInterface) { + $this->providers[] = $provider; + + return $this; + } + + throw new ContainerException( + 'A service provider must be a fully qualified class name or instance ' . + 'of (\Imagify\Dependencies\League\Container\ServiceProvider\ServiceProviderInterface)' + ); + } + + /** + * {@inheritdoc} + */ + public function provides(string $service) : bool + { + foreach ($this->getIterator() as $provider) { + if ($provider->provides($service)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getIterator() : Generator + { + $count = count($this->providers); + + for ($i = 0; $i < $count; $i++) { + yield $this->providers[$i]; + } + } + + /** + * {@inheritdoc} + */ + public function register(string $service) + { + if (false === $this->provides($service)) { + throw new ContainerException( + sprintf('(%s) is not provided by a service provider', $service) + ); + } + + foreach ($this->getIterator() as $provider) { + if (in_array($provider->getIdentifier(), $this->registered, true)) { + continue; + } + + if ($provider->provides($service)) { + $this->registered[] = $provider->getIdentifier(); + $provider->register(); + } + } + } +} diff --git a/classes/Dependencies/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php b/classes/Dependencies/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php new file mode 100644 index 000000000..a9e264887 --- /dev/null +++ b/classes/Dependencies/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php @@ -0,0 +1,36 @@ +leagueContainer property or the `getLeagueContainer` method + * from the ContainerAwareTrait. + * + * @return void + */ + public function register(); + + /** + * Set a custom id for the service provider. This enables + * registering the same service provider multiple times. + * + * @param string $id + * + * @return self + */ + public function setIdentifier(string $id) : ServiceProviderInterface; + + /** + * The id of the service provider uniquely identifies it, so + * that we can quickly determine if it has already been registered. + * Defaults to get_class($provider). + * + * @return string + */ + public function getIdentifier() : string; +} diff --git a/classes/Dependencies/Psr/Container/ContainerExceptionInterface.php b/classes/Dependencies/Psr/Container/ContainerExceptionInterface.php new file mode 100644 index 000000000..c272cbd90 --- /dev/null +++ b/classes/Dependencies/Psr/Container/ContainerExceptionInterface.php @@ -0,0 +1,12 @@ +get_bulk_instance( $context )->get_optimized_media_ids_without_webp(); + $media = $bulk->get_bulk_instance( $context )->get_optimized_media_ids_without_format( $format ); $remaining += count( $media['ids'] ); } diff --git a/classes/Job/MediaOptimization.php b/classes/Job/MediaOptimization.php index 87ab51ede..f8c8b4d52 100644 --- a/classes/Job/MediaOptimization.php +++ b/classes/Job/MediaOptimization.php @@ -187,8 +187,8 @@ private function task_optimize( $item ) { $item['error'] = $data; } elseif ( 'already_optimized' === $data['status'] ) { - // Status is "already_optimized", try to create WebP versions only. - $item['sizes'] = array_filter( $item['sizes'], [ $this->optimization_process, 'is_size_webp' ] ); + // Status is "already_optimized", try to create next-gen versions only. + $item['sizes'] = array_filter( $item['sizes'], [ $this->optimization_process, 'is_size_next_gen' ] ); } elseif ( 'success' !== $data['status'] ) { // Don't go further if the full size has not the "success" status. diff --git a/classes/Optimization/File.php b/classes/Optimization/File.php index 57820f168..6173b2047 100644 --- a/classes/Optimization/File.php +++ b/classes/Optimization/File.php @@ -113,7 +113,7 @@ public function can_be_processed() { return new \WP_Error( 'not_exists', sprintf( - /* translators: %s is a file path. */ + /* translators: %s is a file path. */ __( 'The file %s does not seem to exist.', 'imagify' ), '' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '' ) @@ -124,7 +124,7 @@ public function can_be_processed() { return new \WP_Error( 'not_a_file', sprintf( - /* translators: %s is a file path. */ + /* translators: %s is a file path. */ __( 'This does not seem to be a file: %s.', 'imagify' ), '' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '' ) @@ -135,7 +135,7 @@ public function can_be_processed() { return new \WP_Error( 'not_writable', sprintf( - /* translators: %s is a file path. */ + /* translators: %s is a file path. */ __( 'The file %s does not seem to be writable.', 'imagify' ), '' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '' ) @@ -148,7 +148,7 @@ public function can_be_processed() { return new \WP_Error( 'folder_not_writable', sprintf( - /* translators: %s is a file path. */ + /* translators: %s is a file path. */ __( 'The folder %s does not seem to be writable.', 'imagify' ), '' . esc_html( $this->filesystem->make_path_relative( $parent_folder ) ) . '' ) @@ -197,7 +197,7 @@ public function resize( $dimensions = [], $max_width = 0 ) { return new \WP_Error( 'not_an_image', sprintf( - /* translators: %s is a file path. */ + /* translators: %s is a file path. */ __( 'The file %s does not seem to be an image, and cannot be resized.', 'imagify' ), '' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '' ) @@ -321,7 +321,7 @@ public function create_thumbnail( $destination ) { return new \WP_Error( 'not_an_image', sprintf( - /* translators: %s is a file path. */ + /* translators: %s is a file path. */ __( 'The file %s does not seem to be an image, and cannot be resized.', 'imagify' ), '' . esc_html( $this->filesystem->make_path_relative( $this->path ) ) . '' ) @@ -423,6 +423,22 @@ public function backup( $backup_path = null, $backup_source = null ) { ) ); } + // Check if a '-scaled' version of the image exists. + $scaled_path = preg_replace( '/(\.)([^\.]+)$/', '-scaled.$2', $backup_source ); + if ( $this->filesystem->exists( $scaled_path ) ) { + // Create a backup path for the scaled image. + $scaled_backup_path = preg_replace( '/(\.)([^\.]+)$/', '-scaled.$2', $backup_path ); + // Copy the '-scaled' version to the backup. + $this->filesystem->copy( $scaled_path, $scaled_backup_path, $overwrite, FS_CHMOD_FILE ); + + if ( ! $this->filesystem->exists( $scaled_backup_path ) ) { + return new \WP_Error( 'backup_doesnt_exist', __( 'The file could not be saved.', 'imagify' ), array( + 'file_path' => $this->filesystem->make_path_relative( $scaled_path ), + 'backup_path' => $this->filesystem->make_path_relative( $scaled_backup_path ), + ) ); + } + } + return true; } @@ -438,7 +454,7 @@ public function backup( $backup_path = null, $backup_source = null ) { * @type bool $backup False to prevent backup. True to follow the user's setting. A backup can't be forced. * @type string $backup_path If a backup must be done, this is the path to use. Default is the backup path used for the WP Media Library. * @type int $optimization_level The optimization level (2=ultra, 1=aggressive, 0=normal). - * @type string $convert Set to 'webp' to convert the image to WebP. + * @type string $convert Set to 'webp' to convert the image to WebP, 'avif' to convert image to AVIF. * @type string $context The context. * @type int $original_size The file size, sent to the API. * } @@ -474,7 +490,7 @@ public function optimize( $args = [] ) { * * @param string $path Absolute path to the media file. * @param array $args Arguments passed to the method. - */ + */ do_action( 'imagify_before_optimize_file', $this->path, $args ); /** @@ -485,7 +501,7 @@ public function optimize( $args = [] ) { * * @param string $path Absolute path to the image file. * @param bool $backup True if a backup will be make. - */ + */ do_action_deprecated( 'before_do_imagify', [ $this->path, $args['backup'] ], '1.9', 'imagify_before_optimize_file' ); if ( $args['backup'] ) { @@ -509,6 +525,7 @@ public function optimize( $args = [] ) { if ( $args['convert'] ) { $data['convert'] = $args['convert']; + $format = $args['convert']; } $response = upload_imagify_image( [ @@ -534,8 +551,12 @@ public function optimize( $args = [] ) { $args['convert'] = ''; } - if ( 'webp' === $args['convert'] ) { - $destination_path = $this->get_path_to_webp(); + $formats = [ + 'webp', + 'avif', + ]; + if ( in_array( $args['convert'], $formats, true ) ) { + $destination_path = $this->get_path_to_nextgen( $args['convert'] ); $this->path = $destination_path; $this->file_type = null; $this->editor = null; @@ -557,7 +578,7 @@ public function optimize( $args = [] ) { * * @param string $path Absolute path to the image file. * @param bool $backup True if a backup has been made. - */ + */ do_action_deprecated( 'after_do_imagify', [ $this->path, $args['backup'] ], '1.9', 'imagify_before_optimize_file' ); /** @@ -568,7 +589,7 @@ public function optimize( $args = [] ) { * * @param string $path Absolute path to the media file. * @param array $args Arguments passed to the method. - */ + */ do_action( 'imagify_after_optimize_file', $this->path, $args ); return $response; @@ -603,7 +624,7 @@ protected function get_editor() { $this->editor = new \WP_Error( 'image_editor', sprintf( - /* translators: %1$s is an error message, %2$s is a "More info?" link. */ + /* translators: %1$s is an error message, %2$s is a "More info?" link. */ __( 'No php extensions are available to edit images on the server. ImageMagick or GD is required. The internal error is: %1$s. %2$s', 'imagify' ), $this->editor->get_error_message(), '' . __( 'More info?', 'imagify' ) . '' @@ -765,6 +786,26 @@ public function get_path_to_webp() { return imagify_path_to_webp( $this->path ); } + /** + * Replace the file extension by its next-gen format extension. + * + * @since 2.2 + * + * @param string $format the format we are targeting. + * @return string|bool The file path on success. False if not an image or on failure. + */ + public function get_path_to_nextgen( string $format ) { + if ( ! $this->is_image() ) { + return false; + } + + if ( $this->is_webp() || $this->is_avif() ) { + return false; + } + + return imagify_path_to_nextgen( $this->path, $format ); + } + /** * Tell if the file is a WebP image. * Rejects "path/to/.webp" files. @@ -778,6 +819,18 @@ public function is_webp() { return preg_match( '@(?!^|/|\\\)\.webp$@i', $this->path ); } + /** + * Tell if the file is an AVIF image. + * Rejects "path/to/.avif" files. + * + * @since 2.2 + * + * @return bool + */ + public function is_avif() { + return preg_match( '@(?!^|/|\\\)\.avif$@i', $this->path ); + } + /** * Get the file mime type + file extension. * diff --git a/classes/Optimization/Process/AbstractProcess.php b/classes/Optimization/Process/AbstractProcess.php index bec0948df..6ec243805 100644 --- a/classes/Optimization/Process/AbstractProcess.php +++ b/classes/Optimization/Process/AbstractProcess.php @@ -1,4 +1,6 @@ filesystem = \Imagify_Filesystem::get_instance(); + $this->format = $this->get_current_format(); } /** @@ -114,7 +143,8 @@ public function __construct( $id ) { * * @since 1.9 * - * @param mixed $id Whatever. + * @param mixed $id Whatever. + * * @return bool */ public static function constructor_accepts( $id ) { @@ -212,7 +242,7 @@ public function is_valid() { * * @since 1.9 * - * @param string $describer Capacity describer. See \Imagify\Context\ContextInterface->get_capacity() for possible values. Can also be a "real" user capacity. + * @param string $describer Capacity describer. See \Imagify\Context\ContextInterface->get_capacity() for possible values. Can also be a "real" user capacity. * @return bool */ public function current_user_can( $describer ) { @@ -225,19 +255,15 @@ public function current_user_can( $describer ) { return $media->get_context_instance()->current_user_can( $describer, $media->get_id() ); } - - /** ----------------------------------------------------------------------------------------- */ - /** OPTIMIZATION ============================================================================ */ - /** ----------------------------------------------------------------------------------------- */ - /** * Optimize a media files. * * @since 1.9 * - * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). - * @param array $args An array of optionnal arguments. - * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. + * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). + * @param array $args An array of optionnal arguments. + * + * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function optimize( $optimization_level = null, $args = [] ) { if ( ! $this->is_valid() ) { @@ -256,13 +282,13 @@ public function optimize( $optimization_level = null, $args = [] ) { return new WP_Error( 'optimized', __( 'This media has already been optimized by Imagify.', 'imagify' ) ); } - if ( $data->is_already_optimized() && $this->has_webp() ) { - // If already optimized but has WebP, delete WebP versions and optimization data. + if ( $data->is_already_optimized() && $this->has_next_gen() ) { + // If already optimized but has next-gen, delete next-gen versions and optimization data. $data->delete_optimization_data(); - $deleted = $this->delete_webp_files(); + $deleted = $this->delete_nextgen_files(); if ( is_wp_error( $deleted ) ) { - return new WP_Error( 'webp_not_deleted', __( 'Previous WebP files could not be deleted.', 'imagify' ) ); + return new WP_Error( 'next_gen_not_deleted', __( 'Previous Next-Gen files could not be deleted.', 'imagify' ) ); } } @@ -280,9 +306,10 @@ public function optimize( $optimization_level = null, $args = [] ) { * * @since 1.9 * - * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). - * @param array $args An array of optionnal arguments. - * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. + * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). + * @param array $args An array of optionnal arguments. + * + * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function reoptimize( $optimization_level = null, $args = [] ) { if ( ! $this->is_valid() ) { @@ -322,17 +349,21 @@ public function reoptimize( $optimization_level = null, $args = [] ) { * Optimize several file sizes by pushing tasks into the queue. * * @since 1.9 - * @see MediaOptimization->task_before() - * @see MediaOptimization->task_after() + * @see MediaOptimization->task_before() + * @see MediaOptimization->task_after() * - * @param array $sizes An array of media sizes (strings). Use "full" for the size of the main file. - * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). - * @param array $args { - * An array of optionnal arguments. + * @since 2.2 + * Addition of the image format + * + * @param array $sizes An array of media sizes (strings). Use "full" for the size of the main file. + * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). + * @param array $args { + * An array of optionnal arguments. * * @type string $hook_suffix Suffix used to trigger hooks before and after optimization. * } - * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. + * + * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function optimize_sizes( $sizes, $optimization_level = null, $args = [] ) { if ( ! $this->is_valid() ) { @@ -358,41 +389,51 @@ public function optimize_sizes( $sizes, $optimization_level = null, $args = [] ) } if ( $media->is_image() ) { - if ( $this->get_option( 'convert_to_webp' ) ) { - // Add WebP convertion. + // Add Next-Gen conversion. + $formats = imagify_nextgen_images_formats(); + + foreach ( $formats as $format ) { + if ( 'avif' === $format ) { + $format_suffix = static::AVIF_SUFFIX; + } elseif ( 'webp' === $format ) { + $format_suffix = static::WEBP_SUFFIX; + } + $files = $media->get_media_files(); foreach ( $sizes as $size_name ) { if ( empty( $files[ $size_name ] ) ) { continue; } - if ( 'image/webp' === $files[ $size_name ]['mime-type'] ) { + + if ( $this->get_mime_type( $format ) === $files[ $size_name ]['mime-type'] ) { continue; } - if ( in_array( $size_name . static::WEBP_SUFFIX, $sizes, true ) ) { + + if ( in_array( $size_name . $format_suffix, $sizes, true ) ) { continue; } - array_unshift( $sizes, $size_name . static::WEBP_SUFFIX ); + array_unshift( $sizes, $size_name . $format_suffix ); } } if ( ! $media->get_context_instance()->can_backup() && ! $media->get_backup_path() && ! $this->get_data()->get_size_data( 'full', 'success' ) ) { /** * Backup is NOT activated, and a backup file does NOT exist yet, and the full size is NOT optimized yet. - * WebP conversion needs a backup file, even a temporary one: we’ll create one. + * Next-Gen conversion needs a backup file, even a temporary one: we’ll create one. */ - $webp = false; + $next_gen = false; foreach ( $sizes as $size_name ) { - if ( $this->is_size_webp( $size_name ) ) { - $webp = true; + if ( $this->is_size_next_gen( $size_name ) ) { + $next_gen = true; break; } } - if ( $webp ) { - // We have at least one WebP conversion to do: create a temporary backup. + if ( $next_gen ) { + // We have at least one next-gen conversion to do: create a temporary backup. $backuped = $this->get_original_file()->backup( $media->get_raw_backup_path() ); if ( $backuped ) { @@ -443,9 +484,10 @@ public function optimize_sizes( $sizes, $optimization_level = null, $args = [] ) * * @since 1.9 * - * @param string $size The media size. - * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). - * @return array|\WP_Error Optimized image data. A \WP_Error object on error. + * @param string $size The media size. + * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). + * + * @return array|WP_Error Optimized image data. A WP_Error object on error. */ public function optimize_size( $size, $optimization_level = null ) { if ( ! $this->is_valid() ) { // Bail out. @@ -455,13 +497,13 @@ public function optimize_size( $size, $optimization_level = null ) { $media = $this->get_media(); $sizes = $media->get_media_files(); $thumb_size = $size; - $webp = $this->is_size_webp( $size ); + $next_gen = $this->is_size_next_gen( $size ); $path_is_temp = false; - if ( $webp ) { + if ( $next_gen ) { // We'll make sure the file is an image later. - $thumb_size = $webp; // Contains the name of the non-WebP size. - $webp = true; + $thumb_size = $next_gen; // Contains the name of the non-next-gen size. + $next_gen = true; } if ( empty( $sizes[ $thumb_size ]['path'] ) ) { // Bail out. @@ -469,7 +511,7 @@ public function optimize_size( $size, $optimization_level = null ) { return new WP_Error( 'unknown_size', sprintf( - /* translators: %s is a size name. */ + /* translators: %s is a size name. */ __( 'The size %s is unknown.', 'imagify' ), '' . esc_html( $thumb_size ) . '' ) @@ -478,12 +520,12 @@ public function optimize_size( $size, $optimization_level = null ) { if ( $this->get_data()->get_size_data( $size, 'success' ) ) { // Bail out. // This size is already optimized with Imagify, and must not be optimized again. - if ( $webp ) { + if ( $next_gen ) { return new WP_Error( 'size_is_successfully_optimized', sprintf( - /* translators: %s is a size name. */ - __( 'The WebP format for the size %s already exists.', 'imagify' ), + /* translators: %s is a size name. */ + __( 'The Next-Gen format for the size %s already exists.', 'imagify' ), '' . esc_html( $thumb_size ) . '' ) ); @@ -491,7 +533,7 @@ public function optimize_size( $size, $optimization_level = null ) { return new WP_Error( 'size_is_successfully_optimized', sprintf( - /* translators: %s is a size name. */ + /* translators: %s is a size name. */ __( 'The size %s is already optimized by Imagify.', 'imagify' ), '' . esc_html( $thumb_size ) . '' ) @@ -506,16 +548,16 @@ public function optimize_size( $size, $optimization_level = null ) { $optimization_level = $this->sanitize_optimization_level( $optimization_level ); - if ( $webp && $this->get_data()->get_size_data( $thumb_size, 'success' ) ) { - // We want a WebP version but the source file is already optimized by Imagify. + if ( $next_gen && $this->get_data()->get_size_data( $thumb_size, 'success' ) ) { + // We want a next-gen version but the source file is already optimized by Imagify. $result = $this->create_temporary_copy( $thumb_size, $sizes ); if ( ! $result ) { // Bail out. - // Could not create a copy of the non-WebP version. + // Could not create a copy of the non-next-gen version. $response = new WP_Error( - 'non_webp_copy_failed', + 'non_next_gen_copy_failed', sprintf( - /* translators: %s is a size name. */ + /* translators: %s is a size name. */ __( 'Could not create an unoptimized copy of the size %s.', 'imagify' ), '' . esc_html( $thumb_size ) . '' ) @@ -548,7 +590,7 @@ public function optimize_size( $size, $optimization_level = null ) { $response = new WP_Error( 'extension_not_supported', sprintf( - /* translators: %s is a file extension. */ + /* translators: %s is a file extension. */ __( '%s cannot be optimized.', 'imagify' ), '' . esc_html( strtolower( $extension ) ) . '' ) @@ -564,14 +606,14 @@ public function optimize_size( $size, $optimization_level = null ) { return $response; } - if ( $webp && ! $file->is_image() ) { // Bail out. + if ( $next_gen && ! $file->is_image() ) { // Bail out. if ( $path_is_temp ) { $this->filesystem->delete( $path ); } $response = new WP_Error( - 'no_webp', - __( 'This file is not an image and cannot be converted to WebP format.', 'imagify' ) + 'no_next_gen', + __( 'This file is not an image and cannot be converted to Next-Gen format.', 'imagify' ) ); $this->update_size_optimization_data( $response, $size, $optimization_level ); @@ -583,11 +625,11 @@ public function optimize_size( $size, $optimization_level = null ) { /** * Fires before optimizing a file. - * Return a \WP_Error object to prevent the optimization. + * Return a WP_Error object to prevent the optimization. * * @since 1.9 * - * @param null|WP_Error $response Null by default. Return a \WP_Error object to prevent optimization. + * @param null|WP_Error $response Null by default. Return a WP_Error object to prevent optimization. * @param ProcessInterface $process The optimization process instance. * @param File $file The file instance. If $webp is true, $file references the non-WebP file. * @param string $thumb_size The media size. @@ -595,7 +637,7 @@ public function optimize_size( $size, $optimization_level = null ) { * @param bool $webp The image will be converted to WebP. * @param bool $is_disabled Tell if this size is disabled from optimization. */ - $response = apply_filters( 'imagify_before_optimize_size', null, $this, $file, $thumb_size, $optimization_level, $webp, $is_disabled ); + $response = apply_filters( 'imagify_before_optimize_size', null, $this, $file, $thumb_size, $optimization_level, $next_gen, $is_disabled ); if ( ! is_wp_error( $response ) ) { if ( $is_disabled ) { @@ -603,7 +645,7 @@ public function optimize_size( $size, $optimization_level = null ) { $response = new WP_Error( 'unauthorized_size', sprintf( - /* translators: %s is a size name. */ + /* translators: %s is a size name. */ __( 'The size %s is not authorized to be optimized. Update your Imagify settings if you want to optimize it.', 'imagify' ), '' . esc_html( $thumb_size ) . '' ) @@ -612,21 +654,21 @@ public function optimize_size( $size, $optimization_level = null ) { $response = new WP_Error( 'file_not_exists', sprintf( - /* translators: %s is a file path. */ + /* translators: %s is a file path. */ __( 'The file %s does not seem to exist.', 'imagify' ), '' . esc_html( $this->filesystem->make_path_relative( $file->get_path() ) ) . '' ) ); - } elseif ( $webp && ! $this->can_create_webp_version( $file->get_path() ) ) { + } elseif ( $next_gen && ! $this->can_create_next_gen_version( $file->get_path() ) ) { $response = new WP_Error( 'is_animated_gif', - __( 'This file is an animated gif: since Imagify does not support animated WebP, WebP creation for animated gif is disabled.', 'imagify' ) + __( 'This file is an animated gif: since Imagify does not support animated WebP/AVIF, WebP/AVIF creation for animated gif is disabled.', 'imagify' ) ); } elseif ( ! $this->filesystem->is_writable( $file->get_path() ) ) { $response = new WP_Error( 'file_not_writable', sprintf( - /* translators: %s is a file path. */ + /* translators: %s is a file path. */ __( 'The file %s does not seem to be writable.', 'imagify' ), '' . esc_html( $this->filesystem->make_path_relative( $file->get_path() ) ) . '' ) @@ -635,6 +677,16 @@ public function optimize_size( $size, $optimization_level = null ) { // Maybe resize the file. $response = $this->maybe_resize( $thumb_size, $file ); + $convert = ''; + + if ( $next_gen ) { + if ( strpos( $size, static::AVIF_SUFFIX ) ) { + $convert = 'avif'; + } elseif ( strpos( $size, static::WEBP_SUFFIX ) ) { + $convert = 'webp'; + } + } + if ( ! is_wp_error( $response ) ) { // Resizing succeeded: optimize the file. $response = $file->optimize( [ @@ -642,19 +694,20 @@ public function optimize_size( $size, $optimization_level = null ) { 'backup_path' => $media->get_raw_backup_path(), 'backup_source' => 'full' === $thumb_size ? $media->get_original_path() : null, 'optimization_level' => $optimization_level, - 'convert' => $webp ? 'webp' : '', + 'convert' => $convert, 'keep_exif' => true, 'context' => $media->get_context(), 'original_size' => $response['file_size'], ] ); - $response = $this->compare_webp_file_size( [ - 'response' => $response, - 'file' => $file, - 'is_webp' => $webp, - 'non_webp_thumb_size' => $thumb_size, - 'non_webp_file_path' => $sizes[ $thumb_size ]['path'], // Don't use $path nor $file->get_path(), it may return the path to a temporary file. - 'optimization_level' => $optimization_level, + $response = $this->compare_next_gen_file_size( [ + 'response' => $response, + 'file' => $file, + 'is_next_gen' => $next_gen, + 'next_gen_format' => $convert, + 'non_next_gen_thumb_size' => $thumb_size, + 'non_next_gen_file_path' => $sizes[ $thumb_size ]['path'], // Don't use $path nor $file->get_path(), it may return the path to a temporary file. + 'optimization_level' => $optimization_level, ] ); if ( property_exists( $response, 'message' ) ) { @@ -682,7 +735,7 @@ public function optimize_size( $size, $optimization_level = null ) { * @param bool $webp The image was supposed to be converted to WebP. * @param bool $is_disabled Tell if this size is disabled from optimization. */ - do_action( 'imagify_after_optimize_size', $this, $file, $thumb_size, $optimization_level, $webp, $is_disabled ); + do_action( 'imagify_after_optimize_size', $this, $file, $thumb_size, $optimization_level, $next_gen, $is_disabled ); if ( ! $path_is_temp ) { return $data; @@ -704,108 +757,109 @@ public function optimize_size( $size, $optimization_level = null ) { } /** - * Compare the file size of a file and its WebP version: if the WebP version is heavier than the non-WebP file, delete it. + * Compare the file size of a file and its Next-Gen version: if the Next-Gen version is heavier than the non-next-gen file, delete it. * - * @since 1.9.4 + * @since 2.2 * - * @param array $args { + * @param array $args { * A list of mandatory arguments. * - * @type \sdtClass|\WP_Error $response Optimized image data. A \WP_Error object on error. + * @type \sdtClass|WP_Error $response Optimized image data. A WP_Error object on error. * @type File $file The File instance of the file currently being optimized. - * @type bool $is_webp Tell if we're requesting a WebP file. - * @type string $non_webp_thumb_size Name of the corresponding non-WebP thumbnail size. If we're not creating a WebP file, this corresponds to the current thumbnail size. - * @type string $non_webp_file_path Path to the corresponding non-WebP file. If we're not creating a WebP file, this corresponds to the current file path. + * @type bool $is_next_gen Tell if we're requesting a next-gen file. + * @type string $non_next_gen_thumb_size Name of the corresponding non-next-gen thumbnail size. If we're not creating a Next-Gen file, this corresponds to the current thumbnail size. + * @type string $non_next_gen_file_path Path to the corresponding non-next-gen file. If we're not creating a Next-Gen file, this corresponds to the current file path. * @type string $optimization_level The optimization level. * } - * @return \sdtClass|WP_Error Optimized image data. A WP_Error object on error. + * + * @return \sdtClass|WP_Error Optimized image data. A WP_Error object on error. */ - protected function compare_webp_file_size( $args ) { - static $keep_large_webp; + protected function compare_next_gen_file_size( $args ) { + static $keep_large_next_gen; - if ( ! isset( $keep_large_webp ) ) { + if ( ! isset( $keep_large_next_gen ) ) { /** - * Allow to not store WebP images that are larger than their non-WebP version. + * Allow to not store next-gen images that are larger than their non-next-gen version. * * @since 1.9.4 * - * @param bool $keep_large_webp Set to false if you prefer your visitors over your Pagespeed score. Default value is true. + * @param bool $keep_large_next-gen Set to false if you prefer your visitors over your Pagespeed score. Default value is true. */ - $keep_large_webp = apply_filters( 'imagify_keep_large_webp', true ); + $keep_large_next_gen = apply_filters( 'imagify_keep_large_next_gen', true ); } - if ( $keep_large_webp || is_wp_error( $args['response'] ) || ! $args['file']->is_image() ) { + if ( $keep_large_next_gen || is_wp_error( $args['response'] ) || ! $args['file']->is_image() ) { return $args['response']; } // Optimization succeeded. - if ( ! property_exists( $args['response'], 'message' ) && $args['is_webp'] ) { + if ( ! property_exists( $args['response'], 'message' ) && $args['is_next_gen'] ) { /** - * We just created a WebP version: - * Check if it is lighter than the (maybe optimized) non-WebP file. + * We just created a next-gen version: + * Check if it is lighter than the (maybe optimized) non-next-gen file. */ - $data = $this->get_data()->get_size_data( $args['non_webp_thumb_size'] ); + $data = $this->get_data()->get_size_data( $args['non_next_gen_thumb_size'] ); if ( ! $data ) { - // We haven’t tried to optimize the non-WebP size yet. + // We haven’t tried to optimize the non-next-gen size yet. return $args['response']; } if ( ! empty( $data['optimized_size'] ) ) { - // The non-WebP size is optimized, we know the file size. - $non_webp_file_size = $data['optimized_size']; + // The non-next-gen size is optimized, we know the file size. + $non_next_gen_file_size = $data['optimized_size']; } else { - // The non-WebP size is "already optimized" or "error": grab the file size directly from the file. - $non_webp_file_size = $this->filesystem->size( $args['non_webp_file_path'] ); + // The non-next-gen size is "already optimized" or "error": grab the file size directly from the file. + $non_next_gen_file_size = $this->filesystem->size( $args['non_next_gen_file_path'] ); } - if ( ! $non_webp_file_size || $non_webp_file_size > $args['response']->new_size ) { - // The new WebP file is lighter. + if ( ! $non_next_gen_file_size || $non_next_gen_file_size > $args['response']->new_size ) { + // The new next-gen file is lighter. return $args['response']; } - // The new WebP file is heavier than the non-WebP file: delete it and return an error. + // The new next-gen file is heavier than the non-next-gen file: delete it and return an error. $this->filesystem->delete( $args['file']->get_path() ); return new WP_Error( - 'webp_heavy', + 'next_gen_heavy', sprintf( - /* translators: %s is a size name. */ - __( 'The WebP version of the size %s is heavier than its non-WebP version.', 'imagify' ), - '' . esc_html( $args['non_webp_thumb_size'] ) . '' + /* translators: %s is a size name. */ + __( 'The Next-Gen version of the size %s is heavier than its non-next-gen version.', 'imagify' ), + '' . esc_html( $args['non_next_gen_thumb_size'] ) . '' ) ); } /** - * We just created a non-WebP version: - * Check if its WebP version file is lighter than this one. + * We just created a non-next-gen version: + * Check if its next-gen version file is lighter than this one. */ - $webp_size = $args['non_webp_thumb_size'] . static::WEBP_SUFFIX; - $webp_file_size = $this->get_data()->get_size_data( $webp_size, 'optimized_size' ); + $next_gen_size = $args['non_next_gen_thumb_size'] . $args['next_gen_format']; + $next_gen_file_size = $this->get_data()->get_size_data( $next_gen_size, 'optimized_size' ); - if ( property_exists( $args['response'], 'message' ) || ! $webp_file_size || $webp_file_size < $args['response']->new_size ) { - // The WebP file is lighter than this one. + if ( property_exists( $args['response'], 'message' ) || ! $next_gen_file_size || $next_gen_file_size < $args['response']->new_size ) { + // The next-gen file is lighter than this one. return $args['response']; } - // The new optimized file is lighter than the WebP file: delete the WebP file and store an error. - $webp_path = $args['file']->get_path_to_webp(); + // The new optimized file is lighter than the next-gen file: delete the next-gen file and store an error. + $next_gen_path = $args['file']->get_path_to_nextgen( $args['next_gen_format'] ); - if ( $webp_path && $this->filesystem->is_writable( $webp_path ) ) { - $this->filesystem->delete( $webp_path ); + if ( $next_gen_path && $this->filesystem->is_writable( $next_gen_path ) ) { + $this->filesystem->delete( $next_gen_path ); } - $webp_response = new WP_Error( - 'webp_heavy', + $next_gen_response = new WP_Error( + 'next_gen_heavy', sprintf( - /* translators: %s is a size name. */ - __( 'The WebP version of the size %s is heavier than its non-WebP version.', 'imagify' ), - '' . esc_html( $args['non_webp_thumb_size'] ) . '' + /* translators: %s is a size name. */ + __( 'The Next-Gen version of the size %s is heavier than its non-next-gen version.', 'imagify' ), + '' . esc_html( $args['non_next_gen_thumb_size'] ) . '' ) ); - $this->update_size_optimization_data( $webp_response, $webp_size, $args['optimization_level'] ); + $this->update_size_optimization_data( $next_gen_response, $next_gen_size, $args['optimization_level'] ); return $args['response']; } @@ -815,7 +869,7 @@ protected function compare_webp_file_size( $args ) { * * @since 1.9 * - * @return bool|WP_Error True on success. A \WP_Error instance on failure. + * @return bool|WP_Error True on success. A WP_Error instance on failure. */ public function restore() { if ( ! $this->is_valid() ) { @@ -866,7 +920,7 @@ public function restore() { /** * Fires before restoring a media. - * Return a \WP_Error object to prevent the restoration. + * Return a WP_Error object to prevent the restoration. * * @since 1.9 * @@ -894,7 +948,7 @@ public function restore() { $media->update_dimensions(); // Delete the WebP version. - $this->delete_webp_file( $original_path ); + $this->delete_nextgen_file( $original_path ); // Restore the thumbnails. $response = $this->restore_thumbnails(); @@ -924,28 +978,23 @@ public function restore() { * * @since 1.9 * - * @return bool|WP_Error True on success. A \WP_Error instance on failure. + * @return bool|WP_Error True on success. A WP_Error instance on failure. */ protected function restore_thumbnails() { $media = $this->get_media(); /** - * Delete the WebP versions. + * Delete the next-gen versions. * If the full size file and the original file are not the same, the full size is considered like a thumbnail. - * In that case we must also delete the WebP file associated to the full size. + * In that case we must also delete the next-gen file associated to the full size. */ - $keep_full_webp = $media->get_raw_original_path() === $media->get_raw_fullsize_path(); - $this->delete_webp_files( $keep_full_webp ); + $keep_full_next_gen = $media->get_raw_original_path() === $media->get_raw_fullsize_path(); + $this->delete_nextgen_files( $keep_full_next_gen ); // Generate new thumbnails. return $media->generate_thumbnails(); } - - /** ----------------------------------------------------------------------------------------- */ - /** BACKUP FILE ============================================================================= */ - /** ----------------------------------------------------------------------------------------- */ - /** * Delete the backup file. * @@ -960,28 +1009,29 @@ public function delete_backup() { if ( $backup_path ) { $this->filesystem->delete( $backup_path ); + + // Check for the -scaled version in the backup. + $scaled_backup_path = preg_replace( '/(\.)([^\.]+)$/', '-scaled.$2', $backup_path ); + if ( $this->filesystem->exists( $scaled_backup_path ) ) { + // Delete the -scaled version from the backup. + $this->filesystem->delete( $scaled_backup_path ); + } } } - - /** ----------------------------------------------------------------------------------------- */ - /** TEMPORARY COPY OF A SIZE FILE =========================================================== */ - /** ----------------------------------------------------------------------------------------- */ - - /** - * If we need to create a WebP version, we must create it from an unoptimized image. - * The full size is always optimized before the WebP version creation, and in some cases it’s the same for the thumbnails. - * Then we use the backup file to create temporary files. - */ - /** * Create a temporary copy of a size file. * + * If we need to create a next-gen version, we must create it from an unoptimized image. + * The full size is always optimized before the next-gen version creation, and in some cases it’s the same for the thumbnails. + * Then we use the backup file to create temporary files. + * * @since 1.9 * - * @param string $size The image size name. - * @param array $sizes A list of thumbnail sizes being optimized. - * @return bool True if the file exists/is created. False on failure. + * @param string $size The image size name. + * @param array $sizes A list of thumbnail sizes being optimized. + * + * @return bool True if the file exists/is created. False on failure. */ protected function create_temporary_copy( $size, $sizes = null ) { $media = $this->get_media(); @@ -1036,7 +1086,7 @@ protected function create_temporary_copy( $size, $sizes = null ) { if ( 'full' === $size ) { /** - * We create a copy of the backup to be able to create a WebP version from it. + * We create a copy of the backup to be able to create a next-gen version from it. * That means the optimization process will resize the file if needed, so there is nothing more to do here. */ return true; @@ -1126,8 +1176,9 @@ protected function create_temporary_copy( $size, $sizes = null ) { * * @since 1.9 * - * @param string $size The image size name. - * @param array $sizes A list of thumbnail sizes being optimized. + * @param string $size The image size name. + * @param array $sizes A list of thumbnail sizes being optimized. + * * @return string|bool An image path. False on failure. */ protected function get_temporary_copy_path( $size, $sizes = null ) { @@ -1154,18 +1205,14 @@ protected function get_temporary_copy_path( $size, $sizes = null ) { return $info['dir_path'] . $info['file_base'] . static::TMP_SUFFIX . '.' . $info['extension']; } - - /** ----------------------------------------------------------------------------------------- */ - /** RESIZE FILE ============================================================================= */ - /** ----------------------------------------------------------------------------------------- */ - /** * Maybe resize an image. * * @since 1.9 * - * @param string $size The size name. - * @param File $file A File instance. + * @param string $size The size name. + * @param File $file A File instance. + * * @return array|WP_Error A WP_Error instance on failure, an array on success as follow: { * @type bool $resized True when the image has been resized. * @type bool $backuped True when the image has been backuped. @@ -1189,7 +1236,7 @@ public function maybe_resize( $size, $file ) { return new WP_Error( 'no_dimensions', sprintf( - /* translators: %s is an error message. */ + /* translators: %s is an error message. */ __( 'Resizing failed: %s', 'imagify' ), __( 'Imagify could not get the image dimensions.', 'imagify' ) ) @@ -1215,7 +1262,7 @@ public function maybe_resize( $size, $file ) { return new WP_Error( 'resize_failure', sprintf( - /* translators: %s is an error message. */ + /* translators: %s is an error message. */ __( 'Resizing failed: %s', 'imagify' ), $resized_path->get_error_message() ) @@ -1231,7 +1278,7 @@ public function maybe_resize( $size, $file ) { return new WP_Error( 'backup_failure', sprintf( - /* translators: %s is an error message. */ + /* translators: %s is an error message. */ __( 'Backup failed: %s', 'imagify' ), $backuped->get_error_message() ) @@ -1267,8 +1314,9 @@ public function maybe_resize( $size, $file ) { * * @since 1.9 * - * @param string $size The size name. - * @param File $file A File instance. + * @param string $size The size name. + * @param File $file A File instance. + * * @return bool */ protected function can_resize( $size, $file ) { @@ -1276,8 +1324,16 @@ protected function can_resize( $size, $file ) { return false; } - if ( 'full' !== $size && 'full' . static::WEBP_SUFFIX !== $size ) { - // We resize only the main file and its WebP version. + if ( + 'full' !== $size + && + ( + 'full' . static::WEBP_SUFFIX !== $size + || + 'full' . static::AVIF_SUFFIX !== $size + ) + ) { + // We resize only the main file and its next-gen version. return false; } @@ -1293,7 +1349,8 @@ protected function can_resize( $size, $file ) { * * @since 1.9 * - * @param string $size The size name. + * @param string $size The size name. + * * @return bool */ protected function can_backup( $size ) { @@ -1309,76 +1366,32 @@ protected function can_backup( $size ) { return $this->get_media()->get_context_instance()->can_backup(); } - - /** ----------------------------------------------------------------------------------------- */ - /** WEBP ==================================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** - * Generate WebP images if they are missing. - * - * @since 1.9 + * Get mime type * - * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. + * @param string $format nextgen image format. */ - public function generate_webp_versions() { - if ( ! $this->is_valid() ) { - return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); - } - - $media = $this->get_media(); - - if ( ! $media->is_image() ) { - return new WP_Error( 'no_webp', __( 'This media is not an image and cannot be converted to WebP format.', 'imagify' ) ); - } - - if ( ! $media->has_backup() ) { - return new WP_Error( 'no_backup', __( 'This media has no backup file.', 'imagify' ) ); - } - - $data = $this->get_data(); - - if ( ! $data->is_optimized() && ! $data->is_already_optimized() ) { - return new WP_Error( 'not_optimized', __( 'This media has not been optimized by Imagify yet.', 'imagify' ) ); - } - - if ( $this->has_webp() ) { - return new WP_Error( 'has_webp', __( 'This media already has WebP versions.', 'imagify' ) ); - } - - $files = $media->get_media_files(); - $sizes = []; - $args = [ - 'hook_suffix' => 'generate_webp_versions', + private function get_mime_type( $format ) { + $mime_types = [ + 'avif' => 'image/avif', + 'webp' => 'image/webp', ]; - foreach ( $files as $size_name => $file ) { - if ( 'image/webp' !== $files[ $size_name ]['mime-type'] ) { - array_unshift( $sizes, $size_name . static::WEBP_SUFFIX ); - } - } - - if ( ! $sizes ) { - return new \WP_Error( 'no_sizes', __( 'This media does not have files that can be converted to WebP format.', 'imagify' ) ); - } - - $optimization_level = $data->get_optimization_level(); - - // Optimize. - return $this->optimize_sizes( $sizes, $optimization_level, $args ); + return isset( $mime_types[ $format ] ) ? $mime_types[ $format ] : false; } /** - * Delete the WebP images. + * Delete the next gen format images. * This doesn't delete the related optimization data. * - * @since 1.9 - * @since 1.9.6 Return WP_Error or true. + * @since 2.2 + * + * @param bool $keep_full Set to true to keep the full size. + * @param bool $all_next_gen True: will delete every next-gen format. False: will delete only the current enabled format. * - * @param bool $keep_full Set to true to keep the full size. - * @return bool|WP_Error True on success. A \WP_Error object on failure. + * @return bool|WP_Error True on success. A WP_Error object on failure. */ - public function delete_webp_files( $keep_full = false ) { + public function delete_nextgen_files( $keep_full = false, $all_next_gen = false ) { if ( ! $this->is_valid() ) { return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } @@ -1403,7 +1416,7 @@ public function delete_webp_files( $keep_full = false ) { foreach ( $files as $file ) { if ( 0 === strpos( $file['mime-type'], 'image/' ) ) { - $deleted = $this->delete_webp_file( $file['path'] ); + $deleted = $this->delete_nextgen_file( $file['path'], $all_next_gen ); if ( is_wp_error( $deleted ) ) { ++$error_count; @@ -1415,7 +1428,7 @@ public function delete_webp_files( $keep_full = false ) { return new WP_Error( 'files_not_deleted', sprintf( - /* translators: %s is a formatted number, don’t use %d. */ + /* translators: %s is a formatted number, don’t use %d. */ _n( '%s file could not be deleted.', '%s files could not be deleted.', $error_count, 'imagify' ), number_format_i18n( $error_count ) ) @@ -1426,62 +1439,86 @@ public function delete_webp_files( $keep_full = false ) { } /** - * Delete a WebP image, given its non-WebP version's path. + * Delete a next gen format image, given its non-next-gen version's path. * This doesn't delete the related optimization data. * - * @since 1.9 - * @since 1.9.6 Return WP_Error or true. + * @since 2.2 + * + * @param string $file_path Path to the non-next-gen file. + * @param bool $all_next_gen True: will delete every next-gen format. False: will delete only the current enabled format. * - * @param string $file_path Path to the non-WebP file. - * @return bool|WP_Error True on success. A \WP_Error object on failure. + * @return void|WP_Error A \WP_Error object on failure. */ - protected function delete_webp_file( $file_path ) { + protected function delete_nextgen_file( $file_path, $all_next_gen = false ) { if ( ! $file_path ) { - return new WP_Error( 'no_path', __( 'Path to non-WebP file not provided.', 'imagify' ) ); + return new WP_Error( 'no_path', __( 'Path to non-next-gen file not provided.', 'imagify' ) ); } - $webp_file = new File( $file_path ); - $webp_path = $webp_file->get_path_to_webp(); + $next_gen_file = new File( $file_path ); + $formats = $this->extensions; + + if ( ! $all_next_gen ) { + $formats = imagify_nextgen_images_formats(); + } + // Delete next-gen images. + foreach ( $formats as $extension ) { + $path = $next_gen_file->get_path_to_nextgen( $extension ); + + if ( ! $path ) { + continue; + } + + $this->delete_file( $path ); + } + } - if ( ! $webp_path ) { - return new WP_Error( 'no_webp_path', __( 'Could not get the path to the WebP file.', 'imagify' ) ); + /** + * Delete a next gen format image, given its non-next-gen version's path. + * + * @param string $next_gen_path Path to the non-next-gen file. + * + * @return bool|WP_Error True on success. A WP_Error object on failure. + */ + protected function delete_file( string $next_gen_path ) { + if ( empty( $next_gen_path ) ) { + return new WP_Error( 'no_$next_gen_path', __( 'Could not get the path to the Next-Gen format file.', 'imagify' ) ); } - if ( ! $this->filesystem->exists( $webp_path ) ) { + if ( ! $this->filesystem->exists( $next_gen_path ) ) { return true; } - if ( ! $this->filesystem->is_writable( $webp_path ) ) { + if ( ! $this->filesystem->is_writable( $next_gen_path ) ) { return new WP_Error( 'file_not_writable', sprintf( - /* translators: %s is a file path. */ + /* translators: %s is a file path. */ __( 'The file %s does not seem to be writable.', 'imagify' ), - '' . esc_html( $this->filesystem->make_path_relative( $webp_path ) ) . '' + '' . esc_html( $this->filesystem->make_path_relative( $next_gen_path ) ) . '' ) ); } - if ( ! $this->filesystem->is_file( $webp_path ) ) { + if ( ! $this->filesystem->is_file( $next_gen_path ) ) { return new WP_Error( 'not_a_file', sprintf( - /* translators: %s is a file path. */ + /* translators: %s is a file path. */ __( 'This does not seem to be a file: %s.', 'imagify' ), - '' . esc_html( $this->filesystem->make_path_relative( $webp_path ) ) . '' + '' . esc_html( $this->filesystem->make_path_relative( $next_gen_path ) ) . '' ) ); } - $deleted = $this->filesystem->delete( $webp_path, false, 'f' ); + $deleted = $this->filesystem->delete( $next_gen_path, false, 'f' ); if ( ! $deleted ) { return new WP_Error( 'file_not_deleted', sprintf( - /* translators: %s is a file path. */ + /* translators: %s is a file path. */ __( 'The file %s could not be deleted.', 'imagify' ), - '' . esc_html( $this->filesystem->make_path_relative( $webp_path ) ) . '' + '' . esc_html( $this->filesystem->make_path_relative( $next_gen_path ) ) . '' ) ); } @@ -1490,35 +1527,61 @@ protected function delete_webp_file( $file_path ) { } /** - * Tell if a thumbnail size is an "Imagify WebP" size. + * Gives the next-gen image format we are processing. + * + * @return string Current format we are targeting. + */ + public function get_current_format() { + return $this->get_option( 'convert_to_avif' ) ? static::AVIF_SUFFIX : static::WEBP_SUFFIX; + } + + /** + * Tell if a thumbnail size is an "Imagify Next-Gen" size. * * @since 1.9 + * @since 2.2 addition of the format parameter. * - * @param string $size_name The size name. - * @return string|bool The unsuffixed name of the size if WebP. False if not WebP. + * @param string $size_name The size name. + * + * @return string|bool The unsuffixed name of the size if next-gen. False if not next-gen. */ - public function is_size_webp( $size_name ) { - static $suffix; + public function is_size_next_gen( $size_name ) { + $formats = imagify_nextgen_images_formats(); - if ( ! isset( $suffix ) ) { - $suffix = preg_quote( static::WEBP_SUFFIX, '/' ); - } + foreach ( $formats as $format ) { + $suffix = preg_quote( $this->get_suffix_from_format( $format ), '/' ); - if ( preg_match( '/^(?.+)' . $suffix . '$/', $size_name, $matches ) ) { - return $matches['size']; + if ( preg_match( '/^(?.+)' . $suffix . '$/', $size_name, $matches ) ) { + return $matches['size']; + } } return false; } /** - * Tell if the media has WebP versions. + * Get suffix from format. * - * @since 1.9 + * @param string $format Format extension of next-gen image. + * @return string + */ + private function get_suffix_from_format( string $format ): string { + $suffixes = [ + 'avif' => static::AVIF_SUFFIX, + 'webp' => static::WEBP_SUFFIX, + ]; + + return $suffixes[ $format ]; + } + + /** + * Tell if the media has a next gen format. + * + * @since 2.2 * * @return bool */ - public function has_webp() { + public function has_next_gen() { if ( ! $this->is_valid() ) { return false; } @@ -1533,18 +1596,18 @@ public function has_webp() { return false; } - $needle = static::WEBP_SUFFIX . '";a:4:{s:7:"success";b:1;'; + $needle = $this->format . '";a:4:{s:7:"success";b:1;'; $data = maybe_serialize( $data['sizes'] ); return is_string( $data ) && strpos( $data, $needle ); } /** - * Tell if the media has all WebP versions. + * Tell if the media has all Next-Gen versions. * * @return bool */ - public function is_full_webp() { + public function is_full_next_gen() { if ( ! $this->is_valid() ) { return false; } @@ -1562,39 +1625,42 @@ public function is_full_webp() { } $keys = array_keys( $sizes ); - $non_webp_keys = array_values(array_filter($keys, function ( $key ) { - return strpos( $key, static::WEBP_SUFFIX ) === false; + $non_next_gen_keys = array_values(array_filter($keys, function ( $key ) { + return strpos( $key, $this->format ) === false; })); - return array_reduce($non_webp_keys, function ( $is_fully, $key ) use ( $sizes ) { - return key_exists( $key . self::WEBP_SUFFIX, $sizes ) && $is_fully; + return array_reduce($non_next_gen_keys, function ( $is_fully, $key ) use ( $sizes ) { + return key_exists( $key . $this->format, $sizes ) && $is_fully; }, true); } /** - * Tell if a WebP version can be created for the given file. + * Tell if a Next-Gen version can be created for the given file. * Make sure the file is an image before using this method. * * @since 1.9.5 * * @param string $file_path Path to the file. + * * @return bool */ - public function can_create_webp_version( $file_path ) { + public function can_create_next_gen_version( $file_path ) { if ( ! $file_path ) { return false; } + $can = apply_filters_deprecated( 'imagify_pre_can_create_webp_version', array( null, $file_path ), '2.2', 'imagify_pre_can_create_next_gen_version' ); + /** - * Tell if a WebP version can be created for the given file. + * Tell if a next-gen version can be created for the given file. * The file is an image. * * @since 1.9.5 * - * @param bool $can True to create a WebP version, false otherwise. Null by default. + * @param bool $can True to create a next-gen version, false otherwise. Null by default. * @param string $file_path Path to the file. */ - $can = apply_filters( 'imagify_pre_can_create_webp_version', null, $file_path ); + $can = apply_filters( 'imagify_pre_can_create_next_gen_version', $can, $file_path ); if ( isset( $can ) ) { return (bool) $can; @@ -1611,10 +1677,71 @@ public function can_create_webp_version( $file_path ) { return true; } + /** + * Generate next-gen images if they are missing. + * + * @since 1.9 + * + * @return bool|WP_Error True if successfully launched. A WP_Error instance on failure. + */ + public function generate_nextgen_versions() { + if ( ! $this->is_valid() ) { + return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); + } + + $media = $this->get_media(); + + if ( ! $media->is_image() ) { + return new WP_Error( 'no_next_gen', __( 'This media is not an image and cannot be converted to next-gen format.', 'imagify' ) ); + } + + if ( ! $media->has_backup() ) { + return new WP_Error( 'no_backup', __( 'This media has no backup file.', 'imagify' ) ); + } + + $data = $this->get_data(); + + if ( ! $data->is_optimized() && ! $data->is_already_optimized() ) { + return new WP_Error( 'not_optimized', __( 'This media has not been optimized by Imagify yet.', 'imagify' ) ); + } + + if ( $this->has_next_gen() ) { + return new WP_Error( 'has_next_gen', __( 'This media already has next-gen versions.', 'imagify' ) ); + } + + $files = $media->get_media_files(); + $sizes = []; + $args = [ + 'hook_suffix' => 'generate_nextgen_versions', + ]; + + foreach ( $files as $size_name => $file ) { + $formats = imagify_nextgen_images_formats(); + + foreach ( $formats as $format ) { + if ( 'avif' === $format ) { + $format_suffix = static::AVIF_SUFFIX; + } elseif ( 'webp' === $format ) { + $format_suffix = static::WEBP_SUFFIX; + } + + if ( $this->get_mime_type( $format ) === $files[ $size_name ]['mime-type'] ) { + continue; + } + + array_unshift( $sizes, $size_name . $format_suffix ); + } + } + + if ( ! $sizes ) { + return new WP_Error( 'no_sizes', __( 'This media does not have files that can be converted to next-gen format.', 'imagify' ) ); + } + + $optimization_level = $data->get_optimization_level(); - /** ----------------------------------------------------------------------------------------- */ - /** PROCESS STATUS ========================================================================== */ - /** ----------------------------------------------------------------------------------------- */ + // Optimize. + return $this->optimize_sizes( $sizes, $optimization_level, $args ); + } /** * Tell if a process is running for this media. @@ -1721,11 +1848,6 @@ protected function validate_lock_action( $action ) { return $action; } - - /** ----------------------------------------------------------------------------------------- */ - /** DATA ==================================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** * Tell if a size already has optimization data. * @@ -1744,10 +1866,12 @@ public function size_has_optimization_data( $size ) { * Update the optimization data for a size. * * @since 1.9 + * @since 2.2 - Addition of the format in the call. + * + * @param object $response The API response. + * @param string $size The size name. + * @param int $level The optimization level (0=normal, 1=aggressive, 2=ultra). * - * @param object $response The API response. - * @param string $size The size name. - * @param int $level The optimization level (0=normal, 1=aggressive, 2=ultra). * @return array { * The optimization data. * @@ -1829,7 +1953,7 @@ public function update_size_optimization_data( $response, $size, $level ) { $data = (array) apply_filters( "imagify{$_unauthorized}_file_optimization_data", $data, $response, $size, $level, $this->get_data() ); if ( property_exists( $response, 'message' ) ) { - $size = str_replace( '@imagify-webp', '', $size ); + $size = str_replace( $this->format, '', $size ); } // Store. $this->get_data()->update_size_optimization_data( $size, $data ); @@ -1837,17 +1961,13 @@ public function update_size_optimization_data( $response, $size, $level ) { return $data; } - - /** ----------------------------------------------------------------------------------------- */ - /** VARIOUS TOOLS =========================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** * Get a plugin’s option. * * @since 1.9 * - * @param string $option_name The option nme. + * @param string $option_name The option name. + * * @return mixed */ protected function get_option( $option_name ) { @@ -1866,7 +1986,8 @@ protected function get_option( $option_name ) { * * @since 1.9 * - * @param mixed $optimization_level The optimization level. + * @param mixed $optimization_level The optimization level. + * * @return int */ protected function sanitize_optimization_level( $optimization_level ) { @@ -1880,4 +2001,32 @@ protected function sanitize_optimization_level( $optimization_level ) { return \Imagify_Options::get_instance()->sanitize_and_validate( 'optimization_level', $optimization_level ); } + + /** + * Tell if the media has AVIF versions. + * + * @since 2.2 + * + * @return bool + */ + public function has_avif() { + if ( ! $this->is_valid() ) { + return false; + } + + if ( ! $this->get_media()->is_image() ) { + return false; + } + + $data = $this->get_data()->get_optimization_data(); + + if ( empty( $data['sizes'] ) ) { + return false; + } + + $needle = static::AVIF_SUFFIX . '";a:4:{s:7:"success";b:1;'; + $data = maybe_serialize( $data['sizes'] ); + + return is_string( $data ) && strpos( $data, $needle ); + } } diff --git a/classes/Optimization/Process/Noop.php b/classes/Optimization/Process/Noop.php index 06a18aab8..7483088e7 100644 --- a/classes/Optimization/Process/Noop.php +++ b/classes/Optimization/Process/Noop.php @@ -1,31 +1,35 @@ get_capacity() for possible values. Can also be a "real" user capacity. * - * @param string $describer Capacity describer. See \Imagify\Context\ContextInterface->get_capacity() for possible values. Can also be a "real" user capacity. * @return bool */ public function current_user_can( $describer ) { return false; } - - /** ----------------------------------------------------------------------------------------- */ - /** OPTIMIZATION ============================================================================ */ - /** ----------------------------------------------------------------------------------------- */ - /** * Optimize a media files by pushing tasks into the queue. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 * - * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). - * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. + * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). + * + * @return bool|WP_Error True if successfully launched. A WP_Error instance on failure. */ public function optimize( $optimization_level = null ) { - return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); + return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } /** * Re-optimize a media files with a different level. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 + * + * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). * - * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). - * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. + * @return bool|WP_Error True if successfully launched. A WP_Error instance on failure. */ public function reoptimize( $optimization_level = null ) { - return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); + return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } /** * Optimize several file sizes by pushing tasks into the queue. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 * - * @param array $sizes An array of media sizes (strings). Use "full" for the size of the main file. - * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). - * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. + * @param array $sizes An array of media sizes (strings). Use "full" for the size of the main file. + * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). + * + * @return bool|WP_Error True if successfully launched. A WP_Error instance on failure. */ public function optimize_sizes( $sizes, $optimization_level = null ) { - return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); + return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } /** * Optimize one file with Imagify directly. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 + * + * @param string $size The media size. + * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). * - * @param string $size The media size. - * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). - * @return array|WP_Error The optimization data. A \WP_Error instance on failure. + * @return array|WP_Error The optimization data. A WP_Error instance on failure. */ public function optimize_size( $size, $optimization_level = null ) { - return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); + return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } /** * Restore the media files from the backup file. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 * - * @return bool|WP_Error True on success. A \WP_Error instance on failure. + * @return bool|WP_Error True on success. A WP_Error instance on failure. */ public function restore() { - return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); + return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } - - /** ----------------------------------------------------------------------------------------- */ - /** MISSING THUMBNAILS ====================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** * Get the sizes for this media that have not get through optimization. * No sizes are returned if the file is not optimized, has no backup, or is not an image. * The 'full' size os never returned. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 * * @return array|WP_Error { * A WP_Error object on failure. @@ -229,45 +204,30 @@ public function get_missing_sizes() { /** * Optimize missing thumbnail sizes. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 * - * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. + * @return bool|WP_Error True if successfully launched. A WP_Error instance on failure. */ public function optimize_missing_thumbnails() { - return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); + return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } - - /** ----------------------------------------------------------------------------------------- */ - /** BACKUP FILE ============================================================================= */ - /** ----------------------------------------------------------------------------------------- */ - /** * Delete the backup file. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 */ public function delete_backup() {} - - /** ----------------------------------------------------------------------------------------- */ - /** RESIZE FILE ============================================================================= */ - /** ----------------------------------------------------------------------------------------- */ - /** * Maybe resize an image. * - * @since 1.9 - * @access protected - * @author Grégory Viguier + * @since 1.9 + * + * @param string $size The size name. + * @param File $file A File instance. * - * @param string $size The size name. - * @param File $file A File instance. - * @return array|WP_Error A \WP_Error instance on failure, an array on success as follow: { + * @return array|WP_Error A WP_Error instance on failure, an array on success as follow: { * @type bool $resized True when the image has been resized. * @type bool $backuped True when the image has been backuped. * @type int $file_size The file size in bytes. @@ -281,58 +241,47 @@ public function maybe_resize( $size, $file ) { ]; } - - /** ----------------------------------------------------------------------------------------- */ - /** WEBP ==================================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** - * Generate WebP images if they are missing. + * Generate Nextgen images if they are missing. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 * - * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. + * @return bool|WP_Error True if successfully launched. A WP_Error instance on failure. */ - public function generate_webp_versions() { - return new \WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); + public function generate_nextgen_versions() { + return new WP_Error( 'invalid_media', __( 'This media is not valid.', 'imagify' ) ); } /** - * Delete the WebP images. + * Delete the next gen format images. + * This doesn't delete the related optimization data. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 2.2 + * + * @param bool $keep_full Set to true to keep the full size. + * @return bool|WP_Error True on success. A WP_Error object on failure. */ - public function delete_webp_files() {} + public function delete_nextgen_files( $keep_full = false ) { + return false; + } /** - * Tell if a thumbnail size is an "Imagify WebP" size. + * Tell if a thumbnail size is an "Imagify Next-Gen" size. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 2.2 * * @param string $size_name The size name. - * @return string|bool The unsuffixed name of the size if WebP. False if not WebP. + * + * @return string|bool The unsuffixed name of the size if Next-Gen. False if not a Next-Gen. */ - public function is_size_webp( $size_name ) { + public function is_size_next_gen( $size_name ) { return false; } - - /** ----------------------------------------------------------------------------------------- */ - /** PROCESS STATUS ========================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** * Tell if a process is running for this media. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 * * @return bool */ @@ -343,34 +292,24 @@ public function is_locked() { /** * Set the running status to "running" for a period of time. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 */ public function lock() {} /** * Delete the running status. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 */ public function unlock() {} - - /** ----------------------------------------------------------------------------------------- */ - /** DATA ==================================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** * Tell if a size already has optimization data. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 + * + * @param string $size The size name. * - * @param string $size The size name. * @return bool */ public function size_has_optimization_data( $size ) { @@ -380,13 +319,12 @@ public function size_has_optimization_data( $size ) { /** * Update the optimization data for a size. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 + * + * @param object $response The API response. + * @param string $size The size name. + * @param int $level The optimization level (0=normal, 1=aggressive, 2=ultra). * - * @param object $response The API response. - * @param string $size The size name. - * @param int $level The optimization level (0=normal, 1=aggressive, 2=ultra). * @return array { * The optimization data. * diff --git a/classes/Optimization/Process/ProcessInterface.php b/classes/Optimization/Process/ProcessInterface.php index fa525647f..278624da7 100644 --- a/classes/Optimization/Process/ProcessInterface.php +++ b/classes/Optimization/Process/ProcessInterface.php @@ -1,13 +1,12 @@ get_capacity() for possible values. Can also be a "real" user capacity. * - * @param string $describer Capacity describer. See \Imagify\Context\ContextInterface->get_capacity() for possible values. Can also be a "real" user capacity. * @return bool */ public function current_user_can( $describer ); - - /** ----------------------------------------------------------------------------------------- */ - /** OPTIMIZATION ============================================================================ */ - /** ----------------------------------------------------------------------------------------- */ - /** * Optimize a media files by pushing tasks into the queue. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 * - * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). - * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. + * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). + * + * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function optimize( $optimization_level = null ); /** * Re-optimize a media files with a different level. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 * - * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). - * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. + * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). + * + * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function reoptimize( $optimization_level = null ); /** * Optimize several file sizes by pushing tasks into the queue. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 * - * @param array $sizes An array of media sizes (strings). Use "full" for the size of the main file. + * @param array $sizes An array of media sizes (strings). Use "full" for the size of the main file. * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). - * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. + * + * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function optimize_sizes( $sizes, $optimization_level = null ); /** * Optimize one file with Imagify directly. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 * - * @param string $size The media size. - * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). - * @return array|WP_Error The optimization data. A \WP_Error instance on failure. + * @param string $size The media size. + * @param int $optimization_level The optimization level (0=normal, 1=aggressive, 2=ultra). + * + * @return array|WP_Error The optimization data. A \WP_Error instance on failure. */ public function optimize_size( $size, $optimization_level = null ); /** * Restore the media files from the backup file. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 * * @return bool|WP_Error True on success. A \WP_Error instance on failure. */ public function restore(); - - /** ----------------------------------------------------------------------------------------- */ - /** MISSING THUMBNAILS ====================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** * Get the sizes for this media that have not get through optimization. * No sizes are returned if the file is not optimized, has no backup, or is not an image. * The 'full' size os never returned. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 * * @return array|WP_Error { * A WP_Error object on failure. @@ -188,42 +157,27 @@ public function get_missing_sizes(); /** * Optimize missing thumbnail sizes. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 * * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ public function optimize_missing_thumbnails(); - - /** ----------------------------------------------------------------------------------------- */ - /** BACKUP FILE ============================================================================= */ - /** ----------------------------------------------------------------------------------------- */ - /** * Delete the backup file. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 */ public function delete_backup(); - - /** ----------------------------------------------------------------------------------------- */ - /** RESIZE FILE ============================================================================= */ - /** ----------------------------------------------------------------------------------------- */ - /** * Maybe resize an image. * - * @since 1.9 - * @access protected - * @author Grégory Viguier + * @since 1.9 + * + * @param string $size The size name. + * @param File $file A File instance. * - * @param string $size The size name. - * @param File $file A File instance. * @return array|WP_Error A \WP_Error instance on failure, an array on success as follow: { * @type bool $resized True when the image has been resized. * @type bool $backuped True when the image has been backuped. @@ -232,77 +186,58 @@ public function delete_backup(); */ public function maybe_resize( $size, $file ); - - /** ----------------------------------------------------------------------------------------- */ - /** WEBP ==================================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** - * Generate WebP images if they are missing. + * Generate next-gen images if they are missing. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 * * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ - public function generate_webp_versions(); + public function generate_nextgen_versions(); /** - * Delete the WebP images. + * Delete the next gen format images. * This doesn't delete the related optimization data. * - * @since 1.9 - * @since 1.9.6 Return WP_Error or true. - * @access public - * @author Grégory Viguier + * @since 2.2 + * + * @param bool $keep_full Set to true to keep the full size. * - * @param bool $keep_full Set to true to keep the full size. - * @return bool|\WP_Error True on success. A \WP_Error object on failure. + * @return bool|WP_Error True on success. A \WP_Error object on failure. */ - public function delete_webp_files( $keep_full = false ); + public function delete_nextgen_files( $keep_full = false ); /** - * Tell if a thumbnail size is an "Imagify WebP" size. + * Tell if a thumbnail size is an "Imagify Next-Gen" size. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 2.2 * - * @param string $size_name The size name. - * @return string|bool The unsuffixed name of the size if WebP. False if not WebP. + * @param string $size_name The size name. + * + * @return string|bool The unsuffixed name of the size if next-gen. False if not next-gen. */ - public function is_size_webp( $size_name ); + public function is_size_next_gen( $size_name ); /** - * Tell if the media has all WebP versions. + * Tell if the media has all next-gen versions. * * @return bool */ - public function is_full_webp(); + public function is_full_next_gen(); /** - * Tell if the media has WebP versions. + * Tell if the media has a next-gen format. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 2.2 * * @return bool */ - public function has_webp(); - - - /** ----------------------------------------------------------------------------------------- */ - /** PROCESS STATUS ========================================================================== */ - /** ----------------------------------------------------------------------------------------- */ + public function has_next_gen(); /** * Tell if a process is running for this media. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 * * @return bool */ @@ -311,34 +246,24 @@ public function is_locked(); /** * Set the running status to "running" for a period of time. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 */ public function lock(); /** * Delete the running status. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 */ public function unlock(); - - /** ----------------------------------------------------------------------------------------- */ - /** DATA ==================================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** * Tell if a size already has optimization data. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 + * + * @param string $size The size name. * - * @param string $size The size name. * @return bool */ public function size_has_optimization_data( $size ); @@ -346,13 +271,12 @@ public function size_has_optimization_data( $size ); /** * Update the optimization data for a size. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 + * + * @param object $response The API response. + * @param string $size The size name. + * @param int $level The optimization level (0=normal, 1=aggressive, 2=ultra). * - * @param object $response The API response. - * @param string $size The size name. - * @param int $level The optimization level (0=normal, 1=aggressive, 2=ultra). * @return array { * The optimization data. * diff --git a/classes/Webp/Picture/Display.php b/classes/Picture/Display.php similarity index 71% rename from classes/Webp/Picture/Display.php rename to classes/Picture/Display.php index 7a475327a..eb9e9be2a 100644 --- a/classes/Webp/Picture/Display.php +++ b/classes/Picture/Display.php @@ -1,57 +1,50 @@ tags. + * Display Next-gen images on the site with tags. * * @since 1.9 - * @author Grégory Viguier */ -class Display { - use \Imagify\Traits\InstanceGetterTrait; - +class Display implements SubscriberInterface { /** * Option value. * - * @var string - * @since 1.9 - * @author Grégory Viguier + * @var string */ const OPTION_VALUE = 'picture'; /** * Filesystem object. * - * @var \Imagify_Filesystem - * @since 1.9 - * @access protected - * @author Grégory Viguier + * @var Imagify_Filesystem */ protected $filesystem; /** * Constructor. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @param Imagify_Filesystem $filesystem Filesystem instance. */ - public function __construct() { - $this->filesystem = \Imagify_Filesystem::get_instance(); + public function __construct( Imagify_Filesystem $filesystem ) { + $this->filesystem = $filesystem; } /** - * Init. + * Array of events this subscriber listens to * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @return array */ - public function init() { - add_action( 'template_redirect', [ $this, 'start_content_process' ], -1000 ); - add_filter( 'imagify_process_webp_content', [ $this, 'process_content' ] ); + public static function get_subscribed_events() { + return [ + 'template_redirect' => [ 'start_content_process', -1000 ], + 'imagify_process_webp_content' => 'process_content', + ]; } /** ----------------------------------------------------------------------------------------- */ @@ -61,28 +54,29 @@ public function init() { /** * Start buffering the page content. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 + * + * @return void */ public function start_content_process() { - if ( ! get_imagify_option( 'display_webp' ) ) { + if ( ! get_imagify_option( 'display_nextgen' ) ) { return; } - if ( self::OPTION_VALUE !== get_imagify_option( 'display_webp_method' ) ) { + if ( self::OPTION_VALUE !== get_imagify_option( 'display_nextgen_method' ) ) { return; } + $allow = apply_filters_deprecated( 'imagify_allow_picture_tags_for_webp', [ true ], '2.2', 'imagify_allow_picture_tags_for_nextgen' ); + /** * Prevent the replacement of tags into tags. * - * @since 1.9 - * @author Grégory Viguier + * @since 1.9 * * @param bool $allow True to allow the use of tags (default). False to prevent their use. */ - $allow = apply_filters( 'imagify_allow_picture_tags_for_webp', true ); + $allow = apply_filters( 'imagify_allow_picture_tags_for_nextgen', true ); if ( ! $allow ) { return; @@ -94,11 +88,10 @@ public function start_content_process() { /** * Maybe process the page content. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 + * + * @param string $buffer The buffer content. * - * @param string $buffer The buffer content. * @return string */ public function maybe_process_buffer( $buffer ) { @@ -116,8 +109,7 @@ public function maybe_process_buffer( $buffer ) { /** * Filter the page content after Imagify. * - * @since 1.9 - * @author Grégory Viguier + * @since 1.9 * * @param string $buffer The page content. */ @@ -129,11 +121,10 @@ public function maybe_process_buffer( $buffer ) { /** * Process the content. * - * @since 1.9 - * @access public - * @author Grégory Viguier + * @since 1.9 + * + * @param string $content The content. * - * @param string $content The content. * @return string */ public function process_content( $content ) { @@ -181,13 +172,12 @@ private function remove_picture_tags( $html ) { /** * Build the tag to insert. * - * @since 1.9 - * @see $this->process_image() - * @access protected - * @author Grégory Viguier + * @since 1.9 + * @see $this->process_image() + * + * @param array $image An array of data. * - * @param array $image An array of data. - * @return string A tag. + * @return string A tag. */ protected function build_picture_tag( $image ) { $to_remove = [ @@ -210,8 +200,7 @@ protected function build_picture_tag( $image ) { /** * Filter the attributes to be added to the tag. * - * @since 1.9 - * @author Grégory Viguier + * @since 1.9 * * @param array $attributes A list of attributes to be added to the tag. * @param array $data Data built from the originale tag. See $this->process_image(). @@ -233,8 +222,7 @@ protected function build_picture_tag( $image ) { /** * Allow to add more tags to the tag. * - * @since 1.9 - * @author Grégory Viguier + * @since 1.9 * * @param string $more_source_tags Additional tags. * @param array $data Data built from the originale tag. See $this->process_image(). @@ -250,33 +238,74 @@ protected function build_picture_tag( $image ) { /** * Build the tag to insert in the . * - * @since 1.9 - * @see $this->process_image() - * @access protected - * @author Grégory Viguier + * @since 1.9 + * @see $this->process_image() * - * @param array $image An array of data. - * @return string A tag. + * @param array $image An array of data. + * + * @return string A tag. */ protected function build_source_tag( $image ) { + $source = ''; + + foreach ( [ 'avif', 'webp' ] as $image_type ) { + $attributes = $this->build_source_attributes( $image, $image_type ); + + if ( empty( $attributes ) ) { + continue; + } + + $source .= 'build_attributes( $attributes ) . "/>\n"; + } + + return $source; + } + + /** + * Build the attribute for the source tag. + * + * @param array $image An array of data. + * @param string $image_type Type of image. + * + * @return array + */ + protected function build_source_attributes( array $image, string $image_type ): array { + $mime_type = ''; + $url = ''; + + switch ( $image_type ) { + case 'webp': + $mime_type = 'image/webp'; + $url = 'webp_url'; + break; + case 'avif': + $mime_type = 'image/avif'; + $url = 'avif_url'; + break; + } + $srcset_source = ! empty( $image['srcset_attribute'] ) ? $image['srcset_attribute'] : $image['src_attribute'] . 'set'; $attributes = [ - 'type' => 'image/webp', + 'type' => $mime_type, $srcset_source => [], ]; if ( ! empty( $image['srcset'] ) ) { foreach ( $image['srcset'] as $srcset ) { - if ( empty( $srcset['webp_url'] ) ) { + if ( empty( $srcset[ $url ] ) ) { continue; } - $attributes[ $srcset_source ][] = $srcset['webp_url'] . ' ' . $srcset['descriptor']; + $attributes[ $srcset_source ][] = $srcset[ $url ] . ' ' . $srcset['descriptor']; } } + if ( empty( $attributes[ $srcset_source ] ) && empty( $image['src'][ $url ] ) ) { + return []; + } + if ( empty( $attributes[ $srcset_source ] ) ) { - $attributes[ $srcset_source ][] = $image['src']['webp_url']; + $attributes[ $srcset_source ][] = $image['src'][ $url ]; } $attributes[ $srcset_source ] = implode( ', ', $attributes[ $srcset_source ] ); @@ -301,27 +330,25 @@ protected function build_source_tag( $image ) { /** * Filter the attributes to be added to the tag. * - * @since 1.9 - * @author Grégory Viguier + * @since 1.9 * * @param array $attributes A list of attributes to be added to the tag. * @param array $data Data built from the original tag. See $this->process_image(). */ $attributes = apply_filters( 'imagify_picture_source_attributes', $attributes, $image ); - return 'build_attributes( $attributes ) . "/>\n"; + return $attributes; } /** * Build the tag to insert in the . * - * @since 1.9 - * @see $this->process_image() - * @access protected - * @author Grégory Viguier + * @since 1.9 + * @see $this->process_image() + * + * @param array $image An array of data. * - * @param array $image An array of data. - * @return string A tag. + * @return string A tag. */ protected function build_img_tag( $image ) { /** @@ -349,8 +376,7 @@ protected function build_img_tag( $image ) { /** * Filter the attributes to be added to the tag. * - * @since 1.9 - * @author Grégory Viguier + * @since 1.9 * * @param array $attributes A list of attributes to be added to the tag. * @param array $data Data built from the originale tag. See $this->process_image(). @@ -363,12 +389,11 @@ protected function build_img_tag( $image ) { /** * Create HTML attributes from an array. * - * @since 1.9 - * @access protected - * @author Grégory Viguier + * @since 1.9 + * + * @param array $attributes A list of attribute pairs. * - * @param array $attributes A list of attribute pairs. - * @return string HTML attributes. + * @return string HTML attributes. */ protected function build_attributes( $attributes ) { if ( ! $attributes || ! is_array( $attributes ) ) { @@ -391,11 +416,10 @@ protected function build_attributes( $attributes ) { /** * Get a list of images in a content. * - * @since 1.9 - * @access protected - * @author Grégory Viguier + * @since 1.9 + * + * @param string $content The content. * - * @param string $content The content. * @return array */ protected function get_images( $content ) { @@ -412,9 +436,8 @@ protected function get_images( $content ) { /** * Filter the images to display with a tag. * - * @since 1.9 - * @see $this->process_image() - * @author Grégory Viguier + * @since 1.9 + * @see $this->process_image() * * @param array $images A list of arrays. * @param string $content The page content. @@ -426,12 +449,23 @@ protected function get_images( $content ) { } foreach ( $images as $i => $image ) { - if ( empty( $image['src']['webp_exists'] ) || empty( $image['src']['webp_url'] ) ) { + if ( ( empty( $image['src']['webp_exists'] ) || empty( $image['src']['webp_url'] ) ) && + ( empty( $image['src']['avif_exists'] ) || empty( $image['src']['avif_url'] ) ) ) { + unset( $images[ $i ] ); continue; } + if ( empty( $image['src']['webp_exists'] ) || empty( $image['src']['webp_url'] ) ) { + unset( $images[ $i ]['src']['webp_url'] ); + } + + if ( empty( $image['src']['avif_exists'] ) || empty( $image['src']['avif_url'] ) ) { + unset( $images[ $i ]['src']['avif_url'] ); + } + unset( $images[ $i ]['src']['webp_path'], $images[ $i ]['src']['webp_exists'] ); + unset( $images[ $i ]['src']['avif_path'], $images[ $i ]['src']['avif_exists'] ); if ( empty( $image['srcset'] ) || ! is_array( $image['srcset'] ) ) { unset( $images[ $i ]['srcset'] ); @@ -443,11 +477,22 @@ protected function get_images( $content ) { continue; } + if ( ( empty( $srcset['webp_exists'] ) || empty( $srcset['webp_url'] ) ) && + ( empty( $srcset['avif_exists'] ) || empty( $srcset['avif_url'] ) ) ) { + unset( $images[ $i ]['srcset'][ $j ]['webp_url'] ); + unset( $images[ $i ]['srcset'][ $j ]['avif_url'] ); + } + if ( empty( $srcset['webp_exists'] ) || empty( $srcset['webp_url'] ) ) { unset( $images[ $i ]['srcset'][ $j ]['webp_url'] ); } + if ( empty( $srcset['avif_exists'] ) || empty( $srcset['avif_url'] ) ) { + unset( $images[ $i ]['srcset'][ $j ]['avif_url'] ); + } + unset( $images[ $i ]['srcset'][ $j ]['webp_path'], $images[ $i ]['srcset'][ $j ]['webp_exists'] ); + unset( $images[ $i ]['srcset'][ $j ]['avif_path'], $images[ $i ]['srcset'][ $j ]['avif_exists'] ); } } @@ -457,9 +502,7 @@ protected function get_images( $content ) { /** * Process an image tag and get an array containing some data. * - * @since 1.9 - * @access protected - * @author Grégory Viguier + * @since 1.9 * * @param string $image An image html tag. * @return array|false { @@ -527,24 +570,21 @@ protected function process_image( $image ) { return false; } - $webp_url = imagify_path_to_webp( $src['src'] ); - $webp_path = $this->url_to_path( $webp_url ); - $webp_url .= ! empty( $src['query'] ) ? $src['query'] : ''; - $data = [ 'tag' => $image, 'attributes' => $attributes, 'src_attribute' => $src_source, 'src' => [ 'url' => $attributes[ $src_source ], - 'webp_url' => $webp_url, - 'webp_path' => $webp_path, - 'webp_exists' => $webp_path && $this->filesystem->exists( $webp_path ), ], 'srcset_attribute' => false, 'srcset' => [], ]; + foreach ( $this->get_nextgen_image_data_set( $src ) as $key => $value ) { + $data['src'][ $key ] = $value; + } + // Deal with the srcset attribute. $srcset_source = false; @@ -556,6 +596,8 @@ protected function process_image( $image ) { } if ( $srcset_source ) { + $srcset_data = []; + $data['srcset_attribute'] = $srcset_source; $srcset = explode( ',', $attributes[ $srcset_source ] ); @@ -582,25 +624,23 @@ protected function process_image( $image ) { continue; } - $webp_url = imagify_path_to_webp( $src['src'] ); - $webp_path = $this->url_to_path( $webp_url ); - $webp_url .= ! empty( $src['query'] ) ? $src['query'] : ''; - - $data['srcset'][] = [ + $srcset_data = [ 'url' => $srcs[0], 'descriptor' => $srcs[1], - 'webp_url' => $webp_url, - 'webp_path' => $webp_path, - 'webp_exists' => $webp_path && $this->filesystem->exists( $webp_path ), ]; + + foreach ( $this->get_nextgen_image_data_set( $src ) as $key => $value ) { + $srcset_data[ $key ] = $value; + } + + $data['srcset'][] = $srcset_data; } } /** * Filter a processed image tag. * - * @since 1.9 - * @author Grégory Viguier + * @since 1.9 * * @param array $data An array of data for this image. * @param string $image An image html tag. @@ -618,14 +658,41 @@ protected function process_image( $image ) { return $data; } + /** + * Get the next-gen image(webp & avif) data set. + * + * @param array $src Array of url/path segments. + * + * @return array + */ + protected function get_nextgen_image_data_set( array $src ): array { + $webp_url = imagify_path_to_nextgen( $src['src'], 'webp' ); + $webp_path = $this->url_to_path( $webp_url ); + + $avif_url = imagify_path_to_nextgen( $src['src'], 'avif' ); + $avif_path = $this->url_to_path( $avif_url ); + $query_string = ! empty( $src['query'] ) ? $src['query'] : ''; + + return [ + // WebP data set. + 'webp_url' => $webp_url . $query_string, + 'webp_path' => $webp_path, + 'webp_exists' => $webp_path && $this->filesystem->exists( $webp_path ), + + // Avif data set. + 'avif_url' => $avif_url . $query_string, + 'avif_path' => $avif_path, + 'avif_exists' => $avif_path && $this->filesystem->exists( $avif_path ), + ]; + } + /** * Tell if a content is HTML. * - * @since 1.9 - * @access protected - * @author Grégory Viguier + * @since 1.9 + * + * @param string $content The content. * - * @param string $content The content. * @return bool */ protected function is_html( $content ) { @@ -635,11 +702,10 @@ protected function is_html( $content ) { /** * Convert a file URL to an absolute path. * - * @since 1.9 - * @access protected - * @author Grégory Viguier + * @since 1.9 + * + * @param string $url A file URL. * - * @param string $url A file URL. * @return string|bool The file path. False on failure. */ protected function url_to_path( $url ) { @@ -658,7 +724,7 @@ protected function url_to_path( $url ) { $uploads_dir = $this->filesystem->get_upload_basedir( true ); $root_url = set_url_scheme( $this->filesystem->get_site_root_url() ); $root_dir = $this->filesystem->get_site_root(); - $cdn_url = $this->get_cdn_source(); + $cdn_url = apply_filters( 'imagify_cdn_source_url', '' ); $cdn_url = $cdn_url['url'] ? set_url_scheme( $cdn_url['url'] ) : false; $domain_url = wp_parse_url( $root_url ); @@ -693,108 +759,4 @@ protected function url_to_path( $url ) { return false; } - - /** - * Get the CDN "source". - * - * @since 1.9.3 - * @access public - * @author Grégory Viguier - * - * @param string $option_url An URL to use instead of the one stored in the option. It is used only if no constant/filter. - * @return array { - * @type string $source Where does it come from? Possible values are 'constant', 'filter', or 'option'. - * @type string $name Who? Can be a constant name, a plugin name, or an empty string. - * @type string $url The CDN URL, with a trailing slash. An empty string if no URL is set. - * } - */ - public function get_cdn_source( $option_url = '' ) { - if ( defined( 'IMAGIFY_CDN_URL' ) && IMAGIFY_CDN_URL && is_string( IMAGIFY_CDN_URL ) ) { - // Use a constant. - $source = [ - 'source' => 'constant', - 'name' => 'IMAGIFY_CDN_URL', - 'url' => IMAGIFY_CDN_URL, - ]; - } else { - // Maybe use a filter. - $filter_source = [ - 'name' => null, - 'url' => null, - ]; - - /** - * Provide a custom CDN source. - * - * @since 1.9.3 - * @author Grégory Viguier - * - * @param array $filter_source { - * @type $name string The name of which provides the URL (plugin name, etc). - * @type $url string The CDN URL. - * } - */ - $filter_source = apply_filters( 'imagify_cdn_source', $filter_source ); - - if ( ! empty( $filter_source['url'] ) ) { - $source = [ - 'source' => 'filter', - 'name' => ! empty( $filter_source['name'] ) ? $filter_source['name'] : '', - 'url' => $filter_source['url'], - ]; - } - } - - if ( empty( $source['url'] ) ) { - // No constant, no filter: use the option. - $source = [ - 'source' => 'option', - 'name' => '', - 'url' => $option_url && is_string( $option_url ) ? $option_url : get_imagify_option( 'cdn_url' ), - ]; - } - - if ( empty( $source['url'] ) ) { - // Nothing set. - return [ - 'source' => 'option', - 'name' => '', - 'url' => '', - ]; - } - - $source['url'] = $this->sanitize_cdn_url( $source['url'] ); - - if ( empty( $source['url'] ) ) { - // Not an URL. - return [ - 'source' => 'option', - 'name' => '', - 'url' => '', - ]; - } - - return $source; - } - - /** - * Sanitize the CDN URL value. - * - * @since 1.9.3 - * @access public - * @author Grégory Viguier - * - * @param string $url The URL to sanitize. - * @return string - */ - public function sanitize_cdn_url( $url ) { - $url = sanitize_text_field( $url ); - - if ( ! $url || ! preg_match( '@^https?://.+\.[^.]+@i', $url ) ) { - // Not an URL. - return ''; - } - - return trailingslashit( $url ); - } } diff --git a/classes/Picture/ServiceProvider.php b/classes/Picture/ServiceProvider.php new file mode 100644 index 000000000..1e1f6efca --- /dev/null +++ b/classes/Picture/ServiceProvider.php @@ -0,0 +1,48 @@ +getContainer()->share( 'picture_display', Display::class ) + ->addArgument( $this->getContainer()->get( 'filesystem' ) ); + } + + /** + * Returns the subscribers array + * + * @return array + */ + public function get_subscribers() { + return $this->subscribers; + } +} diff --git a/classes/Plugin.php b/classes/Plugin.php index eaa0f1b55..7f10bbfb6 100644 --- a/classes/Plugin.php +++ b/classes/Plugin.php @@ -3,16 +3,33 @@ namespace Imagify; +use Imagify\Admin\AdminBar; use Imagify\Bulk\Bulk; -use Imagify\CLI\BulkOptimizeCommand; -use Imagify\CLI\GenerateMissingWebpCommand; +use Imagify\CLI\{BulkOptimizeCommand, GenerateMissingNextgenCommand}; +use Imagify\Dependencies\League\Container\Container; +use Imagify\Dependencies\League\Container\ServiceProvider\ServiceProviderInterface; +use Imagify\EventManagement\{EventManager, SubscriberInterface}; use Imagify\Notices\Notices; -use Imagify\Admin\AdminBar; +use Imagify_Filesystem; /** * Main plugin class. */ class Plugin { + /** + * Container instance. + * + * @var Container + */ + private $container; + + /** + * Is the plugin loaded + * + * @var boolean + */ + private $loaded = false; + /** * Absolute path to the plugin (with trailing slash). * @@ -25,22 +42,64 @@ class Plugin { * * @since 1.9 * - * @param array $plugin_args { + * @param Container $container Instance of the container. + * @param array $plugin_args { * An array of arguments. * * @type string $plugin_path Absolute path to the plugin (with trailing slash). * } */ - public function __construct( $plugin_args ) { + public function __construct( Container $container, $plugin_args ) { + $this->container = $container; $this->plugin_path = $plugin_args['plugin_path']; + + add_filter( 'imagify_container', [ $this, 'get_container' ] ); + } + + /** + * Returns the container instance. + * + * @return Container + */ + public function get_container() { + return $this->container; + } + + /** + * Checks if the plugin is loaded + * + * @return boolean + */ + private function is_loaded(): bool { + return $this->loaded; } /** * Plugin init. * + * @param array $providers Array of service providers. + * * @since 1.9 */ - public function init() { + public function init( $providers ) { + if ( $this->is_loaded() ) { + return; + } + + $this->container->share( + 'event_manager', + function () { + return new EventManager(); + } + ); + + $this->container->share( + 'filesystem', + function() { + return new Imagify_Filesystem(); + } + ); + $this->include_files(); class_alias( '\\Imagify\\Traits\\InstanceGetterTrait', '\\Imagify\\Traits\\FakeSingletonTrait' ); @@ -55,7 +114,6 @@ class_alias( '\\Imagify\\Traits\\InstanceGetterTrait', '\\Imagify\\Traits\\FakeS \Imagify_Cron_Sync_Files::get_instance()->init(); \Imagify\Auth\Basic::get_instance()->init(); \Imagify\Job\MediaOptimization::get_instance()->init(); - \Imagify\Stats\OptimizedMediaWithoutWebp::get_instance()->init(); Bulk::get_instance()->init(); AdminBar::get_instance()->init(); @@ -72,15 +130,21 @@ class_alias( '\\Imagify\\Traits\\InstanceGetterTrait', '\\Imagify\\Traits\\FakeS \Imagify_Assets::get_instance()->init(); } - \Imagify\Webp\Display::get_instance()->init(); - add_action( 'init', [ $this, 'maybe_activate' ] ); // Load plugin translations. imagify_load_translations(); imagify_add_command( new BulkOptimizeCommand() ); - imagify_add_command( new GenerateMissingWebpCommand() ); + imagify_add_command( new GenerateMissingNextgenCommand() ); + + foreach ( $providers as $service_provider ) { + $provider_instance = new $service_provider(); + $this->container->addServiceProvider( $provider_instance ); + + // Load each service provider's subscribers if found. + $this->load_subscribers( $provider_instance ); + } /** * Fires when Imagify is fully loaded. @@ -91,6 +155,8 @@ class_alias( '\\Imagify\\Traits\\InstanceGetterTrait', '\\Imagify\\Traits\\FakeS * @param \Imagify_Plugin $plugin Instance of this class. */ do_action( 'imagify_loaded', $this ); + + $this->loaded = true; } /** @@ -171,4 +237,25 @@ public function maybe_activate() { */ do_action( 'imagify_activation', (int) $user_id ); } + + /** + * Load list of event subscribers from service provider. + * + * @param ServiceProviderInterface $service_provider Instance of service provider. + * + * @return void + */ + private function load_subscribers( ServiceProviderInterface $service_provider ) { + if ( empty( $service_provider->get_subscribers() ) ) { + return; + } + + foreach ( $service_provider->get_subscribers() as $subscriber ) { + $subscriber_object = $this->container->get( $subscriber ); + + if ( $subscriber_object instanceof SubscriberInterface ) { + $this->container->get( 'event_manager' )->add_subscriber( $subscriber_object ); + } + } + } } diff --git a/classes/Stats/OptimizedMediaWithoutWebp.php b/classes/Stats/OptimizedMediaWithoutNextGen.php similarity index 63% rename from classes/Stats/OptimizedMediaWithoutWebp.php rename to classes/Stats/OptimizedMediaWithoutNextGen.php index 0fbfcd17e..593809b37 100644 --- a/classes/Stats/OptimizedMediaWithoutWebp.php +++ b/classes/Stats/OptimizedMediaWithoutNextGen.php @@ -1,15 +1,16 @@ [ 'maybe_clear_cache_after_optimization', 10, 2 ], + 'imagify_after_restore_media' => [ 'maybe_clear_cache_after_restoration', 10, 4 ], + 'imagify_delete_media' => 'maybe_clear_cache_on_deletion', + 'update_option_imagify_settings' => [ 'maybe_clear_stat_cache', 9, 2 ], + ]; } - - /** ----------------------------------------------------------------------------------------- */ - /** GET/CACHE THE STAT ====================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** - * Get the number of optimized media without WebP versions. + * Get the number of optimized media without next-gen versions. * - * @since 1.9 + * @since 2.2 * * @return int */ @@ -48,16 +47,16 @@ public function get_stat() { // Sum the counts of each context. foreach ( imagify_get_context_names() as $context ) { - $stat += $bulk->get_bulk_instance( $context )->has_optimized_media_without_webp(); + $stat += $bulk->get_bulk_instance( $context )->has_optimized_media_without_nextgen(); } return $stat; } /** - * Get and cache the number of optimized media without WebP versions. + * Get and cache the number of optimized media without next-gen versions. * - * @since 1.9 + * @since 2.2 * * @return int */ @@ -83,21 +82,16 @@ public function get_cached_stat() { /** * Clear the stat cache. * - * @since 1.9 + * @since 2.2 */ public function clear_cache() { delete_transient( static::NAME ); } - - /** ----------------------------------------------------------------------------------------- */ - /** HOOKS =================================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** * Clear cache after optimizing a media. * - * @since 1.9 + * @since 2.2 * * @param ProcessInterface $process The optimization process. * @param array $item The item being processed. @@ -113,11 +107,15 @@ public function maybe_clear_cache_after_optimization( $process, $item ) { $new_sizes = array_intersect_key( $sizes, $new_sizes ); $size_name = 'full' . $process::WEBP_SUFFIX; + if ( get_imagify_option( 'convert_to_avif' ) ) { + $size_name = 'full' . $process::AVIF_SUFFIX; + } + if ( ! isset( $new_sizes['full'] ) && ! empty( $new_sizes[ $size_name ]['success'] ) ) { /** - * We just successfully generated the WebP version of the full size. + * We just successfully generated the next-gen version of the full size. * The full size was not optimized at the same time, that means it was optimized previously. - * Meaning: we just added a WebP version to a media that was previously optimized, so there is one less optimized media without WebP. + * Meaning: we just added a next-gen version to a media that was previously optimized, so there is one less optimized media without next-gen. */ $this->clear_cache(); return; @@ -125,7 +123,7 @@ public function maybe_clear_cache_after_optimization( $process, $item ) { if ( ! empty( $new_sizes['full']['success'] ) && empty( $new_sizes[ $size_name ]['success'] ) ) { /** - * We now have a new optimized media without WebP. + * We now have a new optimized media without next-gen. */ $this->clear_cache(); } @@ -134,7 +132,7 @@ public function maybe_clear_cache_after_optimization( $process, $item ) { /** * Clear cache after restoring a media. * - * @since 1.9 + * @since 2.2 * * @param ProcessInterface $process The optimization process. * @param bool|WP_Error $response The result of the operation: true on success, a WP_Error object on failure. @@ -149,9 +147,13 @@ public function maybe_clear_cache_after_restoration( $process, $response, $files $sizes = isset( $data['sizes'] ) ? (array) $data['sizes'] : []; $size_name = 'full' . $process::WEBP_SUFFIX; + if ( get_imagify_option( 'convert_to_avif' ) ) { + $size_name = 'full' . $process::AVIF_SUFFIX; + } + if ( ! empty( $sizes['full']['success'] ) && empty( $sizes[ $size_name ]['success'] ) ) { /** - * This media had no WebP versions. + * This media had no next-gen versions. */ $this->clear_cache(); } @@ -160,7 +162,7 @@ public function maybe_clear_cache_after_restoration( $process, $response, $files /** * Clear cache on media deletion. * - * @since 1.9 + * @since 2.2 * * @param ProcessInterface $process An optimization process. */ @@ -173,11 +175,37 @@ public function maybe_clear_cache_on_deletion( $process ) { $sizes = isset( $data['sizes'] ) ? (array) $data['sizes'] : []; $size_name = 'full' . $process::WEBP_SUFFIX; + if ( get_imagify_option( 'convert_to_avif' ) ) { + $size_name = 'full' . $process::AVIF_SUFFIX; + } + if ( ! empty( $sizes['full']['success'] ) && empty( $sizes[ $size_name ]['success'] ) ) { /** - * This media had no WebP versions. + * This media had no next-gen versions. */ $this->clear_cache(); } } + + /** + * Maybe clear the stat cache on option change + * + * @since 2.2 + * + * @param array $old_value The old option value. + * @param array $value The new option value. + * + * @return void + */ + public function maybe_clear_stat_cache( $old_value, $value ) { + if ( isset( $old_value['convert_to_avif'] ) && isset( $value['convert_to_avif'] ) ) { + return; + } + + if ( ! isset( $old_value['convert_to_avif'] ) && ! isset( $value['convert_to_avif'] ) ) { + return; + } + + $this->clear_cache(); + } } diff --git a/classes/Stats/ServiceProvider.php b/classes/Stats/ServiceProvider.php new file mode 100644 index 000000000..afb855147 --- /dev/null +++ b/classes/Stats/ServiceProvider.php @@ -0,0 +1,47 @@ +getContainer()->share( 'optimized_media_without_next_gen', OptimizedMediaWithoutNextGen::class ); + } + + /** + * Returns the subscribers array + * + * @return array + */ + public function get_subscribers() { + return $this->subscribers; + } +} diff --git a/classes/Webp/Apache.php b/classes/Webp/Apache.php index 749f6b87f..4a74e0412 100644 --- a/classes/Webp/Apache.php +++ b/classes/Webp/Apache.php @@ -1,31 +1,29 @@ init(); - RewriteRules\Display::get_instance()->init(); + public static function get_subscribed_events() { + return [ + 'imagify_settings_on_save' => [ 'maybe_add_rewrite_rules', 13 ], + 'imagify_settings_webp_info' => 'maybe_add_webp_info', + 'imagify_activation' => 'activate', + 'imagify_deactivation' => 'deactivate', + ]; } - /** ----------------------------------------------------------------------------------------- */ - /** HOOKS =================================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** * If display WebP images, add the WebP type to the .htaccess/etc file. * * @since 1.9 * - * @param array $values The option values. + * @param array $values The option values. + * * @return array */ public function maybe_add_rewrite_rules( $values ) { - $old_value = (bool) get_imagify_option( 'display_webp' ); - // See \Imagify_Options->validate_values_on_update() for why we use 'convert_to_webp' here. - $new_value = ! empty( $values['display_webp'] ) && ! empty( $values['convert_to_webp'] ); - - if ( $old_value === $new_value ) { - // No changes. - return $values; - } - if ( ! $this->get_server_conf() ) { return $values; } - if ( $new_value ) { + $enabled = isset( $values['display_nextgen'] ) ? true : false; + $result = false; + + if ( $enabled ) { // Add the WebP file type. $result = $this->get_server_conf()->add(); - } else { + } elseif ( ! $enabled ) { // Remove the WebP file type. $result = $this->get_server_conf()->remove(); } @@ -76,10 +70,12 @@ public function maybe_add_rewrite_rules( $values ) { // Display an error message. if ( is_multisite() && strpos( wp_get_referer(), network_admin_url( '/' ) ) === 0 ) { Notices::get_instance()->add_network_temporary_notice( $result->get_error_message() ); - } else { - Notices::get_instance()->add_site_temporary_notice( $result->get_error_message() ); + + return $values; } + Notices::get_instance()->add_site_temporary_notice( $result->get_error_message() ); + return $values; } @@ -130,9 +126,11 @@ public function activate() { if ( ! $conf ) { return; } - if ( ! get_imagify_option( 'display_webp' ) ) { + + if ( ! get_imagify_option( 'display_nextgen' ) ) { return; } + if ( is_wp_error( $conf->is_file_writable() ) ) { return; } @@ -151,9 +149,6 @@ public function deactivate() { if ( ! $conf ) { return; } - if ( ! get_imagify_option( 'display_webp' ) ) { - return; - } $file_path = $conf->get_file_path(); $filesystem = \Imagify_Filesystem::get_instance(); @@ -168,10 +163,6 @@ public function deactivate() { $conf->remove(); } - /** ----------------------------------------------------------------------------------------- */ - /** TOOLS =================================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** * Get the path to the directory conf file. * @@ -194,30 +185,13 @@ public function get_file_path( $relative = false ) { return $file_path; } - /** - * Get the WebP display method by validating the given value. - * - * @since 1.9 - * - * @param array $values The option values. - * @return string 'picture' or 'rewrite'. - */ - public function get_display_webp_method( $values ) { - $options = \Imagify_Options::get_instance(); - $default = $options->get_default_values(); - $default = $default['display_webp_method']; - $method = ! empty( $values['display_webp_method'] ) ? $values['display_webp_method'] : ''; - - return $options->sanitize_and_validate( 'display_webp_method', $method, $default ); - } - /** * Get the server conf instance. * Note: nothing needed for nginx. * * @since 1.9 * - * @return \Imagify\WriteFile\WriteFileInterface + * @return WriteFileInterface */ protected function get_server_conf() { global $is_apache, $is_iis7; @@ -230,8 +204,6 @@ protected function get_server_conf() { $this->server_conf = new Apache(); } elseif ( $is_iis7 ) { $this->server_conf = new IIS(); - } else { - $this->server_conf = false; } return $this->server_conf; diff --git a/classes/Webp/IIS.php b/classes/Webp/IIS.php index 1f65239ed..faa74607b 100644 --- a/classes/Webp/IIS.php +++ b/classes/Webp/IIS.php @@ -1,31 +1,29 @@ get_extensions_pattern(); + $extensions = str_replace( '|webp', '', $extensions ); $home_root = wp_parse_url( home_url( '/' ) ); $home_root = $home_root['path']; diff --git a/classes/Webp/RewriteRules/Display.php b/classes/Webp/RewriteRules/Display.php index 8bc06dbd3..82cb2632a 100644 --- a/classes/Webp/RewriteRules/Display.php +++ b/classes/Webp/RewriteRules/Display.php @@ -1,100 +1,80 @@ [ 'maybe_add_rewrite_rules', 10 ], + 'imagify_settings_webp_info' => 'maybe_add_webp_info', + 'imagify_activation' => 'activate', + 'imagify_deactivation' => 'deactivate', + ]; } - /** ----------------------------------------------------------------------------------------- */ - /** HOOKS =================================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** * If display WebP images via rewrite rules, add the rules to the .htaccess/etc file. * * @since 1.9 * - * @param array $values The option values. + * @param array $values The option values. + * * @return array */ public function maybe_add_rewrite_rules( $values ) { - global $is_apache, $is_iis7, $is_nginx; - - // Display WebP? - $was_enabled = (bool) get_imagify_option( 'display_webp' ); - // See \Imagify_Options->validate_values_on_update() for why we use 'convert_to_webp' here. - $is_enabled = ! empty( $values['display_webp'] ) && ! empty( $values['convert_to_webp'] ); + $was_enabled = (bool) get_imagify_option( 'display_nextgen' ); + $is_enabled = ! empty( $values['display_nextgen'] ); // Which method? - $old_value = get_imagify_option( 'display_webp_method' ); - $new_value = ! empty( $values['display_webp_method'] ) ? $values['display_webp_method'] : ''; + $old_value = get_imagify_option( 'display_nextgen_method' ); + $new_value = ! empty( $values['display_nextgen_method'] ) ? $values['display_nextgen_method'] : ''; // Decide when to add or remove rules. - $is_rewrite = self::OPTION_VALUE === $new_value; - $was_rewrite = self::OPTION_VALUE === $old_value; - $add_or_remove = false; + $is_rewrite = self::OPTION_VALUE === $new_value; + $was_rewrite = self::OPTION_VALUE === $old_value; - if ( $is_enabled && $is_rewrite && ( ! $was_enabled || ! $was_rewrite ) ) { - // Display WebP & use rewrite method, but only if one of the values changed: add rules. - $add_or_remove = 'add'; - } elseif ( $was_enabled && $was_rewrite && ( ! $is_enabled || ! $is_rewrite ) ) { - // Was displaying WebP & was using rewrite method, but only if one of the values changed: remove rules. - $add_or_remove = 'remove'; - } else { + if ( ! $this->get_server_conf() ) { return $values; } - if ( $is_apache ) { - $rules = new Apache(); - } elseif ( $is_iis7 ) { - $rules = new IIS(); - } elseif ( $is_nginx ) { - $rules = new Nginx(); - } else { - return $values; - } + $result = false; - if ( 'add' === $add_or_remove ) { + if ( $is_enabled && $is_rewrite && ( ! $was_enabled || ! $was_rewrite ) ) { // Add the rewrite rules. - $result = $rules->add(); - } else { + $result = $this->get_server_conf()->add(); + } elseif ( $was_enabled && $was_rewrite && ( ! $is_enabled || ! $is_rewrite ) ) { // Remove the rewrite rules. - $result = $rules->remove(); + $result = $this->get_server_conf()->remove(); } if ( ! is_wp_error( $result ) ) { @@ -104,10 +84,12 @@ public function maybe_add_rewrite_rules( $values ) { // Display an error message. if ( is_multisite() && strpos( wp_get_referer(), network_admin_url( '/' ) ) === 0 ) { Notices::get_instance()->add_network_temporary_notice( $result->get_error_message() ); - } else { - Notices::get_instance()->add_site_temporary_notice( $result->get_error_message() ); + + return $values; } + Notices::get_instance()->add_site_temporary_notice( $result->get_error_message() ); + return $values; } @@ -162,10 +144,11 @@ public function activate() { if ( ! $conf ) { return; } - if ( ! get_imagify_option( 'display_webp' ) ) { + + if ( ! get_imagify_option( 'display_nextgen' ) ) { return; } - if ( self::OPTION_VALUE !== get_imagify_option( 'display_webp_method' ) ) { + if ( self::OPTION_VALUE !== get_imagify_option( 'display_nextgen_method' ) ) { return; } if ( is_wp_error( $conf->is_file_writable() ) ) { @@ -186,10 +169,10 @@ public function deactivate() { if ( ! $conf ) { return; } - if ( ! get_imagify_option( 'display_webp' ) ) { + if ( ! get_imagify_option( 'display_nextgen' ) ) { return; } - if ( self::OPTION_VALUE !== get_imagify_option( 'display_webp_method' ) ) { + if ( self::OPTION_VALUE !== get_imagify_option( 'display_nextgen_method' ) ) { return; } @@ -206,17 +189,14 @@ public function deactivate() { $conf->remove(); } - /** ----------------------------------------------------------------------------------------- */ - /** TOOLS =================================================================================== */ - /** ----------------------------------------------------------------------------------------- */ - /** * Get the path to the directory conf file. * * @since 1.9 * * @param bool $relative True to get a path relative to the site’s root. - * @return string|bool The file path. False on failure. + * + * @return string|bool The file path. False on failure. */ public function get_file_path( $relative = false ) { if ( ! $this->get_server_conf() ) { @@ -237,7 +217,7 @@ public function get_file_path( $relative = false ) { * * @since 1.9 * - * @return \Imagify\WriteFile\WriteFileInterface + * @return WriteFileInterface */ protected function get_server_conf() { global $is_apache, $is_iis7, $is_nginx; @@ -252,8 +232,6 @@ protected function get_server_conf() { $this->server_conf = new IIS(); } elseif ( $is_nginx ) { $this->server_conf = new Nginx(); - } else { - $this->server_conf = false; } return $this->server_conf; diff --git a/classes/Webp/RewriteRules/IIS.php b/classes/Webp/RewriteRules/IIS.php index 18ab82708..37abef0ed 100644 --- a/classes/Webp/RewriteRules/IIS.php +++ b/classes/Webp/RewriteRules/IIS.php @@ -1,37 +1,36 @@ get_extensions_pattern(); + $extensions = str_replace( '|webp', '', $extensions ); $home_root = wp_parse_url( home_url( '/' ) ); $home_root = $home_root['path']; diff --git a/classes/Webp/RewriteRules/Nginx.php b/classes/Webp/RewriteRules/Nginx.php index 428166f2a..bcc640f42 100644 --- a/classes/Webp/RewriteRules/Nginx.php +++ b/classes/Webp/RewriteRules/Nginx.php @@ -1,52 +1,70 @@ get_extensions_pattern(); + $extensions = $this->get_extensions_pattern() . '|avif'; $home_root = wp_parse_url( home_url( '/' ) ); $home_root = $home_root['path']; return trim( ' location ~* ^(' . $home_root . '.+)\.(' . $extensions . ')$ { - add_header Vary Accept; + add_header Vary Accept; - if ($http_accept ~* "webp"){ - set $imwebp A; - } - if (-f $request_filename.webp) { - set $imwebp "${imwebp}B"; - } - if ($imwebp = AB) { - rewrite ^(.*) $1.webp; - } + set $canavif 1; + + if ($http_accept !~* "avif"){ + set $canavif 0; + } + + if (!-f $request_filename.avif) { + set $canavif 0; + + } + if ($canavif = 1){ + rewrite ^(.*) $1.avif; + break; + } + + set $canwebp 1; + + if ($http_accept !~* "webp"){ + set $canwebp 0; + } + + if (!-f $request_filename.webp) { + set $canwebp 0; + + } + if ($canwebp = 1){ + rewrite ^(.*) $1.webp; + break; + } }' ); } } diff --git a/classes/Webp/ServiceProvider.php b/classes/Webp/ServiceProvider.php new file mode 100644 index 000000000..12d45ddfa --- /dev/null +++ b/classes/Webp/ServiceProvider.php @@ -0,0 +1,51 @@ +getContainer()->share( 'webp_display', Display::class ); + $this->getContainer()->share( 'webp_rewrite_rules', RewriteRules::class ); + } + + /** + * Returns the subscribers array + * + * @return array + */ + public function get_subscribers() { + return $this->subscribers; + } +} diff --git a/composer.json b/composer.json index 7c08dcbcf..4cc4a0d8b 100644 --- a/composer.json +++ b/composer.json @@ -42,12 +42,14 @@ "coenjacobs/mozart": "^0.7", "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", "deliciousbrains/wp-background-processing": "~1.0", + "league/container": "^3.0", "mnsami/composer-custom-directory-installer": "^2.0", "phpcompatibility/phpcompatibility-wp": "^2.0", "phpunit/phpunit": "^7.5", "roave/security-advisories": "dev-master", "woocommerce/action-scheduler": "^3.4", "wp-coding-standards/wpcs": "^2", + "wp-media/event-manager": "^3.1", "wp-media/phpunit": "2.0" }, "autoload": { @@ -83,7 +85,9 @@ "classmap_directory": "/inc/classes/Dependencies/", "classmap_prefix": "Imagify_", "packages": [ - "deliciousbrains/wp-background-processing" + "deliciousbrains/wp-background-processing", + "league/container", + "wp-media/event-manager" ] } }, diff --git a/config/providers.php b/config/providers.php new file mode 100644 index 000000000..0a40a32a6 --- /dev/null +++ b/config/providers.php @@ -0,0 +1,8 @@ + tag. * * @since 1.9 - * @see \Imagify\Webp\Picture\Display->process_image() + * @see \Imagify\Picture\Display->process_image() * @author Grégory Viguier * * @param array $data An array of data for this image. @@ -443,7 +443,7 @@ public function add_webp_images_to_attachment( $paths, $attachment_id, $metadata } foreach ( $paths as $size_name => $file_path ) { - if ( 'thumb' === $size_name || 'backup' === $size_name || $process->is_size_webp( $size_name ) ) { + if ( 'thumb' === $size_name || 'backup' === $size_name || $process->is_size_next_gen( $size_name ) ) { continue; } diff --git a/inc/3rd-party/nextgen-gallery/classes/Bulk/NGG.php b/inc/3rd-party/nextgen-gallery/classes/Bulk/NGG.php index 7b6bae674..38751bd19 100644 --- a/inc/3rd-party/nextgen-gallery/classes/Bulk/NGG.php +++ b/inc/3rd-party/nextgen-gallery/classes/Bulk/NGG.php @@ -100,10 +100,11 @@ public function get_unoptimized_media_ids( $optimization_level ) { } /** - * Get ids of all optimized media without WebP versions. + * Get ids of all optimized media without next-gen versions. * - * @since 1.9 - * @since 1.9.5 The method doesn't return the IDs directly anymore. + * @since 2.2 + * + * @param string $format Format we are looking for. (webp|avif). * * @return array { * @type array $ids A list of media IDs. @@ -113,7 +114,7 @@ public function get_unoptimized_media_ids( $optimization_level ) { * } * } */ - public function get_optimized_media_ids_without_webp() { + public function get_optimized_media_ids_without_format( $format ) { global $wpdb; $this->set_no_time_limit(); @@ -121,7 +122,12 @@ public function get_optimized_media_ids_without_webp() { $storage = C_Gallery_Storage::get_instance(); $ngg_table = $wpdb->prefix . 'ngg_pictures'; $data_table = DB::get_instance()->get_table_name(); - $webp_suffix = constant( imagify_get_optimization_process_class_name( 'ngg' ) . '::WEBP_SUFFIX' ); + $suffix = constant( imagify_get_optimization_process_class_name( 'ngg' ) . '::WEBP_SUFFIX' ); + + if ( get_imagify_option( 'convert_to_avif' ) ) { + $suffix = constant( imagify_get_optimization_process_class_name( 'ngg' ) . '::AVIF_SUFFIX' ); + } + $files = $wpdb->get_col( $wpdb->prepare( // WPCS: unprepared SQL ok. " SELECT ngg.pid @@ -132,7 +138,7 @@ public function get_optimized_media_ids_without_webp() { ( data.status = 'success' OR data.status = 'already_optimized' ) AND data.data NOT LIKE %s ORDER BY ngg.pid DESC", - '%' . $wpdb->esc_like( $webp_suffix . '";a:4:{s:7:"success";b:1;' ) . '%' + '%' . $wpdb->esc_like( $suffix . '";a:4:{s:7:"success";b:1;' ) . '%' ) ); $wpdb->flush(); @@ -175,18 +181,22 @@ public function get_optimized_media_ids_without_webp() { } /** - * Tell if there are optimized media without WebP versions. + * Tell if there are optimized media without next-gen versions. * - * @since 1.9 + * @since 2.2 * * @return int The number of media. */ - public function has_optimized_media_without_webp() { + public function has_optimized_media_without_nextgen() { global $wpdb; $ngg_table = $wpdb->prefix . 'ngg_pictures'; $data_table = DB::get_instance()->get_table_name(); - $webp_suffix = constant( imagify_get_optimization_process_class_name( 'ngg' ) . '::WEBP_SUFFIX' ); + $suffix = constant( imagify_get_optimization_process_class_name( 'ngg' ) . '::WEBP_SUFFIX' ); + + if ( get_imagify_option( 'convert_to_avif' ) ) { + $suffix = constant( imagify_get_optimization_process_class_name( 'ngg' ) . '::AVIF_SUFFIX' ); + } return (int) $wpdb->get_var( $wpdb->prepare( // WPCS: unprepared SQL ok. " @@ -197,7 +207,7 @@ public function has_optimized_media_without_webp() { WHERE ( data.status = 'success' OR data.status = 'already_optimized' ) AND data.data NOT LIKE %s", - '%' . $wpdb->esc_like( $webp_suffix . '";a:4:{s:7:"success";b:1;' ) . '%' + '%' . $wpdb->esc_like( $suffix . '";a:4:{s:7:"success";b:1;' ) . '%' ) ); } diff --git a/inc/3rd-party/nextgen-gallery/inc/common/attachments.php b/inc/3rd-party/nextgen-gallery/inc/common/attachments.php index 183812061..f13b8cd7b 100644 --- a/inc/3rd-party/nextgen-gallery/inc/common/attachments.php +++ b/inc/3rd-party/nextgen-gallery/inc/common/attachments.php @@ -284,7 +284,8 @@ function imagify_ngg_cleanup_after_media_deletion( $image_id, $image ) { * The backup file has already been deleted by NGG. * Delete the WebP versions and the optimization data. */ - $process->delete_webp_files(); + $process->delete_nextgen_files(); + $process->get_data()->delete_optimization_data(); } diff --git a/inc/3rd-party/regenerate-thumbnails/classes/Main.php b/inc/3rd-party/regenerate-thumbnails/classes/Main.php index 0a30d59af..41f2b4878 100644 --- a/inc/3rd-party/regenerate-thumbnails/classes/Main.php +++ b/inc/3rd-party/regenerate-thumbnails/classes/Main.php @@ -164,7 +164,7 @@ public function launch_async_optimization( $metadata, $media_id ) { if ( ! empty( $optimization_data['sizes'] ) ) { foreach ( $optimization_data['sizes'] as $size_name => $size_data ) { - $non_webp_size_name = $process->is_size_webp( $size_name ); + $non_webp_size_name = $process->is_size_next_gen( $size_name ); if ( ! $non_webp_size_name || ! isset( $sizes[ $non_webp_size_name ] ) ) { continue; diff --git a/inc/Dependencies/ActionScheduler/action-scheduler.php b/inc/Dependencies/ActionScheduler/action-scheduler.php index b950a70b2..cfa1f1c4a 100644 --- a/inc/Dependencies/ActionScheduler/action-scheduler.php +++ b/inc/Dependencies/ActionScheduler/action-scheduler.php @@ -5,8 +5,11 @@ * Description: A robust scheduling library for use in WordPress plugins. * Author: Automattic * Author URI: https://automattic.com/ - * Version: 3.5.4 + * Version: 3.7.1 * License: GPLv3 + * Requires at least: 6.2 + * Tested up to: 6.4 + * Requires PHP: 5.6 * * Copyright 2019 Automattic, Inc. (https://automattic.com/contact/) * @@ -26,27 +29,29 @@ * @package ActionScheduler */ -if ( ! function_exists( 'action_scheduler_register_3_dot_5_dot_4' ) && function_exists( 'add_action' ) ) { // WRCS: DEFINED_VERSION. +if ( ! function_exists( 'action_scheduler_register_3_dot_7_dot_1' ) && function_exists( 'add_action' ) ) { // WRCS: DEFINED_VERSION. if ( ! class_exists( 'ActionScheduler_Versions', false ) ) { require_once __DIR__ . '/classes/ActionScheduler_Versions.php'; add_action( 'plugins_loaded', array( 'ActionScheduler_Versions', 'initialize_latest_version' ), 1, 0 ); } - add_action( 'plugins_loaded', 'action_scheduler_register_3_dot_5_dot_4', 0, 0 ); // WRCS: DEFINED_VERSION. + add_action( 'plugins_loaded', 'action_scheduler_register_3_dot_7_dot_1', 0, 0 ); // WRCS: DEFINED_VERSION. + // phpcs:disable Generic.Functions.OpeningFunctionBraceKernighanRitchie.ContentAfterBrace /** * Registers this version of Action Scheduler. */ - function action_scheduler_register_3_dot_5_dot_4() { // WRCS: DEFINED_VERSION. + function action_scheduler_register_3_dot_7_dot_1() { // WRCS: DEFINED_VERSION. $versions = ActionScheduler_Versions::instance(); - $versions->register( '3.5.4', 'action_scheduler_initialize_3_dot_5_dot_4' ); // WRCS: DEFINED_VERSION. + $versions->register( '3.7.1', 'action_scheduler_initialize_3_dot_7_dot_1' ); // WRCS: DEFINED_VERSION. } + // phpcs:disable Generic.Functions.OpeningFunctionBraceKernighanRitchie.ContentAfterBrace /** * Initializes this version of Action Scheduler. */ - function action_scheduler_initialize_3_dot_5_dot_4() { // WRCS: DEFINED_VERSION. + function action_scheduler_initialize_3_dot_7_dot_1() { // WRCS: DEFINED_VERSION. // A final safety check is required even here, because historic versions of Action Scheduler // followed a different pattern (in some unusual cases, we could reach this point and the // ActionScheduler class is already defined—so we need to guard against that). @@ -58,7 +63,7 @@ function action_scheduler_initialize_3_dot_5_dot_4() { // WRCS: DEFINED_VERSION. // Support usage in themes - load this version if no plugin has loaded a version yet. if ( did_action( 'plugins_loaded' ) && ! doing_action( 'plugins_loaded' ) && ! class_exists( 'ActionScheduler', false ) ) { - action_scheduler_initialize_3_dot_5_dot_4(); // WRCS: DEFINED_VERSION. + action_scheduler_initialize_3_dot_7_dot_1(); // WRCS: DEFINED_VERSION. do_action( 'action_scheduler_pre_theme_init' ); ActionScheduler_Versions::initialize_latest_version(); } diff --git a/inc/Dependencies/ActionScheduler/changelog.txt b/inc/Dependencies/ActionScheduler/changelog.txt index 69aef1ff7..368a94f61 100644 --- a/inc/Dependencies/ActionScheduler/changelog.txt +++ b/inc/Dependencies/ActionScheduler/changelog.txt @@ -1,5 +1,65 @@ *** Changelog *** += 3.7.1 - 2023-12-13 = +* Release/3.7.0. +* Tweak - WP 6.4 compatibility. +* update semver to 5.7.2 because of a security vulnerability in 5.7.1. + += 3.7.0 - 2023-11-20 = +* Important: starting with this release, Action Scheduler follows an L-2 version policy (WordPress, and consequently PHP). +* Add extended indexes for hook_status_scheduled_date_gmt and status_sheduled_date_gmt. +* Catch and log exceptions thrown when actions can't be created, e.g. under a corrupt database schema. +* Release/3.6.4. +* Tweak - WP 6.4 compatibility. +* Update unit tests for upcoming dependency version policy. +* make sure hook action_scheduler_failed_execution can access original exception object. +* mention dependency version policy in usage.md. + += 3.6.4 - 2023-10-11 = +* Performance improvements when bulk cancelling actions. +* Dev-related fixes. + += 3.6.3 - 2023-09-13 = +* Use `_doing_it_wrong` in initialization check. + += 3.6.2 - 2023-08-09 = +* Add guidance about passing arguments. +* Atomic option locking. +* Improve bulk delete handling. +* Include database error in the exception message. +* Tweak - WP 6.3 compatibility. + += 3.6.1 - 2023-06-14 = +* Document new optional `$priority` arg for various API functions. +* Document the new `--exclude-groups` WP CLI option. +* Document the new `action_scheduler_init` hook. +* Ensure actions within each claim are executed in the expected order. +* Fix incorrect text domain. +* Remove SHOW TABLES usage when checking if tables exist. + += 3.6.0 - 2023-05-10 = +* Add $unique parameter to function signatures. +* Add a cast-to-int for extra safety before forming new DateTime object. +* Add a hook allowing exceptions for consistently failing recurring actions. +* Add action priorities. +* Add init hook. +* Always raise the time limit. +* Bump minimatch from 3.0.4 to 3.0.8. +* Bump yaml from 2.2.1 to 2.2.2. +* Defensive coding relating to gaps in declared schedule types. +* Do not process an action if it cannot be set to `in-progress`. +* Filter view labels (status names) should be translatable | #919. +* Fix WPCLI progress messages. +* Improve data-store initialization flow. +* Improve error handling across all supported PHP versions. +* Improve logic for flushing the runtime cache. +* Support exclusion of multiple groups. +* Update lint-staged and Node/NPM requirements. +* add CLI clean command. +* add CLI exclude-group filter. +* exclude past-due from list table all filter count. +* throwing an exception if as_schedule_recurring_action interval param is not of type integer. + = 3.5.4 - 2023-01-17 = * Add pre filters during action registration. * Async scheduling. diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_ActionFactory.php b/inc/Dependencies/ActionScheduler/classes/ActionScheduler_ActionFactory.php index 8e2e65018..07bf01b35 100644 --- a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_ActionFactory.php +++ b/inc/Dependencies/ActionScheduler/classes/ActionScheduler_ActionFactory.php @@ -13,10 +13,15 @@ class ActionScheduler_ActionFactory { * @param array $args Args to pass to callbacks when the hook is triggered. * @param ActionScheduler_Schedule $schedule The action's schedule. * @param string $group A group to put the action in. + * phpcs:ignore Squiz.Commenting.FunctionComment.ExtraParamComment + * @param int $priority The action priority. * * @return ActionScheduler_Action An instance of the stored action. */ public function get_stored_action( $status, $hook, array $args = array(), ActionScheduler_Schedule $schedule = null, $group = '' ) { + // The 6th parameter ($priority) is not formally declared in the method signature to maintain compatibility with + // third-party subclasses created before this param was added. + $priority = func_num_args() >= 6 ? (int) func_get_arg( 5 ) : 10; switch ( $status ) { case ActionScheduler_Store::STATUS_PENDING: @@ -36,17 +41,19 @@ public function get_stored_action( $status, $hook, array $args = array(), Action $action_class = apply_filters( 'action_scheduler_stored_action_class', $action_class, $status, $hook, $args, $schedule, $group ); $action = new $action_class( $hook, $args, $schedule, $group ); + $action->set_priority( $priority ); /** * Allow 3rd party code to change the instantiated action for a given hook, args, schedule and group. * - * @param ActionScheduler_Action $action The instantiated action. - * @param string $hook The instantiated action's hook. - * @param array $args The instantiated action's args. + * @param ActionScheduler_Action $action The instantiated action. + * @param string $hook The instantiated action's hook. + * @param array $args The instantiated action's args. * @param ActionScheduler_Schedule $schedule The instantiated action's schedule. - * @param string $group The instantiated action's group. + * @param string $group The instantiated action's group. + * @param int $priority The action priority. */ - return apply_filters( 'action_scheduler_stored_action_instance', $action, $hook, $args, $schedule, $group ); + return apply_filters( 'action_scheduler_stored_action_instance', $action, $hook, $args, $schedule, $group, $priority ); } /** @@ -229,9 +236,100 @@ public function repeat( $action ) { $schedule_class = get_class( $schedule ); $new_schedule = new $schedule( $next, $schedule->get_recurrence(), $schedule->get_first_date() ); $new_action = new ActionScheduler_Action( $action->get_hook(), $action->get_args(), $new_schedule, $action->get_group() ); + $new_action->set_priority( $action->get_priority() ); return $this->store( $new_action ); } + /** + * Creates a scheduled action. + * + * This general purpose method can be used in place of specific methods such as async(), + * async_unique(), single() or single_unique(), etc. + * + * @internal Not intended for public use, should not be overriden by subclasses. + * + * @param array $options { + * Describes the action we wish to schedule. + * + * @type string $type Must be one of 'async', 'cron', 'recurring', or 'single'. + * @type string $hook The hook to be executed. + * @type array $arguments Arguments to be passed to the callback. + * @type string $group The action group. + * @type bool $unique If the action should be unique. + * @type int $when Timestamp. Indicates when the action, or first instance of the action in the case + * of recurring or cron actions, becomes due. + * @type int|string $pattern Recurrence pattern. This is either an interval in seconds for recurring actions + * or a cron expression for cron actions. + * @type int $priority Lower values means higher priority. Should be in the range 0-255. + * } + * + * @return int The action ID. Zero if there was an error scheduling the action. + */ + public function create( array $options = array() ) { + $defaults = array( + 'type' => 'single', + 'hook' => '', + 'arguments' => array(), + 'group' => '', + 'unique' => false, + 'when' => time(), + 'pattern' => null, + 'priority' => 10, + ); + + $options = array_merge( $defaults, $options ); + + // Cron/recurring actions without a pattern are treated as single actions (this gives calling code the ability + // to use functions like as_schedule_recurring_action() to schedule recurring as well as single actions). + if ( ( 'cron' === $options['type'] || 'recurring' === $options['type'] ) && empty( $options['pattern'] ) ) { + $options['type'] = 'single'; + } + + switch ( $options['type'] ) { + case 'async': + $schedule = new ActionScheduler_NullSchedule(); + break; + + case 'cron': + $date = as_get_datetime_object( $options['when'] ); + $cron = CronExpression::factory( $options['pattern'] ); + $schedule = new ActionScheduler_CronSchedule( $date, $cron ); + break; + + case 'recurring': + $date = as_get_datetime_object( $options['when'] ); + $schedule = new ActionScheduler_IntervalSchedule( $date, $options['pattern'] ); + break; + + case 'single': + $date = as_get_datetime_object( $options['when'] ); + $schedule = new ActionScheduler_SimpleSchedule( $date ); + break; + + default: + error_log( "Unknown action type '{$options['type']}' specified when trying to create an action for '{$options['hook']}'." ); + return 0; + } + + $action = new ActionScheduler_Action( $options['hook'], $options['arguments'], $schedule, $options['group'] ); + $action->set_priority( $options['priority'] ); + + $action_id = 0; + try { + $action_id = $options['unique'] ? $this->store_unique_action( $action ) : $this->store( $action ); + } catch ( Exception $e ) { + error_log( + sprintf( + /* translators: %1$s is the name of the hook to be enqueued, %2$s is the exception message. */ + __( 'Caught exception while enqueuing action "%1$s": %2$s', 'action-scheduler' ), + $options['hook'], + $e->getMessage() + ) + ); + } + return $action_id; + } + /** * Save action to database. * diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_Compatibility.php b/inc/Dependencies/ActionScheduler/classes/ActionScheduler_Compatibility.php index 85e0ed9da..bb28023bc 100644 --- a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_Compatibility.php +++ b/inc/Dependencies/ActionScheduler/classes/ActionScheduler_Compatibility.php @@ -4,7 +4,6 @@ * Class ActionScheduler_Compatibility */ class ActionScheduler_Compatibility { - /** * Converts a shorthand byte value to an integer byte value. * @@ -89,21 +88,18 @@ public static function raise_time_limit( $limit = 0 ) { $limit = (int) $limit; $max_execution_time = (int) ini_get( 'max_execution_time' ); - /* - * If the max execution time is already unlimited (zero), or if it exceeds or is equal to the proposed - * limit, there is no reason for us to make further changes (we never want to lower it). - */ - if ( - 0 === $max_execution_time - || ( $max_execution_time >= $limit && $limit !== 0 ) - ) { + // If the max execution time is already set to zero (unlimited), there is no reason to make a further change. + if ( 0 === $max_execution_time ) { return; } + // Whichever of $max_execution_time or $limit is higher is the amount by which we raise the time limit. + $raise_by = 0 === $limit || $limit > $max_execution_time ? $limit : $max_execution_time; + if ( function_exists( 'wc_set_time_limit' ) ) { - wc_set_time_limit( $limit ); + wc_set_time_limit( $raise_by ); } elseif ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) { // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved - @set_time_limit( $limit ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + @set_time_limit( $raise_by ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged } } } diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_ListTable.php b/inc/Dependencies/ActionScheduler/classes/ActionScheduler_ListTable.php index 9e631f754..a21fdbe37 100644 --- a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_ListTable.php +++ b/inc/Dependencies/ActionScheduler/classes/ActionScheduler_ListTable.php @@ -252,7 +252,7 @@ private static function human_interval( $interval, $periods_to_include = 2 ) { */ protected function get_recurrence( $action ) { $schedule = $action->get_schedule(); - if ( $schedule->is_recurring() ) { + if ( $schedule->is_recurring() && method_exists( $schedule, 'get_recurrence' ) ) { $recurrence = $schedule->get_recurrence(); if ( is_numeric( $recurrence ) ) { @@ -471,7 +471,7 @@ protected function get_schedule_display_string( ActionScheduler_Schedule $schedu return __( 'async', 'action-scheduler' ); } - if ( ! $schedule->get_date() ) { + if ( ! method_exists( $schedule, 'get_date' ) || ! $schedule->get_date() ) { return '0000-00-00 00:00:00'; } @@ -502,7 +502,20 @@ protected function get_schedule_display_string( ActionScheduler_Schedule $schedu */ protected function bulk_delete( array $ids, $ids_sql ) { foreach ( $ids as $id ) { - $this->store->delete_action( $id ); + try { + $this->store->delete_action( $id ); + } catch ( Exception $e ) { + // A possible reason for an exception would include a scenario where the same action is deleted by a + // concurrent request. + error_log( + sprintf( + /* translators: 1: action ID 2: exception message. */ + __( 'Action Scheduler was unable to delete action %1$d. Reason: %2$s', 'action-scheduler' ), + $id, + $e->getMessage() + ) + ); + } } } diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_OptionLock.php b/inc/Dependencies/ActionScheduler/classes/ActionScheduler_OptionLock.php index 4bc9a3fc2..911f9b77c 100644 --- a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_OptionLock.php +++ b/inc/Dependencies/ActionScheduler/classes/ActionScheduler_OptionLock.php @@ -24,7 +24,37 @@ class ActionScheduler_OptionLock extends ActionScheduler_Lock { * @bool True if lock value has changed, false if not or if set failed. */ public function set( $lock_type ) { - return update_option( $this->get_key( $lock_type ), time() + $this->get_duration( $lock_type ) ); + global $wpdb; + + $lock_key = $this->get_key( $lock_type ); + $existing_lock_value = $this->get_existing_lock( $lock_type ); + $new_lock_value = $this->new_lock_value( $lock_type ); + + // The lock may not exist yet, or may have been deleted. + if ( empty( $existing_lock_value ) ) { + return (bool) $wpdb->insert( + $wpdb->options, + array( + 'option_name' => $lock_key, + 'option_value' => $new_lock_value, + 'autoload' => 'no', + ) + ); + } + + if ( $this->get_expiration_from( $existing_lock_value ) >= time() ) { + return false; + } + + // Otherwise, try to obtain the lock. + return (bool) $wpdb->update( + $wpdb->options, + array( 'option_value' => $new_lock_value ), + array( + 'option_name' => $lock_key, + 'option_value' => $existing_lock_value, + ) + ); } /** @@ -34,7 +64,30 @@ public function set( $lock_type ) { * @return bool|int False if no lock is set, otherwise the timestamp for when the lock is set to expire. */ public function get_expiration( $lock_type ) { - return get_option( $this->get_key( $lock_type ) ); + return $this->get_expiration_from( $this->get_existing_lock( $lock_type ) ); + } + + /** + * Given the lock string, derives the lock expiration timestamp (or false if it cannot be determined). + * + * @param string $lock_value String containing a timestamp, or pipe-separated combination of unique value and timestamp. + * + * @return false|int + */ + private function get_expiration_from( $lock_value ) { + $lock_string = explode( '|', $lock_value ); + + // Old style lock? + if ( count( $lock_string ) === 1 && is_numeric( $lock_string[0] ) ) { + return (int) $lock_string[0]; + } + + // New style lock? + if ( count( $lock_string ) === 2 && is_numeric( $lock_string[1] ) ) { + return (int) $lock_string[1]; + } + + return false; } /** @@ -46,4 +99,37 @@ public function get_expiration( $lock_type ) { protected function get_key( $lock_type ) { return sprintf( 'action_scheduler_lock_%s', $lock_type ); } + + /** + * Supplies the existing lock value, or an empty string if not set. + * + * @param string $lock_type A string to identify different lock types. + * + * @return string + */ + private function get_existing_lock( $lock_type ) { + global $wpdb; + + // Now grab the existing lock value, if there is one. + return (string) $wpdb->get_var( + $wpdb->prepare( + "SELECT option_value FROM $wpdb->options WHERE option_name = %s", + $this->get_key( $lock_type ) + ) + ); + } + + /** + * Supplies a lock value consisting of a unique value and the current timestamp, which are separated by a pipe + * character. + * + * Example: (string) "649de012e6b262.09774912|1688068114" + * + * @param string $lock_type A string to identify different lock types. + * + * @return string + */ + private function new_lock_value( $lock_type ) { + return uniqid( '', true ) . '|' . ( time() + $this->get_duration( $lock_type ) ); + } } diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_QueueCleaner.php b/inc/Dependencies/ActionScheduler/classes/ActionScheduler_QueueCleaner.php index 49cd44bb2..6f2a696d2 100644 --- a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_QueueCleaner.php +++ b/inc/Dependencies/ActionScheduler/classes/ActionScheduler_QueueCleaner.php @@ -18,6 +18,14 @@ class ActionScheduler_QueueCleaner { */ private $month_in_seconds = 2678400; + /** + * @var string[] Default list of statuses purged by the cleaner process. + */ + private $default_statuses_to_purge = [ + ActionScheduler_Store::STATUS_COMPLETE, + ActionScheduler_Store::STATUS_CANCELED, + ]; + /** * ActionScheduler_QueueCleaner constructor. * @@ -29,46 +37,113 @@ public function __construct( ActionScheduler_Store $store = null, $batch_size = $this->batch_size = $batch_size; } + /** + * Default queue cleaner process used by queue runner. + * + * @return array + */ public function delete_old_actions() { + /** + * Filter the minimum scheduled date age for action deletion. + * + * @param int $retention_period Minimum scheduled age in seconds of the actions to be deleted. + */ $lifespan = apply_filters( 'action_scheduler_retention_period', $this->month_in_seconds ); - $cutoff = as_get_datetime_object($lifespan.' seconds ago'); - $statuses_to_purge = array( - ActionScheduler_Store::STATUS_COMPLETE, - ActionScheduler_Store::STATUS_CANCELED, - ); + try { + $cutoff = as_get_datetime_object( $lifespan . ' seconds ago' ); + } catch ( Exception $e ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* Translators: %s is the exception message. */ + esc_html__( 'It was not possible to determine a valid cut-off time: %s.', 'action-scheduler' ), + esc_html( $e->getMessage() ) + ), + '3.5.5' + ); + + return array(); + } + + + /** + * Filter the statuses when cleaning the queue. + * + * @param string[] $default_statuses_to_purge Action statuses to clean. + */ + $statuses_to_purge = (array) apply_filters( 'action_scheduler_default_cleaner_statuses', $this->default_statuses_to_purge ); + + return $this->clean_actions( $statuses_to_purge, $cutoff, $this->get_batch_size() ); + } + + /** + * Delete selected actions limited by status and date. + * + * @param string[] $statuses_to_purge List of action statuses to purge. Defaults to canceled, complete. + * @param DateTime $cutoff_date Date limit for selecting actions. Defaults to 31 days ago. + * @param int|null $batch_size Maximum number of actions per status to delete. Defaults to 20. + * @param string $context Calling process context. Defaults to `old`. + * @return array Actions deleted. + */ + public function clean_actions( array $statuses_to_purge, DateTime $cutoff_date, $batch_size = null, $context = 'old' ) { + $batch_size = $batch_size !== null ? $batch_size : $this->batch_size; + $cutoff = $cutoff_date !== null ? $cutoff_date : as_get_datetime_object( $this->month_in_seconds . ' seconds ago' ); + $lifespan = time() - $cutoff->getTimestamp(); + if ( empty( $statuses_to_purge ) ) { + $statuses_to_purge = $this->default_statuses_to_purge; + } + $deleted_actions = []; foreach ( $statuses_to_purge as $status ) { $actions_to_delete = $this->store->query_actions( array( 'status' => $status, 'modified' => $cutoff, 'modified_compare' => '<=', - 'per_page' => $this->get_batch_size(), + 'per_page' => $batch_size, 'orderby' => 'none', ) ); - foreach ( $actions_to_delete as $action_id ) { - try { - $this->store->delete_action( $action_id ); - } catch ( Exception $e ) { - - /** - * Notify 3rd party code of exceptions when deleting a completed action older than the retention period - * - * This hook provides a way for 3rd party code to log or otherwise handle exceptions relating to their - * actions. - * - * @since 2.0.0 - * - * @param int $action_id The scheduled actions ID in the data store - * @param Exception $e The exception thrown when attempting to delete the action from the data store - * @param int $lifespan The retention period, in seconds, for old actions - * @param int $count_of_actions_to_delete The number of old actions being deleted in this batch - */ - do_action( 'action_scheduler_failed_old_action_deletion', $action_id, $e, $lifespan, count( $actions_to_delete ) ); - } + $deleted_actions = array_merge( $deleted_actions, $this->delete_actions( $actions_to_delete, $lifespan, $context ) ); + } + + return $deleted_actions; + } + + /** + * @param int[] $actions_to_delete List of action IDs to delete. + * @param int $lifespan Minimum scheduled age in seconds of the actions being deleted. + * @param string $context Context of the delete request. + * @return array Deleted action IDs. + */ + private function delete_actions( array $actions_to_delete, $lifespan = null, $context = 'old' ) { + $deleted_actions = []; + if ( $lifespan === null ) { + $lifespan = $this->month_in_seconds; + } + + foreach ( $actions_to_delete as $action_id ) { + try { + $this->store->delete_action( $action_id ); + $deleted_actions[] = $action_id; + } catch ( Exception $e ) { + /** + * Notify 3rd party code of exceptions when deleting a completed action older than the retention period + * + * This hook provides a way for 3rd party code to log or otherwise handle exceptions relating to their + * actions. + * + * @param int $action_id The scheduled actions ID in the data store + * @param Exception $e The exception thrown when attempting to delete the action from the data store + * @param int $lifespan The retention period, in seconds, for old actions + * @param int $count_of_actions_to_delete The number of old actions being deleted in this batch + * @since 2.0.0 + * + */ + do_action( "action_scheduler_failed_{$context}_action_deletion", $action_id, $e, $lifespan, count( $actions_to_delete ) ); } } + return $deleted_actions; } /** diff --git a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_QueueRunner.php b/inc/Dependencies/ActionScheduler/classes/ActionScheduler_QueueRunner.php index b890dca13..1ec3eab2a 100644 --- a/inc/Dependencies/ActionScheduler/classes/ActionScheduler_QueueRunner.php +++ b/inc/Dependencies/ActionScheduler/classes/ActionScheduler_QueueRunner.php @@ -103,9 +103,12 @@ public function unhook_dispatch_async_request() { * should dispatch a request to process pending actions. */ public function maybe_dispatch_async_request() { - if ( is_admin() && ! ActionScheduler::lock()->is_locked( 'async-request-runner' ) ) { - // Only start an async queue at most once every 60 seconds - ActionScheduler::lock()->set( 'async-request-runner' ); + // Only start an async queue at most once every 60 seconds. + if ( + is_admin() + && ! ActionScheduler::lock()->is_locked( 'async-request-runner' ) + && ActionScheduler::lock()->set( 'async-request-runner' ) + ) { $this->async_request->maybe_dispatch(); } } @@ -185,9 +188,15 @@ protected function do_batch( $size = 100, $context = '' ) { protected function clear_caches() { /* * Calling wp_cache_flush_runtime() lets us clear the runtime cache without invalidating the external object - * cache, so we will always prefer this when it is available (but it was only introduced in WordPress 6.0). + * cache, so we will always prefer this method (as compared to calling wp_cache_flush()) when it is available. + * + * However, this function was only introduced in WordPress 6.0. Additionally, the preferred way of detecting if + * it is supported changed in WordPress 6.1 so we use two different methods to decide if we should utilize it. */ - if ( function_exists( 'wp_cache_flush_runtime' ) ) { + $flushing_runtime_cache_explicitly_supported = function_exists( 'wp_cache_supports' ) && wp_cache_supports( 'flush_runtime' ); + $flushing_runtime_cache_implicitly_supported = ! function_exists( 'wp_cache_supports' ) && function_exists( 'wp_cache_flush_runtime' ); + + if ( $flushing_runtime_cache_explicitly_supported || $flushing_runtime_cache_implicitly_supported ) { wp_cache_flush_runtime(); } elseif ( ! wp_using_ext_object_cache() diff --git a/inc/Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Clean_Command.php b/inc/Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Clean_Command.php new file mode 100644 index 000000000..ff6e57aa3 --- /dev/null +++ b/inc/Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Clean_Command.php @@ -0,0 +1,125 @@ +] + * : The maximum number of actions to delete per batch. Defaults to 20. + * + * [--batches=] + * : Limit execution to a number of batches. Defaults to 0, meaning batches will continue all eligible actions are deleted. + * + * [--status=] + * : Only clean actions with the specified status. Defaults to Canceled, Completed. Define multiple statuses as a comma separated string (without spaces), e.g. `--status=complete,failed,canceled` + * + * [--before=] + * : Only delete actions with scheduled date older than this. Defaults to 31 days. e.g `--before='7 days ago'`, `--before='02-Feb-2020 20:20:20'` + * + * [--pause=] + * : The number of seconds to pause between batches. Default no pause. + * + * @param array $args Positional arguments. + * @param array $assoc_args Keyed arguments. + * @throws \WP_CLI\ExitException When an error occurs. + * + * @subcommand clean + */ + public function clean( $args, $assoc_args ) { + // Handle passed arguments. + $batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 20 ) ); + $batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) ); + $status = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'status', '' ) ); + $status = array_filter( array_map( 'trim', $status ) ); + $before = \WP_CLI\Utils\get_flag_value( $assoc_args, 'before', '' ); + $sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 ); + + $batches_completed = 0; + $actions_deleted = 0; + $unlimited = $batches === 0; + try { + $lifespan = as_get_datetime_object( $before ); + } catch ( Exception $e ) { + $lifespan = null; + } + + try { + // Custom queue cleaner instance. + $cleaner = new ActionScheduler_QueueCleaner( null, $batch ); + + // Clean actions for as long as possible. + while ( $unlimited || $batches_completed < $batches ) { + if ( $sleep && $batches_completed > 0 ) { + sleep( $sleep ); + } + + $deleted = count( $cleaner->clean_actions( $status, $lifespan, null,'CLI' ) ); + if ( $deleted <= 0 ) { + break; + } + $actions_deleted += $deleted; + $batches_completed++; + $this->print_success( $deleted ); + } + } catch ( Exception $e ) { + $this->print_error( $e ); + } + + $this->print_total_batches( $batches_completed ); + if ( $batches_completed > 1 ) { + $this->print_success( $actions_deleted ); + } + } + + /** + * Print WP CLI message about how many batches of actions were processed. + * + * @param int $batches_processed + */ + protected function print_total_batches( int $batches_processed ) { + WP_CLI::log( + sprintf( + /* translators: %d refers to the total number of batches processed */ + _n( '%d batch processed.', '%d batches processed.', $batches_processed, 'action-scheduler' ), + $batches_processed + ) + ); + } + + /** + * Convert an exception into a WP CLI error. + * + * @param Exception $e The error object. + * + * @throws \WP_CLI\ExitException + */ + protected function print_error( Exception $e ) { + WP_CLI::error( + sprintf( + /* translators: %s refers to the exception error message */ + __( 'There was an error deleting an action: %s', 'action-scheduler' ), + $e->getMessage() + ) + ); + } + + /** + * Print a success message with the number of completed actions. + * + * @param int $actions_deleted + */ + protected function print_success( int $actions_deleted ) { + WP_CLI::success( + sprintf( + /* translators: %d refers to the total number of actions deleted */ + _n( '%d action deleted.', '%d actions deleted.', $actions_deleted, 'action-scheduler' ), + $actions_deleted + ) + ); + } +} diff --git a/inc/Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php b/inc/Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php index c33de6867..4681daa49 100644 --- a/inc/Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php +++ b/inc/Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php @@ -90,7 +90,7 @@ protected function setup_progress_bar() { $count = count( $this->actions ); $this->progress_bar = new ProgressBar( /* translators: %d: amount of actions */ - sprintf( _n( 'Running %d action', 'Running %d actions', $count, 'action-scheduler' ), number_format_i18n( $count ) ), + sprintf( _n( 'Running %d action', 'Running %d actions', $count, 'action-scheduler' ), $count ), $count ); } diff --git a/inc/Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php b/inc/Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php index 70b052e58..2c68a3860 100644 --- a/inc/Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php +++ b/inc/Dependencies/ActionScheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php @@ -55,6 +55,9 @@ public function fix_schema( $args, $assoc_args ) { * [--group=] * : Only run actions from the specified group. Omitting this option runs actions from all groups. * + * [--exclude-groups=] + * : Run actions from all groups except the specified group(s). Define multiple groups as a comma separated string (without spaces), e.g. '--group_a,group_b'. This option is ignored when `--group` is used. + * * [--free-memory-on=] * : The number of actions to process between freeing memory. 0 disables freeing memory. Default 50. * @@ -72,15 +75,16 @@ public function fix_schema( $args, $assoc_args ) { */ public function run( $args, $assoc_args ) { // Handle passed arguments. - $batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 100 ) ); - $batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) ); - $clean = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'cleanup-batch-size', $batch ) ); - $hooks = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'hooks', '' ) ); - $hooks = array_filter( array_map( 'trim', $hooks ) ); - $group = \WP_CLI\Utils\get_flag_value( $assoc_args, 'group', '' ); - $free_on = \WP_CLI\Utils\get_flag_value( $assoc_args, 'free-memory-on', 50 ); - $sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 ); - $force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force', false ); + $batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 100 ) ); + $batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) ); + $clean = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'cleanup-batch-size', $batch ) ); + $hooks = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'hooks', '' ) ); + $hooks = array_filter( array_map( 'trim', $hooks ) ); + $group = \WP_CLI\Utils\get_flag_value( $assoc_args, 'group', '' ); + $exclude_groups = \WP_CLI\Utils\get_flag_value( $assoc_args, 'exclude-groups', '' ); + $free_on = \WP_CLI\Utils\get_flag_value( $assoc_args, 'free-memory-on', 50 ); + $sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 ); + $force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force', false ); ActionScheduler_DataController::set_free_ticks( $free_on ); ActionScheduler_DataController::set_sleep_time( $sleep ); @@ -88,6 +92,13 @@ public function run( $args, $assoc_args ) { $batches_completed = 0; $actions_completed = 0; $unlimited = $batches === 0; + if ( is_callable( [ ActionScheduler::store(), 'set_claim_filter' ] ) ) { + $exclude_groups = $this->parse_comma_separated_string( $exclude_groups ); + + if ( ! empty( $exclude_groups ) ) { + ActionScheduler::store()->set_claim_filter('exclude-groups', $exclude_groups ); + } + } try { // Custom queue cleaner instance. @@ -116,6 +127,17 @@ public function run( $args, $assoc_args ) { $this->print_success( $actions_completed ); } + /** + * Converts a string of comma-separated values into an array of those same values. + * + * @param string $string The string of one or more comma separated values. + * + * @return array + */ + private function parse_comma_separated_string( $string ): array { + return array_filter( str_getcsv( $string ) ); + } + /** * Print WP CLI message about how many actions are about to be processed. * @@ -126,9 +148,9 @@ public function run( $args, $assoc_args ) { protected function print_total_actions( $total ) { WP_CLI::log( sprintf( - /* translators: %d refers to how many scheduled taks were found to run */ + /* translators: %d refers to how many scheduled tasks were found to run */ _n( 'Found %d scheduled task', 'Found %d scheduled tasks', $total, 'action-scheduler' ), - number_format_i18n( $total ) + $total ) ); } @@ -145,7 +167,7 @@ protected function print_total_batches( $batches_completed ) { sprintf( /* translators: %d refers to the total number of batches executed */ _n( '%d batch executed.', '%d batches executed.', $batches_completed, 'action-scheduler' ), - number_format_i18n( $batches_completed ) + $batches_completed ) ); } @@ -179,9 +201,9 @@ protected function print_error( Exception $e ) { protected function print_success( $actions_completed ) { WP_CLI::success( sprintf( - /* translators: %d refers to the total number of taskes completed */ + /* translators: %d refers to the total number of tasks completed */ _n( '%d scheduled task completed.', '%d scheduled tasks completed.', $actions_completed, 'action-scheduler' ), - number_format_i18n( $actions_completed ) + $actions_completed ) ); } diff --git a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler.php b/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler.php index e8873f11e..0163f7072 100644 --- a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler.php +++ b/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler.php @@ -153,11 +153,41 @@ public static function init( $plugin_file ) { add_action( 'init', array( $store, 'init' ), 1, 0 ); add_action( 'init', array( $logger, 'init' ), 1, 0 ); add_action( 'init', array( $runner, 'init' ), 1, 0 ); + + add_action( + 'init', + /** + * Runs after the active store's init() method has been called. + * + * It would probably be preferable to have $store->init() (or it's parent method) set this itself, + * once it has initialized, however that would cause problems in cases where a custom data store is in + * use and it has not yet been updated to follow that same logic. + */ + function () { + self::$data_store_initialized = true; + + /** + * Fires when Action Scheduler is ready: it is safe to use the procedural API after this point. + * + * @since 3.5.5 + */ + do_action( 'action_scheduler_init' ); + }, + 1 + ); } else { $admin_view->init(); $store->init(); $logger->init(); $runner->init(); + self::$data_store_initialized = true; + + /** + * Fires when Action Scheduler is ready: it is safe to use the procedural API after this point. + * + * @since 3.5.5 + */ + do_action( 'action_scheduler_init' ); } if ( apply_filters( 'action_scheduler_load_deprecated_functions', true ) ) { @@ -166,14 +196,13 @@ public static function init( $plugin_file ) { if ( defined( 'WP_CLI' ) && WP_CLI ) { WP_CLI::add_command( 'action-scheduler', 'ActionScheduler_WPCLI_Scheduler_command' ); + WP_CLI::add_command( 'action-scheduler', 'ActionScheduler_WPCLI_Clean_Command' ); if ( ! ActionScheduler_DataController::is_migration_complete() && Controller::instance()->allow_migration() ) { $command = new Migration_Command(); $command->register(); } } - self::$data_store_initialized = true; - /** * Handle WP comment cleanup after migration. */ @@ -192,8 +221,12 @@ public static function init( $plugin_file ) { */ public static function is_initialized( $function_name = null ) { if ( ! self::$data_store_initialized && ! empty( $function_name ) ) { - $message = sprintf( __( '%s() was called before the Action Scheduler data store was initialized', 'action-scheduler' ), esc_attr( $function_name ) ); - error_log( $message, E_WARNING ); + $message = sprintf( + /* translators: %s function name. */ + __( '%s() was called before the Action Scheduler data store was initialized', 'action-scheduler' ), + esc_attr( $function_name ) + ); + _doing_it_wrong( $function_name, $message, '3.1.6' ); } return self::$data_store_initialized; diff --git a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php b/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php index ccc997f2f..8d1465fc1 100644 --- a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php +++ b/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php @@ -673,24 +673,34 @@ protected function display_filter_by_status() { // Helper to set 'all' filter when not set on status counts passed in. if ( ! isset( $this->status_counts['all'] ) ) { - $this->status_counts = array( 'all' => array_sum( $this->status_counts ) ) + $this->status_counts; + $all_count = array_sum( $this->status_counts ); + if ( isset( $this->status_counts['past-due'] ) ) { + $all_count -= $this->status_counts['past-due']; + } + $this->status_counts = array( 'all' => $all_count ) + $this->status_counts; } - foreach ( $this->status_counts as $status_name => $count ) { + // Translated status labels. + $status_labels = ActionScheduler_Store::instance()->get_status_labels(); + $status_labels['all'] = _x( 'All', 'status labels', 'action-scheduler' ); + $status_labels['past-due'] = _x( 'Past-due', 'status labels', 'action-scheduler' ); + + foreach ( $this->status_counts as $status_slug => $count ) { if ( 0 === $count ) { continue; } - if ( $status_name === $request_status || ( empty( $request_status ) && 'all' === $status_name ) ) { + if ( $status_slug === $request_status || ( empty( $request_status ) && 'all' === $status_slug ) ) { $status_list_item = '
  • %3$s (%4$d)
  • '; } else { $status_list_item = '
  • %3$s (%4$d)
  • '; } - $status_filter_url = ( 'all' === $status_name ) ? remove_query_arg( 'status' ) : add_query_arg( 'status', $status_name ); + $status_name = isset( $status_labels[ $status_slug ] ) ? $status_labels[ $status_slug ] : ucfirst( $status_slug ); + $status_filter_url = ( 'all' === $status_slug ) ? remove_query_arg( 'status' ) : add_query_arg( 'status', $status_slug ); $status_filter_url = remove_query_arg( array( 'paged', 's' ), $status_filter_url ); - $status_list_items[] = sprintf( $status_list_item, esc_attr( $status_name ), esc_url( $status_filter_url ), esc_html( ucfirst( $status_name ) ), absint( $count ) ); + $status_list_items[] = sprintf( $status_list_item, esc_attr( $status_slug ), esc_url( $status_filter_url ), esc_html( $status_name ), absint( $count ) ); } if ( $status_list_items ) { diff --git a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php b/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php index 3440f0016..673499fca 100644 --- a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php +++ b/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php @@ -48,30 +48,56 @@ public function __construct( ActionScheduler_Store $store = null, ActionSchedule * Generally, this should be capitalised and not localised as it's a proper noun. */ public function process_action( $action_id, $context = '' ) { - try { - $valid_action = false; - do_action( 'action_scheduler_before_execute', $action_id, $context ); + // Temporarily override the error handler while we process the current action. + set_error_handler( + /** + * Temporary error handler which can catch errors and convert them into exceptions. This faciliates more + * robust error handling across all supported PHP versions. + * + * @throws Exception + * + * @param int $type Error level expressed as an integer. + * @param string $message Error message. + */ + function ( $type, $message ) { + throw new Exception( $message ); + }, + E_USER_ERROR | E_RECOVERABLE_ERROR + ); - if ( ActionScheduler_Store::STATUS_PENDING !== $this->store->get_status( $action_id ) ) { - do_action( 'action_scheduler_execution_ignored', $action_id, $context ); - return; + /* + * The nested try/catch structure is required because we potentially need to convert thrown errors into + * exceptions (and an exception thrown from a catch block cannot be caught by a later catch block in the *same* + * structure). + */ + try { + try { + $valid_action = false; + do_action( 'action_scheduler_before_execute', $action_id, $context ); + + if ( ActionScheduler_Store::STATUS_PENDING !== $this->store->get_status( $action_id ) ) { + do_action( 'action_scheduler_execution_ignored', $action_id, $context ); + return; + } + + $valid_action = true; + do_action( 'action_scheduler_begin_execute', $action_id, $context ); + + $action = $this->store->fetch_action( $action_id ); + $this->store->log_execution( $action_id ); + $action->execute(); + do_action( 'action_scheduler_after_execute', $action_id, $action, $context ); + $this->store->mark_complete( $action_id ); + } catch ( Throwable $e ) { + // Throwable is defined when executing under PHP 7.0 and up. We convert it to an exception, for + // compatibility with ActionScheduler_Logger. + throw new Exception( $e->getMessage(), $e->getCode(), $e ); } - - $valid_action = true; - do_action( 'action_scheduler_begin_execute', $action_id, $context ); - - $action = $this->store->fetch_action( $action_id ); - $this->store->log_execution( $action_id ); - $action->execute(); - do_action( 'action_scheduler_after_execute', $action_id, $action, $context ); - $this->store->mark_complete( $action_id ); } catch ( Exception $e ) { - if ( $valid_action ) { - $this->store->mark_failure( $action_id ); - do_action( 'action_scheduler_failed_execution', $action_id, $e, $context ); - } else { - do_action( 'action_scheduler_failed_validation', $action_id, $e, $context ); - } + // This catch block exists for compatibility with PHP 5.6. + $this->handle_action_error( $action_id, $e, $context, $valid_action ); + } finally { + restore_error_handler(); } if ( isset( $action ) && is_a( $action, 'ActionScheduler_Action' ) && $action->get_schedule()->is_recurring() ) { @@ -79,6 +105,39 @@ public function process_action( $action_id, $context = '' ) { } } + /** + * Marks actions as either having failed execution or failed validation, as appropriate. + * + * @param int $action_id Action ID. + * @param Exception $e Exception instance. + * @param string $context Execution context. + * @param bool $valid_action If the action is valid. + * + * @return void + */ + private function handle_action_error( $action_id, $e, $context, $valid_action ) { + if ( $valid_action ) { + $this->store->mark_failure( $action_id ); + /** + * Runs when action execution fails. + * + * @param int $action_id Action ID. + * @param Exception $e Exception instance. + * @param string $context Execution context. + */ + do_action( 'action_scheduler_failed_execution', $action_id, $e, $context ); + } else { + /** + * Runs when action validation fails. + * + * @param int $action_id Action ID. + * @param Exception $e Exception instance. + * @param string $context Execution context. + */ + do_action( 'action_scheduler_failed_validation', $action_id, $e, $context ); + } + } + /** * Schedule the next instance of the action if necessary. * @@ -143,12 +202,22 @@ private function recurring_action_is_consistently_failing( ActionScheduler_Actio return false; } - // Now let's fetch the first action (having the same hook) of *any status*ithin the same window. + // Now let's fetch the first action (having the same hook) of *any status* within the same window. unset( $query_args['status'] ); $first_action_id_with_the_same_hook = $this->store->query_actions( $query_args ); - // If the IDs match, then actions for this hook must be consistently failing. - return $first_action_id_with_the_same_hook === $first_failing_action_id; + /** + * If a recurring action is assessed as consistently failing, it will not be rescheduled. This hook provides a + * way to observe and optionally override that assessment. + * + * @param bool $is_consistently_failing If the action is considered to be consistently failing. + * @param ActionScheduler_Action $action The action being assessed. + */ + return (bool) apply_filters( + 'action_scheduler_recurring_action_is_consistently_failing', + $first_action_id_with_the_same_hook === $first_failing_action_id, + $action + ); } /** diff --git a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php b/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php index 2334fda10..3fd259ea7 100644 --- a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php +++ b/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php @@ -25,7 +25,7 @@ abstract class ActionScheduler_Abstract_Schema { /** * @var array Names of tables that will be registered by this class. */ - protected $tables = []; + protected $tables = array(); /** * Can optionally be used by concrete classes to carry out additional initialization work @@ -90,10 +90,10 @@ private function schema_update_required() { $plugin_option_name = 'schema-'; switch ( static::class ) { - case 'ActionScheduler_StoreSchema' : + case 'ActionScheduler_StoreSchema': $plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Store_Table_Maker'; break; - case 'ActionScheduler_LoggerSchema' : + case 'ActionScheduler_LoggerSchema': $plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Logger_Table_Maker'; break; } @@ -129,7 +129,7 @@ private function mark_schema_update_complete() { * @return void */ private function update_table( $table ) { - require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $definition = $this->get_table_definition( $table ); if ( $definition ) { $updated = dbDelta( $definition ); @@ -148,7 +148,7 @@ private function update_table( $table ) { * table prefix for the current blog */ protected function get_full_table_name( $table ) { - return $GLOBALS[ 'wpdb' ]->prefix . $table; + return $GLOBALS['wpdb']->prefix . $table; } /** @@ -159,14 +159,19 @@ protected function get_full_table_name( $table ) { public function tables_exist() { global $wpdb; - $existing_tables = $wpdb->get_col( 'SHOW TABLES' ); - $expected_tables = array_map( - function ( $table_name ) use ( $wpdb ) { - return $wpdb->prefix . $table_name; - }, - $this->tables - ); + $tables_exist = true; - return count( array_intersect( $existing_tables, $expected_tables ) ) === count( $expected_tables ); + foreach ( $this->tables as $table_name ) { + $table_name = $wpdb->prefix . $table_name; + $pattern = str_replace( '_', '\\_', $table_name ); + $existing_table = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pattern ) ); + + if ( $existing_table !== $table_name ) { + $tables_exist = false; + break; + } + } + + return $tables_exist; } } diff --git a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Lock.php b/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Lock.php index 86e852851..e388a58fa 100644 --- a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Lock.php +++ b/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Lock.php @@ -26,6 +26,8 @@ public function is_locked( $lock_type ) { /** * Set a lock. * + * To prevent race conditions, implementations should avoid setting the lock if the lock is already held. + * * @param string $lock_type A string to identify different lock types. * @return bool */ diff --git a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Store.php b/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Store.php index a55529332..faaaa9ed3 100644 --- a/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Store.php +++ b/inc/Dependencies/ActionScheduler/classes/abstracts/ActionScheduler_Store.php @@ -347,7 +347,7 @@ public function cancel_actions_by_hook( $hook ) { 'hook' => $hook, 'status' => self::STATUS_PENDING, 'per_page' => 1000, - 'orderby' => 'action_id', + 'orderby' => 'none', ) ); @@ -372,7 +372,7 @@ public function cancel_actions_by_group( $group ) { 'group' => $group, 'status' => self::STATUS_PENDING, 'per_page' => 1000, - 'orderby' => 'action_id', + 'orderby' => 'none', ) ); diff --git a/inc/Dependencies/ActionScheduler/classes/actions/ActionScheduler_Action.php b/inc/Dependencies/ActionScheduler/classes/actions/ActionScheduler_Action.php index f538f506b..ddf33d5d9 100644 --- a/inc/Dependencies/ActionScheduler/classes/actions/ActionScheduler_Action.php +++ b/inc/Dependencies/ActionScheduler/classes/actions/ActionScheduler_Action.php @@ -10,6 +10,19 @@ class ActionScheduler_Action { protected $schedule = NULL; protected $group = ''; + /** + * Priorities are conceptually similar to those used for regular WordPress actions. + * Like those, a lower priority takes precedence over a higher priority and the default + * is 10. + * + * Unlike regular WordPress actions, the priority of a scheduled action is strictly an + * integer and should be kept within the bounds 0-255 (anything outside the bounds will + * be brought back into the acceptable range). + * + * @var int + */ + protected $priority = 10; + public function __construct( $hook, array $args = array(), ActionScheduler_Schedule $schedule = NULL, $group = '' ) { $schedule = empty( $schedule ) ? new ActionScheduler_NullSchedule() : $schedule; $this->set_hook($hook); @@ -93,4 +106,30 @@ public function get_group() { public function is_finished() { return FALSE; } + + /** + * Sets the priority of the action. + * + * @param int $priority Priority level (lower is higher priority). Should be in the range 0-255. + * + * @return void + */ + public function set_priority( $priority ) { + if ( $priority < 0 ) { + $priority = 0; + } elseif ( $priority > 255 ) { + $priority = 255; + } + + $this->priority = (int) $priority; + } + + /** + * Gets the action priority. + * + * @return int + */ + public function get_priority() { + return $this->priority; + } } diff --git a/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBLogger.php b/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBLogger.php index 37bfd0d44..d285c8d27 100644 --- a/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBLogger.php +++ b/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBLogger.php @@ -82,7 +82,7 @@ private function create_entry_from_db_record( $record ) { } /** - * Retrieve the an action's log entries from the database. + * Retrieve an action's log entries from the database. * * @param int $action_id Action ID. * diff --git a/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBStore.php b/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBStore.php index 5009454f7..602c3dd97 100644 --- a/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBStore.php +++ b/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_DBStore.php @@ -25,6 +25,13 @@ class ActionScheduler_DBStore extends ActionScheduler_Store { /** @var int */ protected static $max_index_length = 191; + /** @var array List of claim filters. */ + protected $claim_filters = [ + 'group' => '', + 'hooks' => '', + 'exclude-groups' => '', + ]; + /** * Initialize the data store * @@ -84,7 +91,8 @@ private function save_action_to_db( ActionScheduler_Action $action, DateTime $da 'scheduled_date_gmt' => $this->get_scheduled_date_string( $action, $date ), 'scheduled_date_local' => $this->get_scheduled_date_string_local( $action, $date ), 'schedule' => serialize( $action->get_schedule() ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize - 'group_id' => $this->get_group_id( $action->get_group() ), + 'group_id' => current( $this->get_group_ids( $action->get_group() ) ), + 'priority' => $action->get_priority(), ); $args = wp_json_encode( $action->get_args() ); @@ -172,6 +180,7 @@ private function build_where_clause_for_insert( $data, $table_name, $unique ) { ActionScheduler_Store::STATUS_RUNNING, ); $pending_status_placeholders = implode( ', ', array_fill( 0, count( $pending_statuses ), '%s' ) ); + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $pending_status_placeholders is hardcoded. $where_clause = $wpdb->prepare( " @@ -242,23 +251,35 @@ protected function get_args_for_query( $args ) { /** * Get a group's ID based on its name/slug. * - * @param string $slug The string name of a group. - * @param bool $create_if_not_exists Whether to create the group if it does not already exist. Default, true - create the group. + * @param string|array $slugs The string name of a group, or names for several groups. + * @param bool $create_if_not_exists Whether to create the group if it does not already exist. Default, true - create the group. * - * @return int The group's ID, if it exists or is created, or 0 if it does not exist and is not created. + * @return array The group IDs, if they exist or were successfully created. May be empty. */ - protected function get_group_id( $slug, $create_if_not_exists = true ) { - if ( empty( $slug ) ) { - return 0; + protected function get_group_ids( $slugs, $create_if_not_exists = true ) { + $slugs = (array) $slugs; + $group_ids = array(); + + if ( empty( $slugs ) ) { + return array(); } + /** @var \wpdb $wpdb */ global $wpdb; - $group_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT group_id FROM {$wpdb->actionscheduler_groups} WHERE slug=%s", $slug ) ); - if ( empty( $group_id ) && $create_if_not_exists ) { - $group_id = $this->create_group( $slug ); + + foreach ( $slugs as $slug ) { + $group_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT group_id FROM {$wpdb->actionscheduler_groups} WHERE slug=%s", $slug ) ); + + if ( empty( $group_id ) && $create_if_not_exists ) { + $group_id = $this->create_group( $slug ); + } + + if ( $group_id ) { + $group_ids[] = $group_id; + } } - return $group_id; + return $group_ids; } /** @@ -355,7 +376,7 @@ protected function make_action_from_db_record( $data ) { } $group = $data->group ? $data->group : ''; - return ActionScheduler::factory()->get_stored_action( $data->status, $data->hook, $args, $schedule, $group ); + return ActionScheduler::factory()->get_stored_action( $data->status, $data->hook, $args, $schedule, $group, $data->priority ); } /** @@ -684,7 +705,7 @@ protected function bulk_cancel_actions( $query_args ) { array( 'per_page' => 1000, 'status' => self::STATUS_PENDING, - 'orderby' => 'action_id', + 'orderby' => 'none', ) ); @@ -796,6 +817,33 @@ protected function generate_claim_id() { return $wpdb->insert_id; } + /** + * Set a claim filter. + * + * @param string $filter_name Claim filter name. + * @param mixed $filter_values Values to filter. + * @return void + */ + public function set_claim_filter( $filter_name, $filter_values ) { + if ( isset( $this->claim_filters[ $filter_name ] ) ) { + $this->claim_filters[ $filter_name ] = $filter_values; + } + } + + /** + * Get the claim filter value. + * + * @param string $filter_name Claim filter name. + * @return mixed + */ + public function get_claim_filter( $filter_name ) { + if ( isset( $this->claim_filters[ $filter_name ] ) ) { + return $this->claim_filters[ $filter_name ]; + } + + return ''; + } + /** * Mark actions claimed. * @@ -813,9 +861,8 @@ protected function claim_actions( $claim_id, $limit, \DateTime $before_date = nu /** @var \wpdb $wpdb */ global $wpdb; - $now = as_get_datetime_object(); - $date = is_null( $before_date ) ? $now : clone $before_date; - + $now = as_get_datetime_object(); + $date = is_null( $before_date ) ? $now : clone $before_date; // can't use $wpdb->update() because of the <= condition. $update = "UPDATE {$wpdb->actionscheduler_actions} SET claim_id=%d, last_attempt_gmt=%s, last_attempt_local=%s"; $params = array( @@ -824,6 +871,18 @@ protected function claim_actions( $claim_id, $limit, \DateTime $before_date = nu current_time( 'mysql' ), ); + // Set claim filters. + if ( ! empty( $hooks ) ) { + $this->set_claim_filter( 'hooks', $hooks ); + } else { + $hooks = $this->get_claim_filter( 'hooks' ); + } + if ( ! empty( $group ) ) { + $this->set_claim_filter( 'group', $group ); + } else { + $group = $this->get_claim_filter( 'group' ); + } + $where = 'WHERE claim_id = 0 AND scheduled_date_gmt <= %s AND status=%s'; $params[] = $date->format( 'Y-m-d H:i:s' ); $params[] = self::STATUS_PENDING; @@ -834,18 +893,33 @@ protected function claim_actions( $claim_id, $limit, \DateTime $before_date = nu $params = array_merge( $params, array_values( $hooks ) ); } - if ( ! empty( $group ) ) { - - $group_id = $this->get_group_id( $group, false ); + $group_operator = 'IN'; + if ( empty( $group ) ) { + $group = $this->get_claim_filter( 'exclude-groups' ); + $group_operator = 'NOT IN'; + } - // throw exception if no matching group found, this matches ActionScheduler_wpPostStore's behaviour. - if ( empty( $group_id ) ) { - /* translators: %s: group name */ - throw new InvalidArgumentException( sprintf( __( 'The group "%s" does not exist.', 'action-scheduler' ), $group ) ); + if ( ! empty( $group ) ) { + $group_ids = $this->get_group_ids( $group, false ); + + // throw exception if no matching group(s) found, this matches ActionScheduler_wpPostStore's behaviour. + if ( empty( $group_ids ) ) { + throw new InvalidArgumentException( + sprintf( + /* translators: %s: group name(s) */ + _n( + 'The group "%s" does not exist.', + 'The groups "%s" do not exist.', + is_array( $group ) ? count( $group ) : 1, + 'action-scheduler' + ), + $group + ) + ); } - $where .= ' AND group_id = %d'; - $params[] = $group_id; + $id_list = implode( ',', array_map( 'intval', $group_ids ) ); + $where .= " AND group_id {$group_operator} ( $id_list )"; } /** @@ -855,13 +929,23 @@ protected function claim_actions( $claim_id, $limit, \DateTime $before_date = nu * * @param string $order_by_sql */ - $order = apply_filters( 'action_scheduler_claim_actions_order_by', 'ORDER BY attempts ASC, scheduled_date_gmt ASC, action_id ASC' ); + $order = apply_filters( 'action_scheduler_claim_actions_order_by', 'ORDER BY priority ASC, attempts ASC, scheduled_date_gmt ASC, action_id ASC' ); $params[] = $limit; $sql = $wpdb->prepare( "{$update} {$where} {$order} LIMIT %d", $params ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders $rows_affected = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ( false === $rows_affected ) { - throw new \RuntimeException( __( 'Unable to claim actions. Database error.', 'action-scheduler' ) ); + $error = empty( $wpdb->last_error ) + ? _x( 'unknown', 'database error', 'action-scheduler' ) + : $wpdb->last_error; + + throw new \RuntimeException( + sprintf( + /* translators: %s database error. */ + __( 'Unable to claim actions. Database error: %s.', 'action-scheduler' ), + $error + ) + ); } return (int) $rows_affected; @@ -912,7 +996,7 @@ public function find_actions_by_claim_id( $claim_id ) { $cut_off = $before_date->format( 'Y-m-d H:i:s' ); $sql = $wpdb->prepare( - "SELECT action_id, scheduled_date_gmt FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d", + "SELECT action_id, scheduled_date_gmt FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d ORDER BY priority ASC, attempts ASC, scheduled_date_gmt ASC, action_id ASC", $claim_id ); @@ -955,7 +1039,7 @@ public function release_claim( ActionScheduler_ActionClaim $claim ) { if ( $row_updates < count( $action_ids ) ) { throw new RuntimeException( sprintf( - __( 'Unable to release actions from claim id %d.', 'woocommerce' ), + __( 'Unable to release actions from claim id %d.', 'action-scheduler' ), $claim->get_id() ) ); @@ -1005,6 +1089,8 @@ public function mark_failure( $action_id ) { /** * Add execution message to action log. * + * @throws Exception If the action status cannot be updated to self::STATUS_RUNNING ('in-progress'). + * * @param int $action_id Action ID. * * @return void @@ -1015,7 +1101,20 @@ public function log_execution( $action_id ) { $sql = "UPDATE {$wpdb->actionscheduler_actions} SET attempts = attempts+1, status=%s, last_attempt_gmt = %s, last_attempt_local = %s WHERE action_id = %d"; $sql = $wpdb->prepare( $sql, self::STATUS_RUNNING, current_time( 'mysql', true ), current_time( 'mysql' ), $action_id ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $status_updated = $wpdb->query( $sql ); + + if ( ! $status_updated ) { + throw new Exception( + sprintf( + /* translators: 1: action ID. 2: status slug. */ + __( 'Unable to update the status of action %1$d to %2$s.', 'action-scheduler' ), + $action_id, + self::STATUS_RUNNING + ) + ); + } } /** diff --git a/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore.php b/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore.php index 7883ca82b..7c6b06d1c 100644 --- a/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore.php +++ b/inc/Dependencies/ActionScheduler/classes/data-stores/ActionScheduler_wpPostStore.php @@ -936,6 +936,8 @@ private function get_post_column( $action_id, $column_name ) { /** * Log Execution. * + * @throws Exception If the action status cannot be updated to self::STATUS_RUNNING ('in-progress'). + * * @param string $action_id Action ID. */ public function log_execution( $action_id ) { @@ -947,7 +949,7 @@ public function log_execution( $action_id ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->query( + $status_updated = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET menu_order = menu_order+1, post_status=%s, post_modified_gmt = %s, post_modified = %s WHERE ID = %d AND post_type = %s", self::STATUS_RUNNING, @@ -957,6 +959,17 @@ public function log_execution( $action_id ) { self::POST_TYPE ) ); + + if ( ! $status_updated ) { + throw new Exception( + sprintf( + /* translators: 1: action ID. 2: status slug. */ + __( 'Unable to update the status of action %1$d to %2$s.', 'action-scheduler' ), + $action_id, + self::STATUS_RUNNING + ) + ); + } } /** diff --git a/inc/Dependencies/ActionScheduler/classes/migration/Runner.php b/inc/Dependencies/ActionScheduler/classes/migration/Runner.php index 867c5de68..2304a79ad 100644 --- a/inc/Dependencies/ActionScheduler/classes/migration/Runner.php +++ b/inc/Dependencies/ActionScheduler/classes/migration/Runner.php @@ -79,7 +79,7 @@ public function run( $batch_size = 10 ) { if ( $this->progress_bar ) { /* translators: %d: amount of actions */ - $this->progress_bar->set_message( sprintf( _n( 'Migrating %d action', 'Migrating %d actions', $batch_size, 'action-scheduler' ), number_format_i18n( $batch_size ) ) ); + $this->progress_bar->set_message( sprintf( _n( 'Migrating %d action', 'Migrating %d actions', $batch_size, 'action-scheduler' ), $batch_size ) ); $this->progress_bar->set_count( $batch_size ); } diff --git a/inc/Dependencies/ActionScheduler/classes/schema/ActionScheduler_StoreSchema.php b/inc/Dependencies/ActionScheduler/classes/schema/ActionScheduler_StoreSchema.php index d52f27f6f..a0bd8cb20 100644 --- a/inc/Dependencies/ActionScheduler/classes/schema/ActionScheduler_StoreSchema.php +++ b/inc/Dependencies/ActionScheduler/classes/schema/ActionScheduler_StoreSchema.php @@ -16,7 +16,7 @@ class ActionScheduler_StoreSchema extends ActionScheduler_Abstract_Schema { /** * @var int Increment this value to trigger a schema update. */ - protected $schema_version = 6; + protected $schema_version = 7; public function __construct() { $this->tables = [ @@ -38,6 +38,7 @@ protected function get_table_definition( $table ) { $table_name = $wpdb->$table; $charset_collate = $wpdb->get_charset_collate(); $max_index_length = 191; // @see wp_get_db_schema() + $hook_status_scheduled_date_gmt_max_index_length = $max_index_length - 20 - 8; // - status, - scheduled_date_gmt $default_date = self::DEFAULT_DATE; switch ( $table ) { @@ -49,6 +50,7 @@ protected function get_table_definition( $table ) { status varchar(20) NOT NULL, scheduled_date_gmt datetime NULL default '{$default_date}', scheduled_date_local datetime NULL default '{$default_date}', + priority tinyint unsigned NOT NULL default '10', args varchar($max_index_length), schedule longtext, group_id bigint(20) unsigned NOT NULL default '0', @@ -58,8 +60,8 @@ protected function get_table_definition( $table ) { claim_id bigint(20) unsigned NOT NULL default '0', extended_args varchar(8000) DEFAULT NULL, PRIMARY KEY (action_id), - KEY hook (hook($max_index_length)), - KEY status (status), + KEY hook_status_scheduled_date_gmt (hook($hook_status_scheduled_date_gmt_max_index_length), status, scheduled_date_gmt), + KEY status_scheduled_date_gmt (status, scheduled_date_gmt), KEY scheduled_date_gmt (scheduled_date_gmt), KEY args (args($max_index_length)), KEY group_id (group_id), diff --git a/inc/Dependencies/ActionScheduler/functions.php b/inc/Dependencies/ActionScheduler/functions.php index 09ef353d9..cf803da3f 100644 --- a/inc/Dependencies/ActionScheduler/functions.php +++ b/inc/Dependencies/ActionScheduler/functions.php @@ -12,10 +12,11 @@ * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @param bool $unique Whether the action should be unique. + * @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255. * - * @return int The action ID. + * @return int The action ID. Zero if there was an error scheduling the action. */ -function as_enqueue_async_action( $hook, $args = array(), $group = '', $unique = false ) { +function as_enqueue_async_action( $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } @@ -33,13 +34,23 @@ function as_enqueue_async_action( $hook, $args = array(), $group = '', $unique = * @param string $hook Action hook. * @param array $args Action arguments. * @param string $group Action group. + * @param int $priority Action priority. */ - $pre = apply_filters( 'pre_as_enqueue_async_action', null, $hook, $args, $group ); + $pre = apply_filters( 'pre_as_enqueue_async_action', null, $hook, $args, $group, $priority ); if ( null !== $pre ) { return is_int( $pre ) ? $pre : 0; } - return ActionScheduler::factory()->async_unique( $hook, $args, $group, $unique ); + return ActionScheduler::factory()->create( + array( + 'type' => 'async', + 'hook' => $hook, + 'arguments' => $args, + 'group' => $group, + 'unique' => $unique, + 'priority' => $priority, + ) + ); } /** @@ -50,10 +61,11 @@ function as_enqueue_async_action( $hook, $args = array(), $group = '', $unique = * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @param bool $unique Whether the action should be unique. + * @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255. * - * @return int The action ID. + * @return int The action ID. Zero if there was an error scheduling the action. */ -function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '', $unique = false ) { +function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } @@ -72,13 +84,24 @@ function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = * @param string $hook Action hook. * @param array $args Action arguments. * @param string $group Action group. + * @param int $priorities Action priority. */ - $pre = apply_filters( 'pre_as_schedule_single_action', null, $timestamp, $hook, $args, $group ); + $pre = apply_filters( 'pre_as_schedule_single_action', null, $timestamp, $hook, $args, $group, $priority ); if ( null !== $pre ) { return is_int( $pre ) ? $pre : 0; } - return ActionScheduler::factory()->single_unique( $hook, $args, $timestamp, $group, $unique ); + return ActionScheduler::factory()->create( + array( + 'type' => 'single', + 'hook' => $hook, + 'arguments' => $args, + 'when' => $timestamp, + 'group' => $group, + 'unique' => $unique, + 'priority' => $priority, + ) + ); } /** @@ -90,14 +113,34 @@ function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @param bool $unique Whether the action should be unique. + * @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255. * - * @return int The action ID. + * @return int The action ID. Zero if there was an error scheduling the action. */ -function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '', $unique = false ) { +function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } + $interval = (int) $interval_in_seconds; + + // We expect an integer and allow it to be passed using float and string types, but otherwise + // should reject unexpected values. + if ( ! is_numeric( $interval_in_seconds ) || $interval_in_seconds != $interval ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: 1: provided value 2: provided type. */ + esc_html__( 'An integer was expected but "%1$s" (%2$s) was received.', 'action-scheduler' ), + esc_html( $interval_in_seconds ), + esc_html( gettype( $interval_in_seconds ) ) + ), + '3.6.0' + ); + + return 0; + } + /** * Provides an opportunity to short-circuit the default process for enqueuing recurring * actions. @@ -113,13 +156,25 @@ function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, * @param string $hook Action hook. * @param array $args Action arguments. * @param string $group Action group. + * @param int $priority Action priority. */ - $pre = apply_filters( 'pre_as_schedule_recurring_action', null, $timestamp, $interval_in_seconds, $hook, $args, $group ); + $pre = apply_filters( 'pre_as_schedule_recurring_action', null, $timestamp, $interval_in_seconds, $hook, $args, $group, $priority ); if ( null !== $pre ) { return is_int( $pre ) ? $pre : 0; } - return ActionScheduler::factory()->recurring_unique( $hook, $args, $timestamp, $interval_in_seconds, $group, $unique ); + return ActionScheduler::factory()->create( + array( + 'type' => 'recurring', + 'hook' => $hook, + 'arguments' => $args, + 'when' => $timestamp, + 'pattern' => $interval_in_seconds, + 'group' => $group, + 'unique' => $unique, + 'priority' => $priority, + ) + ); } /** @@ -143,10 +198,11 @@ function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @param bool $unique Whether the action should be unique. + * @param int $priority Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255. * - * @return int The action ID. + * @return int The action ID. Zero if there was an error scheduling the action. */ -function as_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '', $unique = false ) { +function as_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '', $unique = false, $priority = 10 ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } @@ -166,13 +222,25 @@ function as_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), * @param string $hook Action hook. * @param array $args Action arguments. * @param string $group Action group. + * @param int $priority Action priority. */ - $pre = apply_filters( 'pre_as_schedule_cron_action', null, $timestamp, $schedule, $hook, $args, $group ); + $pre = apply_filters( 'pre_as_schedule_cron_action', null, $timestamp, $schedule, $hook, $args, $group, $priority ); if ( null !== $pre ) { return is_int( $pre ) ? $pre : 0; } - return ActionScheduler::factory()->cron_unique( $hook, $args, $timestamp, $schedule, $group, $unique ); + return ActionScheduler::factory()->create( + array( + 'type' => 'cron', + 'hook' => $hook, + 'arguments' => $args, + 'when' => $timestamp, + 'pattern' => $schedule, + 'group' => $group, + 'unique' => $unique, + 'priority' => $priority, + ) + ); } /** @@ -215,9 +283,10 @@ function as_unschedule_action( $hook, $args = array(), $group = '' ) { ActionScheduler::logger()->log( $action_id, sprintf( - /* translators: %s is the name of the hook to be cancelled. */ - __( 'Caught exception while cancelling action: %s', 'action-scheduler' ), - esc_attr( $hook ) + /* translators: %1$s is the name of the hook to be cancelled, %2$s is the exception message. */ + __( 'Caught exception while cancelling action "%1$s": %2$s', 'action-scheduler' ), + $hook, + $exception->getMessage() ) ); diff --git a/inc/Dependencies/ActionScheduler/readme.txt b/inc/Dependencies/ActionScheduler/readme.txt index 3518b1544..7e0cc7e77 100644 --- a/inc/Dependencies/ActionScheduler/readme.txt +++ b/inc/Dependencies/ActionScheduler/readme.txt @@ -1,10 +1,10 @@ === Action Scheduler === Contributors: Automattic, wpmuguru, claudiosanches, peterfabian1000, vedjain, jamosova, obliviousharmony, konamiman, sadowski, royho, barryhughes-1 Tags: scheduler, cron -Requires at least: 5.2 -Tested up to: 6.0 -Stable tag: 3.5.4 +Stable tag: 3.7.1 License: GPLv3 +Requires at least: 6.2 +Tested up to: 6.4 Requires PHP: 5.6 Action Scheduler - Job Queue for WordPress @@ -47,6 +47,66 @@ Collaboration is cool. We'd love to work with you to improve Action Scheduler. [ == Changelog == += 3.7.1 - 2023-12-13 = +* Release/3.7.0. +* Tweak - WP 6.4 compatibility. +* update semver to 5.7.2 because of a security vulnerability in 5.7.1. + += 3.7.0 - 2023-11-20 = +* Important: starting with this release, Action Scheduler follows an L-2 version policy (WordPress, and consequently PHP). +* Add extended indexes for hook_status_scheduled_date_gmt and status_sheduled_date_gmt. +* Catch and log exceptions thrown when actions can't be created, e.g. under a corrupt database schema. +* Release/3.6.4. +* Tweak - WP 6.4 compatibility. +* Update unit tests for upcoming dependency version policy. +* make sure hook action_scheduler_failed_execution can access original exception object. +* mention dependency version policy in usage.md. + += 3.6.4 - 2023-10-11 = +* Performance improvements when bulk cancelling actions. +* Dev-related fixes. + += 3.6.3 - 2023-09-13 = +* Use `_doing_it_wrong` in initialization check. + += 3.6.2 - 2023-08-09 = +* Add guidance about passing arguments. +* Atomic option locking. +* Improve bulk delete handling. +* Include database error in the exception message. +* Tweak - WP 6.3 compatibility. + += 3.6.1 - 2023-06-14 = +* Document new optional `$priority` arg for various API functions. +* Document the new `--exclude-groups` WP CLI option. +* Document the new `action_scheduler_init` hook. +* Ensure actions within each claim are executed in the expected order. +* Fix incorrect text domain. +* Remove SHOW TABLES usage when checking if tables exist. + += 3.6.0 - 2023-05-10 = +* Add $unique parameter to function signatures. +* Add a cast-to-int for extra safety before forming new DateTime object. +* Add a hook allowing exceptions for consistently failing recurring actions. +* Add action priorities. +* Add init hook. +* Always raise the time limit. +* Bump minimatch from 3.0.4 to 3.0.8. +* Bump yaml from 2.2.1 to 2.2.2. +* Defensive coding relating to gaps in declared schedule types. +* Do not process an action if it cannot be set to `in-progress`. +* Filter view labels (status names) should be translatable | #919. +* Fix WPCLI progress messages. +* Improve data-store initialization flow. +* Improve error handling across all supported PHP versions. +* Improve logic for flushing the runtime cache. +* Support exclusion of multiple groups. +* Update lint-staged and Node/NPM requirements. +* add CLI clean command. +* add CLI exclude-group filter. +* exclude past-due from list table all filter count. +* throwing an exception if as_schedule_recurring_action interval param is not of type integer. + = 3.5.4 - 2023-01-17 = * Add pre filters during action registration. * Async scheduling. diff --git a/inc/admin/upgrader.php b/inc/admin/upgrader.php index 0fcb17613..010ac6f09 100755 --- a/inc/admin/upgrader.php +++ b/inc/admin/upgrader.php @@ -296,7 +296,7 @@ function _imagify_new_upgrade( $network_version, $site_version ) { // 1.9.6 if ( version_compare( $site_version, '1.9.6' ) < 0 ) { - \Imagify\Stats\OptimizedMediaWithoutWebp::get_instance()->clear_cache(); + \Imagify\Stats\OptimizedMediaWithoutNextGen::get_instance()->clear_cache(); } // 1.9.11 diff --git a/inc/classes/Dependencies/deliciousbrains/wp-background-processing/classes/wp-async-request.php b/inc/classes/Dependencies/deliciousbrains/wp-background-processing/classes/wp-async-request.php index 3728718af..083c42093 100644 --- a/inc/classes/Dependencies/deliciousbrains/wp-background-processing/classes/wp-async-request.php +++ b/inc/classes/Dependencies/deliciousbrains/wp-background-processing/classes/wp-async-request.php @@ -5,6 +5,11 @@ * @package WP-Background-Processing */ +// phpcs:disable Generic.Commenting.DocComment.MissingShort +/** @noinspection PhpIllegalPsrClassPathInspection */ +/** @noinspection AutoloadingIssuesInspection */ +// phpcs:disable Generic.Commenting.DocComment.MissingShort + /** * Abstract Imagify_WP_Async_Request class. * @@ -51,7 +56,7 @@ abstract class Imagify_WP_Async_Request { protected $data = array(); /** - * Initiate new async request + * Initiate new async request. */ public function __construct() { $this->identifier = $this->prefix . '_' . $this->action; @@ -61,7 +66,7 @@ public function __construct() { } /** - * Set data used during the request + * Set data used during the request. * * @param array $data Data. * @@ -74,9 +79,9 @@ public function data( $data ) { } /** - * Dispatch the async request + * Dispatch the async request. * - * @return array|WP_Error + * @return array|WP_Error|false HTTP Response array, WP_Error on failure, or false if not attempted. */ public function dispatch() { $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); @@ -86,7 +91,7 @@ public function dispatch() { } /** - * Get query args + * Get query args. * * @return array */ @@ -109,7 +114,7 @@ protected function get_query_args() { } /** - * Get query URL + * Get query URL. * * @return string */ @@ -129,7 +134,7 @@ protected function get_query_url() { } /** - * Get post args + * Get post args. * * @return array */ @@ -139,11 +144,11 @@ protected function get_post_args() { } $args = array( - 'timeout' => 0.01, + 'timeout' => 5, 'blocking' => false, 'body' => $this->data, - 'cookies' => $_COOKIE, - 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), + 'cookies' => $_COOKIE, // Passing cookies ensures request is performed as initiating user. + 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), // Local requests, fine to pass false. ); /** @@ -155,27 +160,49 @@ protected function get_post_args() { } /** - * Maybe handle + * Maybe handle a dispatched request. * * Check for correct nonce and pass to handler. + * + * @return void|mixed */ public function maybe_handle() { - // Don't lock up other requests while processing + // Don't lock up other requests while processing. session_write_close(); check_ajax_referer( $this->identifier, 'nonce' ); $this->handle(); - wp_die(); + return $this->maybe_wp_die(); + } + + /** + * Should the process exit with wp_die? + * + * @param mixed $return What to return if filter says don't die, default is null. + * + * @return void|mixed + * @noinspection ForgottenDebugOutputInspection + */ + protected function maybe_wp_die( $return = null ) { + /** + * Should wp_die be used? + * + * @return bool + */ + if ( apply_filters( $this->identifier . '_wp_die', true ) ) { + wp_die(); + } + + return $return; } /** - * Handle + * Handle a dispatched request. * * Override this method to perform any actions required * during the async request. */ abstract protected function handle(); - } diff --git a/inc/classes/Dependencies/deliciousbrains/wp-background-processing/classes/wp-background-process.php b/inc/classes/Dependencies/deliciousbrains/wp-background-processing/classes/wp-background-process.php index 3928be667..56bbe8470 100644 --- a/inc/classes/Dependencies/deliciousbrains/wp-background-processing/classes/wp-background-process.php +++ b/inc/classes/Dependencies/deliciousbrains/wp-background-processing/classes/wp-background-process.php @@ -5,6 +5,11 @@ * @package WP-Background-Processing */ +// phpcs:disable Generic.Commenting.DocComment.MissingShort +/** @noinspection PhpIllegalPsrClassPathInspection */ +/** @noinspection AutoloadingIssuesInspection */ +// phpcs:disable Generic.Commenting.DocComment.MissingShort + /** * Abstract Imagify_WP_Background_Process class. * @@ -36,7 +41,7 @@ abstract class Imagify_WP_Background_Process extends Imagify_WP_Async_Request { /** * Cron_hook_identifier * - * @var mixed + * @var string * @access protected */ protected $cron_hook_identifier; @@ -44,13 +49,27 @@ abstract class Imagify_WP_Background_Process extends Imagify_WP_Async_Request { /** * Cron_interval_identifier * - * @var mixed + * @var string * @access protected */ protected $cron_interval_identifier; /** - * Initiate new background process + * The status set when process is cancelling. + * + * @var int + */ + const STATUS_CANCELLED = 1; + + /** + * The status set when process is paused or pausing. + * + * @var int; + */ + const STATUS_PAUSED = 2; + + /** + * Initiate new background process. */ public function __construct() { parent::__construct(); @@ -59,16 +78,22 @@ public function __construct() { $this->cron_interval_identifier = $this->identifier . '_cron_interval'; add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) ); + // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) ); } /** - * Dispatch + * Schedule the cron healthcheck and dispatch an async request to start processing the queue. * * @access public - * @return void + * @return array|WP_Error|false HTTP Response array, WP_Error on failure, or false if not attempted. */ public function dispatch() { + if ( $this->is_processing() ) { + // Process already running. + return false; + } + // Schedule the cron healthcheck. $this->schedule_event(); @@ -77,7 +102,9 @@ public function dispatch() { } /** - * Push to queue + * Push to the queue. + * + * Note, save must be called in order to persist queued items to a batch for processing. * * @param mixed $data Data. * @@ -90,7 +117,7 @@ public function push_to_queue( $data ) { } /** - * Save queue + * Save the queued items for future processing. * * @return $this */ @@ -101,11 +128,14 @@ public function save() { update_site_option( $key, $this->data ); } + // Clean out data so that new data isn't prepended with closed session's data. + $this->data = array(); + return $this; } /** - * Update queue + * Update a batch's queued items. * * @param string $key Key. * @param array $data Data. @@ -121,7 +151,7 @@ public function update( $key, $data ) { } /** - * Delete queue + * Delete a batch of queued items. * * @param string $key Key. * @@ -134,83 +164,209 @@ public function delete( $key ) { } /** - * Generate key + * Delete entire job queue. + */ + public function delete_all() { + $batches = $this->get_batches(); + + foreach ( $batches as $batch ) { + $this->delete( $batch->key ); + } + + delete_site_option( $this->get_status_key() ); + + $this->cancelled(); + } + + /** + * Cancel job on next batch. + */ + public function cancel() { + update_site_option( $this->get_status_key(), self::STATUS_CANCELLED ); + + // Just in case the job was paused at the time. + $this->dispatch(); + } + + /** + * Has the process been cancelled? + * + * @return bool + */ + public function is_cancelled() { + $status = get_site_option( $this->get_status_key(), 0 ); + + return absint( $status ) === self::STATUS_CANCELLED; + } + + /** + * Called when background process has been cancelled. + */ + protected function cancelled() { + do_action( $this->identifier . '_cancelled' ); + } + + /** + * Pause job on next batch. + */ + public function pause() { + update_site_option( $this->get_status_key(), self::STATUS_PAUSED ); + } + + /** + * Is the job paused? + * + * @return bool + */ + public function is_paused() { + $status = get_site_option( $this->get_status_key(), 0 ); + + return absint( $status ) === self::STATUS_PAUSED; + } + + /** + * Called when background process has been paused. + */ + protected function paused() { + do_action( $this->identifier . '_paused' ); + } + + /** + * Resume job. + */ + public function resume() { + delete_site_option( $this->get_status_key() ); + + $this->schedule_event(); + $this->dispatch(); + $this->resumed(); + } + + /** + * Called when background process has been resumed. + */ + protected function resumed() { + do_action( $this->identifier . '_resumed' ); + } + + /** + * Is queued? + * + * @return bool + */ + public function is_queued() { + return ! $this->is_queue_empty(); + } + + /** + * Is the tool currently active, e.g. starting, working, paused or cleaning up? + * + * @return bool + */ + public function is_active() { + return $this->is_queued() || $this->is_processing() || $this->is_paused() || $this->is_cancelled(); + } + + /** + * Generate key for a batch. * * Generates a unique key based on microtime. Queue items are * given a unique key so that they can be merged upon save. * - * @param int $length Length. + * @param int $length Optional max length to trim key to, defaults to 64 characters. + * @param string $key Optional string to append to identifier before hash, defaults to "batch". * * @return string */ - protected function generate_key( $length = 64 ) { - $unique = md5( microtime() . rand() ); - $prepend = $this->identifier . '_batch_'; + protected function generate_key( $length = 64, $key = 'batch' ) { + $unique = md5( microtime() . wp_rand() ); + $prepend = $this->identifier . '_' . $key . '_'; return substr( $prepend . $unique, 0, $length ); } /** - * Maybe process queue + * Get the status key. + * + * @return string + */ + protected function get_status_key() { + return $this->identifier . '_status'; + } + + /** + * Maybe process a batch of queued items. * * Checks whether data exists within the queue and that * the process is not already running. */ public function maybe_handle() { - // Don't lock up other requests while processing + // Don't lock up other requests while processing. session_write_close(); - if ( $this->is_process_running() ) { + if ( $this->is_processing() ) { // Background process already running. - wp_die(); + return $this->maybe_wp_die(); + } + + if ( $this->is_cancelled() ) { + $this->clear_scheduled_event(); + $this->delete_all(); + + return $this->maybe_wp_die(); + } + + if ( $this->is_paused() ) { + $this->clear_scheduled_event(); + $this->paused(); + + return $this->maybe_wp_die(); } if ( $this->is_queue_empty() ) { // No data to process. - wp_die(); + return $this->maybe_wp_die(); } check_ajax_referer( $this->identifier, 'nonce' ); $this->handle(); - wp_die(); + return $this->maybe_wp_die(); } /** - * Is queue empty + * Is queue empty? * * @return bool + * @noinspection IsEmptyFunctionUsageInspection */ protected function is_queue_empty() { - global $wpdb; - - $table = $wpdb->options; - $column = 'option_name'; - - if ( is_multisite() ) { - $table = $wpdb->sitemeta; - $column = 'meta_key'; - } - - $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; - - $count = $wpdb->get_var( $wpdb->prepare( " - SELECT COUNT(*) - FROM {$table} - WHERE {$column} LIKE %s - ", $key ) ); - - return ( $count > 0 ) ? false : true; + return empty( $this->get_batch() ); } /** - * Is process running + * Is process running? * * Check whether the current process is already running * in a background process. + * + * @return bool + * + * @deprecated 1.1.0 Superseded. + * @see is_processing() + * @noinspection PhpUnused */ protected function is_process_running() { + return $this->is_processing(); + } + + /** + * Is the background process currently running? + * + * @return bool + */ + public function is_processing() { if ( get_site_transient( $this->identifier . '_process_lock' ) ) { // Process already running. return true; @@ -220,7 +376,7 @@ protected function is_process_running() { } /** - * Lock process + * Lock process. * * Lock the process so that multiple instances can't run simultaneously. * Override if applicable, but the duration should be greater than that @@ -236,7 +392,7 @@ protected function lock_process() { } /** - * Unlock process + * Unlock process. * * Unlock the process so that other instances can spawn. * @@ -249,13 +405,34 @@ protected function unlock_process() { } /** - * Get batch + * Get batch. * - * @return stdClass Return the first batch from the queue + * @return stdClass Return the first batch of queued items. */ protected function get_batch() { + return array_reduce( + $this->get_batches( 1 ), + static function ( $carry, $batch ) { + return $batch; + }, + array() + ); + } + + /** + * Get batches. + * + * @param int $limit Number of batches to return, defaults to all. + * + * @return array of stdClass + */ + public function get_batches( $limit = 0 ) { global $wpdb; + if ( empty( $limit ) || ! is_int( $limit ) ) { + $limit = 0; + } + $table = $wpdb->options; $column = 'option_name'; $key_column = 'option_id'; @@ -270,30 +447,68 @@ protected function get_batch() { $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; - $query = $wpdb->get_row( $wpdb->prepare( " + $sql = ' SELECT * - FROM {$table} - WHERE {$column} LIKE %s - ORDER BY {$key_column} ASC - LIMIT 1 - ", $key ) ); + FROM ' . $table . ' + WHERE ' . $column . ' LIKE %s + ORDER BY ' . $key_column . ' + '; + + $args = array( $key ); + + if ( ! empty( $limit ) ) { + $sql .= ' LIMIT %d'; + + $args[] = $limit; + } + + $items = $wpdb->get_results( $wpdb->prepare( $sql, $args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - $batch = new stdClass(); - $batch->key = $query->$column; - $batch->data = maybe_unserialize( $query->$value_column ); + $batches = array(); - return $batch; + if ( ! empty( $items ) ) { + $batches = array_map( + static function ( $item ) use ( $column, $value_column ) { + $batch = new stdClass(); + $batch->key = $item->{$column}; + $batch->data = maybe_unserialize( $item->{$value_column} ); + + return $batch; + }, + $items + ); + } + + return $batches; } /** - * Handle + * Handle a dispatched request. * * Pass each queue item to the task handler, while remaining * within server memory and time limit constraints. + * + * @noinspection DisconnectedForeachInstructionInspection */ protected function handle() { $this->lock_process(); + /** + * Number of seconds to sleep between batches. Defaults to 0 seconds, minimum 0. + * + * @param int $seconds + */ + $throttle_seconds = max( + 0, + apply_filters( + $this->identifier . '_seconds_between_batches', + apply_filters( + $this->prefix . '_seconds_between_batches', + 0 + ) + ) + ); + do { $batch = $this->get_batch(); @@ -306,19 +521,25 @@ protected function handle() { unset( $batch->data[ $key ] ); } - if ( $this->time_exceeded() || $this->memory_exceeded() ) { - // Batch limits reached. + // Keep the batch up to date while processing it. + if ( ! empty( $batch->data ) ) { + $this->update( $batch->key, $batch->data ); + } + + // Let the server breathe a little. + sleep( $throttle_seconds ); + + // Batch limits reached, or pause or cancel request. + if ( $this->time_exceeded() || $this->memory_exceeded() || $this->is_paused() || $this->is_cancelled() ) { break; } } - // Update or delete current batch. - if ( ! empty( $batch->data ) ) { - $this->update( $batch->key, $batch->data ); - } else { + // Delete current batch if fully processed. + if ( empty( $batch->data ) ) { $this->delete( $batch->key ); } - } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() ); + } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() && ! $this->is_paused() && ! $this->is_cancelled() ); $this->unlock_process(); @@ -329,11 +550,11 @@ protected function handle() { $this->complete(); } - wp_die(); + return $this->maybe_wp_die(); } /** - * Memory exceeded + * Memory exceeded? * * Ensures the batch process never exceeds 90% * of the maximum WordPress memory. @@ -353,7 +574,7 @@ protected function memory_exceeded() { } /** - * Get memory limit + * Get memory limit in bytes. * * @return int */ @@ -365,7 +586,7 @@ protected function get_memory_limit() { $memory_limit = '128M'; } - if ( ! $memory_limit || - 1 === intval( $memory_limit ) ) { + if ( ! $memory_limit || -1 === (int) $memory_limit ) { // Unlimited, set to 32GB. $memory_limit = '32000M'; } @@ -374,7 +595,7 @@ protected function get_memory_limit() { } /** - * Time exceeded. + * Time limit exceeded? * * Ensures the batch never exceeds a sensible time limit. * A timeout limit of 30s is common on shared hosting. @@ -385,7 +606,10 @@ protected function time_exceeded() { $finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds $return = false; - if ( time() >= $finish ) { + if ( + ! ( defined( 'WP_CLI' ) && WP_CLI ) && + time() >= $finish + ) { $return = true; } @@ -393,18 +617,29 @@ protected function time_exceeded() { } /** - * Complete. + * Complete processing. * * Override if applicable, but ensure that the below actions are * performed, or, call parent::complete(). */ protected function complete() { - // Unschedule the cron healthcheck. + delete_site_option( $this->get_status_key() ); + + // Remove the cron healthcheck job from the cron schedule. $this->clear_scheduled_event(); + + $this->completed(); + } + + /** + * Called when background process has completed. + */ + protected function completed() { + do_action( $this->identifier . '_completed' ); } /** - * Schedule cron healthcheck + * Schedule the cron healthcheck job. * * @access public * @@ -413,29 +648,35 @@ protected function complete() { * @return mixed */ public function schedule_cron_healthcheck( $schedules ) { - $interval = apply_filters( $this->identifier . '_cron_interval', 5 ); + $interval = apply_filters( $this->cron_interval_identifier, 5 ); if ( property_exists( $this, 'cron_interval' ) ) { - $interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval ); + $interval = apply_filters( $this->cron_interval_identifier, $this->cron_interval ); } - // Adds every 5 minutes to the existing schedules. - $schedules[ $this->identifier . '_cron_interval' ] = array( + if ( 1 === $interval ) { + $display = __( 'Every Minute' ); + } else { + $display = sprintf( __( 'Every %d Minutes' ), $interval ); + } + + // Adds an "Every NNN Minute(s)" schedule to the existing cron schedules. + $schedules[ $this->cron_interval_identifier ] = array( 'interval' => MINUTE_IN_SECONDS * $interval, - 'display' => sprintf( __( 'Every %d Minutes' ), $interval ), + 'display' => $display, ); return $schedules; } /** - * Handle cron healthcheck + * Handle cron healthcheck event. * * Restart the background process if not already running * and data exists in the queue. */ public function handle_cron_healthcheck() { - if ( $this->is_process_running() ) { + if ( $this->is_processing() ) { // Background process already running. exit; } @@ -446,13 +687,11 @@ public function handle_cron_healthcheck() { exit; } - $this->handle(); - - exit; + $this->dispatch(); } /** - * Schedule event + * Schedule the cron healthcheck event. */ protected function schedule_event() { if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { @@ -461,7 +700,7 @@ protected function schedule_event() { } /** - * Clear scheduled event + * Clear scheduled cron healthcheck event. */ protected function clear_scheduled_event() { $timestamp = wp_next_scheduled( $this->cron_hook_identifier ); @@ -472,24 +711,20 @@ protected function clear_scheduled_event() { } /** - * Cancel Process + * Cancel the background process. * - * Stop processing queue items, clear cronjob and delete batch. + * Stop processing queue items, clear cron job and delete batch. * + * @deprecated 1.1.0 Superseded. + * @see cancel() + * @noinspection PhpUnused */ public function cancel_process() { - if ( ! $this->is_queue_empty() ) { - $batch = $this->get_batch(); - - $this->delete( $batch->key ); - - wp_clear_scheduled_hook( $this->cron_hook_identifier ); - } - + $this->cancel(); } /** - * Task + * Perform task with queued item. * * Override this method to perform any actions required on each * queue item. Return the modified item for further processing @@ -501,5 +736,4 @@ public function cancel_process() { * @return mixed */ abstract protected function task( $item ); - -} \ No newline at end of file +} diff --git a/inc/classes/Dependencies/wp-media/event-manager/EventManager.php b/inc/classes/Dependencies/wp-media/event-manager/EventManager.php new file mode 100644 index 000000000..5b593e5ae --- /dev/null +++ b/inc/classes/Dependencies/wp-media/event-manager/EventManager.php @@ -0,0 +1,135 @@ + + */ +class EventManager { + /** + * Adds a callback to a specific hook of the WordPress plugin API. + * + * @uses add_filter() + * + * @param string $hook_name Name of the hook. + * @param callable $callback Callback function. + * @param int $priority Priority. + * @param int $accepted_args Number of arguments. + */ + public function add_callback( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) { + add_filter( $hook_name, $callback, $priority, $accepted_args ); + } + + /** + * Add an event subscriber. + * + * The event manager registers all the hooks that the given subscriber + * wants to register with the WordPress Plugin API. + * + * @param SubscriberInterface $subscriber SubscriberInterface implementation. + */ + public function add_subscriber( SubscriberInterface $subscriber ) { + if ( $subscriber instanceof EventManagerAwareSubscriberInterface ) { + $subscriber->set_event_manager( $this ); + } + + $events = $subscriber->get_subscribed_events(); + + if ( empty( $events ) ) { + return; + } + + foreach ( $subscriber->get_subscribed_events() as $hook_name => $parameters ) { + $this->add_subscriber_callback( $subscriber, $hook_name, $parameters ); + } + } + + /** + * Checks the WordPress plugin API to see if the given hook has + * the given callback. The priority of the callback will be returned + * or false. If no callback is given will return true or false if + * there's any callbacks registered to the hook. + * + * @uses has_filter() + * + * @param string $hook_name Hook name. + * @param mixed $callback Callback. + * + * @return bool|int + */ + public function has_callback( $hook_name, $callback = false ) { + return has_filter( $hook_name, $callback ); + } + + /** + * Removes the given callback from the given hook. The WordPress plugin API only + * removes the hook if the callback and priority match a registered hook. + * + * @uses remove_filter() + * + * @param string $hook_name Hook name. + * @param callable $callback Callback. + * @param int $priority Priority. + * + * @return bool + */ + public function remove_callback( $hook_name, $callback, $priority = 10 ) { + return remove_filter( $hook_name, $callback, $priority ); + } + + /** + * Remove an event subscriber. + * + * The event manager removes all the hooks that the given subscriber + * wants to register with the WordPress Plugin API. + * + * @param SubscriberInterface $subscriber SubscriberInterface implementation. + */ + public function remove_subscriber( SubscriberInterface $subscriber ) { + foreach ( $subscriber->get_subscribed_events() as $hook_name => $parameters ) { + $this->remove_subscriber_callback( $subscriber, $hook_name, $parameters ); + } + } + + /** + * Adds the given subscriber's callback to a specific hook + * of the WordPress plugin API. + * + * @param SubscriberInterface $subscriber SubscriberInterface implementation. + * @param string $hook_name Hook name. + * @param mixed $parameters Parameters, can be a string, an array or a multidimensional array. + */ + private function add_subscriber_callback( SubscriberInterface $subscriber, $hook_name, $parameters ) { + if ( is_string( $parameters ) ) { + $this->add_callback( $hook_name, [ $subscriber, $parameters ] ); + } elseif ( is_array( $parameters ) && count( $parameters ) !== count( $parameters, COUNT_RECURSIVE ) ) { + foreach ( $parameters as $parameter ) { + $this->add_subscriber_callback( $subscriber, $hook_name, $parameter ); + } + } elseif ( is_array( $parameters ) && isset( $parameters[0] ) ) { + $this->add_callback( $hook_name, [ $subscriber, $parameters[0] ], isset( $parameters[1] ) ? $parameters[1] : 10, isset( $parameters[2] ) ? $parameters[2] : 1 ); + } + } + + /** + * Removes the given subscriber's callback to a specific hook + * of the WordPress plugin API. + * + * @param SubscriberInterface $subscriber SubscriberInterface implementation. + * @param string $hook_name Hook name. + * @param mixed $parameters Parameters, can be a string, an array or a multidimensional array. + */ + private function remove_subscriber_callback( SubscriberInterface $subscriber, $hook_name, $parameters ) { + if ( is_string( $parameters ) ) { + $this->remove_callback( $hook_name, [ $subscriber, $parameters ] ); + } elseif ( is_array( $parameters ) && count( $parameters ) !== count( $parameters, COUNT_RECURSIVE ) ) { + foreach ( $parameters as $parameter ) { + $this->remove_subscriber_callback( $subscriber, $hook_name, $parameter ); + } + } elseif ( is_array( $parameters ) && isset( $parameters[0] ) ) { + $this->remove_callback( $hook_name, [ $subscriber, $parameters[0] ], isset( $parameters[1] ) ? $parameters[1] : 10 ); + } + } +} diff --git a/inc/classes/Dependencies/wp-media/event-manager/EventManagerAwareSubscriberInterface.php b/inc/classes/Dependencies/wp-media/event-manager/EventManagerAwareSubscriberInterface.php new file mode 100644 index 000000000..b09201820 --- /dev/null +++ b/inc/classes/Dependencies/wp-media/event-manager/EventManagerAwareSubscriberInterface.php @@ -0,0 +1,14 @@ + + */ +interface SubscriberInterface { + /** + * Returns an array of events that this subscriber wants to listen to. + * + * The array key is the event name. The value can be: + * + * * The method name + * * An array with the method name and priority + * * An array with the method name, priority and number of accepted arguments + * + * For instance: + * + * * array('hook_name' => 'method_name') + * * array('hook_name' => array('method_name', $priority)) + * * array('hook_name' => array('method_name', $priority, $accepted_args)) + * * array('hook_name' => array(array('method_name_1', $priority_1, $accepted_args_1)), array('method_name_2', $priority_2, $accepted_args_2))) + * + * @return array + */ + public static function get_subscribed_events(); +} diff --git a/inc/classes/class-imagify-admin-ajax-post.php b/inc/classes/class-imagify-admin-ajax-post.php index 4d5439735..420ebbcb4 100755 --- a/inc/classes/class-imagify-admin-ajax-post.php +++ b/inc/classes/class-imagify-admin-ajax-post.php @@ -27,8 +27,8 @@ class Imagify_Admin_Ajax_Post extends Imagify_Admin_Ajax_Post_Deprecated { 'imagify_manual_optimize', 'imagify_manual_reoptimize', 'imagify_optimize_missing_sizes', - 'imagify_generate_webp_versions', - 'imagify_delete_webp_versions', + 'imagify_generate_nextgen_versions', + 'imagify_delete_nextgen_versions', 'imagify_restore', // Custom folders optimization. 'imagify_optimize_file', @@ -194,7 +194,7 @@ protected function optimize_missing_sizes( $media_id, $context ) { } /** - * Generate WebP images if they are missing. + * Generate next-gen images if they are missing. * * @since 1.9 * @@ -202,12 +202,12 @@ protected function optimize_missing_sizes( $media_id, $context ) { * @param string $context The context. * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ - protected function generate_webp_versions( $media_id, $context ) { - return imagify_get_optimization_process( $media_id, $context )->generate_webp_versions(); + protected function generate_nextgen_versions( $media_id, $context ) { + return imagify_get_optimization_process( $media_id, $context )->generate_nextgen_versions(); } /** - * Delete WebP images for media that are "already_optimize". + * Delete Next gen images for media that are "already_optimize". * * @since 1.9.6 * @@ -215,7 +215,7 @@ protected function generate_webp_versions( $media_id, $context ) { * @param string $context The context. * @return bool|WP_Error True if successfully launched. A \WP_Error instance on failure. */ - protected function delete_webp_versions( $media_id, $context ) { + protected function delete_nextgen_versions( $media_id, $context ) { $process = imagify_get_optimization_process( $media_id, $context ); if ( ! $process->is_valid() ) { @@ -228,15 +228,15 @@ protected function delete_webp_versions( $media_id, $context ) { return new WP_Error( 'not_already_optimized', __( 'This media does not have the right optimization status.', 'imagify' ) ); } - if ( ! $process->has_webp() ) { + if ( ! $process->has_next_gen() ) { return true; } $data->delete_optimization_data(); - $deleted = $process->delete_webp_files(); + $deleted = $process->delete_nextgen_files( false, true ); if ( is_wp_error( $deleted ) ) { - return new WP_Error( 'webp_not_deleted', __( 'Previous WebP files could not be deleted.', 'imagify' ) ); + return new WP_Error( 'nextgen_not_deleted', __( 'Previous next-gen files could not be deleted.', 'imagify' ) ); } return true; @@ -359,11 +359,11 @@ public function imagify_optimize_missing_sizes_callback() { } /** - * Generate WebP images if they are missing. + * Generate next-gen images if they are missing. * * @since 1.9 */ - public function imagify_generate_webp_versions_callback() { + public function imagify_generate_nextgen_versions_callback() { $context = $this->get_context(); $media_id = $this->get_media_id(); @@ -371,13 +371,13 @@ public function imagify_generate_webp_versions_callback() { imagify_die( __( 'Invalid request', 'imagify' ) ); } - imagify_check_nonce( 'imagify-generate-webp-versions-' . $media_id . '-' . $context ); + imagify_check_nonce( 'imagify-generate-nextgen-versions-' . $media_id . '-' . $context ); if ( ! imagify_get_context( $context )->current_user_can( 'manual-optimize', $media_id ) ) { imagify_die(); } - $result = $this->generate_webp_versions( $media_id, $context ); + $result = $this->generate_nextgen_versions( $media_id, $context ); imagify_maybe_redirect( is_wp_error( $result ) ? $result : false ); @@ -392,11 +392,11 @@ public function imagify_generate_webp_versions_callback() { } /** - * Generate WebP images if they are missing. + * Generate next-gen images if they are missing. * * @since 1.9.6 */ - public function imagify_delete_webp_versions_callback() { + public function imagify_delete_nextgen_versions_callback() { $context = $this->get_context(); $media_id = $this->get_media_id(); @@ -404,13 +404,13 @@ public function imagify_delete_webp_versions_callback() { imagify_die( __( 'Invalid request', 'imagify' ) ); } - imagify_check_nonce( 'imagify-delete-webp-versions-' . $media_id . '-' . $context ); + imagify_check_nonce( 'imagify-delete-nextgen-versions-' . $media_id . '-' . $context ); if ( ! imagify_get_context( $context )->current_user_can( 'manual-restore', $media_id ) ) { imagify_die(); } - $result = $this->delete_webp_versions( $media_id, $context ); + $result = $this->delete_nextgen_versions( $media_id, $context ); imagify_maybe_redirect( is_wp_error( $result ) ? $result : false ); diff --git a/inc/classes/class-imagify-files-list-table.php b/inc/classes/class-imagify-files-list-table.php index 0ca273f18..eddf8191b 100755 --- a/inc/classes/class-imagify-files-list-table.php +++ b/inc/classes/class-imagify-files-list-table.php @@ -606,11 +606,11 @@ public function column_optimization( $item ) { process->get_media()->is_image() ) { - $has_webp = $item->process->has_webp() ? __( 'Yes', 'imagify' ) : __( 'No', 'imagify' ); + $has_nextgen = $item->process->has_next_gen() ? __( 'Yes', 'imagify' ) : __( 'No', 'imagify' ); ?>
  • - - + +
  • optimize_button( $item ); $this->retry_button( $item ); $this->reoptimize_buttons( $item ); - $this->generate_webp_versions_button( $item ); - $this->delete_webp_versions_button( $item ); + $this->generate_nextgen_versions_button( $item ); + $this->delete_nextgen_versions_button( $item ); $this->restore_button( $item ); } @@ -806,14 +806,14 @@ protected function reoptimize_buttons( $item ) { } /** - * Prints a button to generate WebP versions if they are missing. + * Prints a button to generate Next gen versions if they are missing. * * @since 1.7 * * @param object $item The current item. It must contain at least a $process property. */ - protected function generate_webp_versions_button( $item ) { - $button = get_imagify_attachment_generate_webp_versions_link( $item->process ); + protected function generate_nextgen_versions_button( $item ) { + $button = get_imagify_attachment_generate_nextgen_versions_link( $item->process ); if ( $button ) { echo $button . '
    '; @@ -821,14 +821,14 @@ protected function generate_webp_versions_button( $item ) { } /** - * Prints a button to delete WebP versions when the status is "already_optimized". + * Prints a button to delete next-gen versions when the status is "already_optimized". * * @since 1.9.6 * * @param object $item The current item. It must contain at least a $process property. */ - protected function delete_webp_versions_button( $item ) { - $button = get_imagify_attachment_delete_webp_versions_link( $item->process ); + protected function delete_nextgen_versions_button( $item ) { + $button = get_imagify_attachment_delete_nextgen_versions_link( $item->process ); if ( $button ) { echo $button . '
    '; diff --git a/inc/classes/class-imagify-options.php b/inc/classes/class-imagify-options.php index 9c196ce4d..328377625 100644 --- a/inc/classes/class-imagify-options.php +++ b/inc/classes/class-imagify-options.php @@ -27,20 +27,20 @@ class Imagify_Options extends Imagify_Abstract_Options { * @since 1.7 */ protected $default_values = [ - 'api_key' => '', - 'optimization_level' => 2, - 'lossless' => 0, - 'auto_optimize' => 0, - 'backup' => 0, - 'resize_larger' => 0, - 'resize_larger_w' => 0, - 'convert_to_webp' => 0, - 'display_webp' => 0, - 'display_webp_method' => 'picture', - 'cdn_url' => '', - 'disallowed-sizes' => [], - 'admin_bar_menu' => 0, - 'partner_links' => 0, + 'api_key' => '', + 'optimization_level' => 2, + 'lossless' => 0, + 'auto_optimize' => 0, + 'backup' => 0, + 'resize_larger' => 0, + 'resize_larger_w' => 0, + 'display_nextgen' => 0, + 'display_nextgen_method' => 'picture', + 'cdn_url' => '', + 'disallowed-sizes' => [], + 'admin_bar_menu' => 0, + 'partner_links' => 0, + 'convert_to_avif' => 0, ]; /** @@ -54,7 +54,6 @@ class Imagify_Options extends Imagify_Abstract_Options { 'optimization_level' => 2, 'auto_optimize' => 1, 'backup' => 1, - 'convert_to_webp' => 1, 'admin_bar_menu' => 1, 'partner_links' => 1, ]; @@ -131,10 +130,11 @@ public function sanitize_and_validate_value( $key, $value, $default ) { case 'lossless': case 'resize_larger': case 'convert_to_webp': - case 'display_webp': + case 'display_nextgen': case 'admin_bar_menu': case 'partner_links': - return 1; + case 'convert_to_avif': + return empty( $value ) ? 0 : 1; case 'resize_larger_w': if ( $value <= 0 ) { @@ -159,7 +159,7 @@ public function sanitize_and_validate_value( $key, $value, $default ) { $value = array_map( 'sanitize_text_field', $value ); return array_fill_keys( $value, 1 ); - case 'display_webp_method': + case 'display_nextgen_method': $values = [ 'picture' => 1, 'rewrite' => 1, @@ -172,7 +172,7 @@ public function sanitize_and_validate_value( $key, $value, $default ) { return $reset_values[ $key ]; case 'cdn_url': - $cdn_source = \Imagify\Webp\Picture\Display::get_instance()->get_cdn_source( $value ); + $cdn_source = apply_filters( 'imagify_cdn_source_url', $value ); if ( 'option' !== $cdn_source['source'] ) { /** @@ -202,11 +202,6 @@ public function validate_values_on_update( $values ) { unset( $values['resize_larger'], $values['resize_larger_w'] ); } - // Don't display wepb if conversion is disabled. - if ( empty( $values['convert_to_webp'] ) ) { - unset( $values['convert_to_webp'], $values['display_webp'] ); - } - return $values; } } diff --git a/inc/common/attachments.php b/inc/common/attachments.php index b58f070e7..ddf34d612 100755 --- a/inc/common/attachments.php +++ b/inc/common/attachments.php @@ -22,7 +22,7 @@ function imagify_trigger_delete_attachment_hook( $post_id ) { add_action( 'imagify_delete_media', 'imagify_cleanup_after_media_deletion' ); /** - * Delete the backup file and the WebP files when an attachement is deleted. + * Delete the backup file and the next-gen files when an attachement is deleted. * * @since 1.9 * @author Grégory Viguier @@ -36,15 +36,16 @@ function imagify_cleanup_after_media_deletion( $process ) { /** * The optimization data will be automatically deleted by WP (post metas). - * Delete the WebP versions and the backup file. + * Delete the Nextgen versions and the backup file. */ - $process->delete_webp_files(); + $process->delete_nextgen_files( false, true ); + $process->delete_backup(); } -add_filter( 'ext2type', 'imagify_add_webp_type' ); +add_filter( 'ext2type', 'imagify_add_avif_type' ); /** - * Add the WebP extension to wp_get_ext_types(). + * Add the AVIF extension to wp_get_ext_types(). * * @since 1.9 * @author Grégory Viguier @@ -52,9 +53,9 @@ function imagify_cleanup_after_media_deletion( $process ) { * @param array $ext2type Multi-dimensional array with extensions for a default set of file types. * @return array */ -function imagify_add_webp_type( $ext2type ) { - if ( ! in_array( 'webp', $ext2type['image'], true ) ) { - $ext2type['image'][] = 'webp'; +function imagify_add_avif_type( $ext2type ) { + if ( ! in_array( 'avif', $ext2type['image'], true ) ) { + $ext2type['image'][] = 'avif'; } return $ext2type; } @@ -67,3 +68,37 @@ function imagify_add_webp_type( $ext2type ) { * @author Grégory Viguier */ add_filter( 'big_image_size_threshold', [ imagify_get_context( 'wp' ), 'get_resizing_threshold' ], IMAGIFY_INT_MAX ); + +/** + * Add filters to manage images formats that will be generated + * + * @return array + */ +function imagify_nextgen_images_formats() { + $formats = [ + 'webp' => 'webp', + ]; + + if ( get_imagify_option( 'convert_to_avif' ) ) { + $formats['avif'] = 'avif'; + + unset( $formats['webp'] ); + } + + $default = $formats; + + /** + * Filters the array of next gen formats to generate with Imagify + * + * @since 2.2 + * + * @param array $formats Array of image formats + */ + $formats = apply_filters( 'imagify_nextgen_images_formats', $formats ); + + if ( ! is_array( $formats ) ) { + $formats = $default; + } + + return $formats; +} diff --git a/inc/functions/admin-ui.php b/inc/functions/admin-ui.php index 372d3f5a0..245d53f0d 100644 --- a/inc/functions/admin-ui.php +++ b/inc/functions/admin-ui.php @@ -23,8 +23,8 @@ function get_imagify_attachment_optimization_text( $process ) { $output_after = $is_media_page ? '
    ' : ''; $reoptimize_link = get_imagify_attachment_reoptimize_link( $process ); $reoptimize_link .= get_imagify_attachment_optimize_missing_thumbnails_link( $process ); - $reoptimize_link .= get_imagify_attachment_generate_webp_versions_link( $process ); - $reoptimize_link .= get_imagify_attachment_delete_webp_versions_link( $process ); + $reoptimize_link .= get_imagify_attachment_generate_nextgen_versions_link( $process ); + $reoptimize_link .= get_imagify_attachment_delete_nextgen_versions_link( $process ); $reoptimize_output = $reoptimize_link ? $reoptimize_link : ''; $reoptimize_output_before = ''; @@ -94,12 +94,12 @@ function get_imagify_attachment_optimization_text( $process ) { $output .= $output_before . '' . __( 'Level:', 'imagify' ) . ' ' . $optimization_level . '' . $output_after; if ( $media->is_image() ) { - $has_webp = $process->has_webp() ? __( 'Yes', 'imagify' ) : __( 'No', 'imagify' ); + $has_nextgen = $process->has_next_gen() ? __( 'Yes', 'imagify' ) : __( 'No', 'imagify' ); - if ( $process->has_webp() ) { - $has_webp = $process->is_full_webp() ? __( 'Yes', 'imagify' ) : __( 'Partially', 'imagify' ); + if ( $process->has_next_gen() ) { + $has_nextgen = $process->is_full_next_gen() ? __( 'Yes', 'imagify' ) : __( 'Partially', 'imagify' ); } - $output .= $output_before . '' . __( 'WebP generated:', 'imagify' ) . ' ' . esc_html( $has_webp ) . '' . $output_after; + $output .= $output_before . '' . __( 'Next-Gen generated:', 'imagify' ) . ' ' . esc_html( $has_nextgen ) . '' . $output_after; $total_optimized_thumbnails = $data->get_optimized_sizes_count(); @@ -317,20 +317,22 @@ function get_imagify_attachment_optimize_missing_thumbnails_link( $process ) { } /** - * Get the link to generate WebP versions if they are missing. + * Get the link to generate next-gen versions if they are missing. * - * @since 1.9 - * @author Grégory Viguier + * @since 1.9 * - * @param ProcessInterface $process The optimization process object. - * @return string The output to print. + * @param ProcessInterface $process The optimization process object. + * + * @return string The output to print. */ -function get_imagify_attachment_generate_webp_versions_link( $process ) { +function get_imagify_attachment_generate_nextgen_versions_link( $process ) { if ( ! $process->is_valid() ) { return ''; } - if ( ! get_imagify_option( 'convert_to_webp' ) ) { + $formats = imagify_nextgen_images_formats(); + + if ( empty( $formats ) ) { return ''; } @@ -340,7 +342,13 @@ function get_imagify_attachment_generate_webp_versions_link( $process ) { return ''; } - if ( 'image/webp' === $media->get_mime_type() ) { + if ( + get_imagify_option( 'convert_to_avif' ) + && + 'image/avif' === $media->get_mime_type() + ) { + return ''; + } elseif ( 'image/webp' === $media->get_mime_type() ) { return ''; } @@ -350,14 +358,16 @@ function get_imagify_attachment_generate_webp_versions_link( $process ) { return ''; } - if ( $process->has_webp() ) { + if ( $process->has_next_gen() ) { return ''; } $context = $media->get_context(); + $display = apply_filters_deprecated( 'imagify_display_generate_webp_versions_link', array( true, $process, $context ), '2.2', 'imagify_display_generate_next_gen_versions_link' ); + /** - * Allow to not display the "Generate WebP versions" link. + * Allow to not display the "Generate next-gen versions" link. * * @since 1.9 * @author Grégory Viguier @@ -366,14 +376,14 @@ function get_imagify_attachment_generate_webp_versions_link( $process ) { * @param ProcessInterface $process The optimization process object. * @param string $context The context. */ - $display = apply_filters( 'imagify_display_generate_webp_versions_link', true, $process, $context ); + $display = apply_filters( 'imagify_display_generate_next_gen_versions_link', $display, $process, $context ); // Stop the process if the filter is false. if ( ! $display ) { return ''; } - $url = get_imagify_admin_url( 'generate-webp-versions', [ + $url = get_imagify_admin_url( 'generate-nextgen-versions', [ 'attachment_id' => $media->get_id(), 'context' => $context, ] ); @@ -386,7 +396,7 @@ function get_imagify_attachment_generate_webp_versions_link( $process ) { } /** - * Get the link to delete WebP versions when the status is "already_optimized". + * Get the link to delete next-gen versions when the status is "already_optimized". * * @since 1.9.6 * @author Grégory Viguier @@ -394,7 +404,7 @@ function get_imagify_attachment_generate_webp_versions_link( $process ) { * @param ProcessInterface $process The optimization process object. * @return string The output to print. */ -function get_imagify_attachment_delete_webp_versions_link( $process ) { +function get_imagify_attachment_delete_nextgen_versions_link( $process ) { if ( ! $process->is_valid() ) { return ''; } @@ -409,12 +419,12 @@ function get_imagify_attachment_delete_webp_versions_link( $process ) { $data = $process->get_data(); - if ( ! $data->is_already_optimized() || ! $process->has_webp() ) { + if ( ! $data->is_already_optimized() || ! $process->has_next_gen() ) { return ''; } $class = ''; - $url = get_imagify_admin_url( 'delete-webp-versions', [ + $url = get_imagify_admin_url( 'delete-nextgen-versions', [ 'attachment_id' => $media_id, 'context' => $context, ] ); diff --git a/inc/functions/admin.php b/inc/functions/admin.php index 41c0be4d0..efe7c9354 100755 --- a/inc/functions/admin.php +++ b/inc/functions/admin.php @@ -103,11 +103,11 @@ function get_imagify_admin_url( $action = 'settings', $arg = [] ) { case 'optimize-missing-sizes': return wp_nonce_url( admin_url( 'admin-post.php?action=imagify_optimize_missing_sizes&attachment_id=' . $id . '&context=' . $context ), 'imagify-optimize-missing-sizes-' . $id . '-' . $context ); - case 'generate-webp-versions': - return wp_nonce_url( admin_url( 'admin-post.php?action=imagify_generate_webp_versions&attachment_id=' . $id . '&context=' . $context ), 'imagify-generate-webp-versions-' . $id . '-' . $context ); + case 'generate-nextgen-versions': + return wp_nonce_url( admin_url( 'admin-post.php?action=imagify_generate_nextgen_versions&attachment_id=' . $id . '&context=' . $context ), 'imagify-generate-nextgen-versions-' . $id . '-' . $context ); - case 'delete-webp-versions': - return wp_nonce_url( admin_url( 'admin-post.php?action=imagify_delete_webp_versions&attachment_id=' . $id . '&context=' . $context ), 'imagify-delete-webp-versions-' . $id . '-' . $context ); + case 'delete-nextgen-versions': + return wp_nonce_url( admin_url( 'admin-post.php?action=imagify_delete_nextgen_versions&attachment_id=' . $id . '&context=' . $context ), 'imagify-delete-nextgen-versions-' . $id . '-' . $context ); case 'optimize': case 'manual-upload': // Deprecated. diff --git a/inc/functions/api.php b/inc/functions/api.php index e53831340..2168dd70e 100755 --- a/inc/functions/api.php +++ b/inc/functions/api.php @@ -277,14 +277,14 @@ function imagify_bulk_optimize( $contexts, $optimization_level ) { } /** - * Runs the WebP generation + * Runs the next-gen generation * * @param array $contexts An array of contexts (WP/Custom folders). * * @return void */ -function imagify_generate_webp( $contexts ) { - Imagify\Bulk\Bulk::get_instance()->run_generate_webp( $contexts ); +function imagify_generate_nextgen( $contexts ) { + Imagify\Bulk\Bulk::get_instance()->run_generate_nextgen( $contexts ); } /** diff --git a/inc/functions/common.php b/inc/functions/common.php index dbc24f470..ac4bffc3a 100755 --- a/inc/functions/common.php +++ b/inc/functions/common.php @@ -185,6 +185,32 @@ function imagify_path_to_webp( $path ) { return $path . '.webp'; } +/** + * Convert a path (or URL) to its next-gen version. + * To keep the function simple: + * - Not tested if it's an image. + * - File existance is not tested. + * - If an URL is given, make sure it doesn't contain query args. + * + * @since 2.2 + * + * @param string $path A file path or URL. + * @param string $format format we are targeting. + * @return string + */ +function imagify_path_to_nextgen( $path, string $format ) { + switch ( $format ) { + case 'webp': + $path = $path . '.webp'; + break; + case 'avif': + $path = $path . '.avif'; + break; + } + + return $path; +} + /** * Tell if the current user can optimize custom folders. * diff --git a/inc/functions/i18n.php b/inc/functions/i18n.php index 425c99a42..8edd3a29c 100755 --- a/inc/functions/i18n.php +++ b/inc/functions/i18n.php @@ -2,7 +2,7 @@ use Imagify\Imagifybeat\Actions; use Imagify\Imagifybeat\Core; -use Imagify\Stats\OptimizedMediaWithoutWebp; +use Imagify\Stats\OptimizedMediaWithoutNextGen; defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); @@ -79,27 +79,27 @@ function get_imagify_localize_script_translations( $context ) { ], ]; - if ( OptimizedMediaWithoutWebp::get_instance()->get_cached_stat() ) { + if ( OptimizedMediaWithoutNextGen::get_instance()->get_cached_stat() ) { $contexts = imagify_get_context_names(); $translations['bulk'] = [ - 'curlMissing' => ! Imagify_Requirements::supports_curl(), - 'editorMissing' => ! Imagify_Requirements::supports_image_editor(), - 'extHttpBlocked' => Imagify_Requirements::is_imagify_blocked(), - 'apiDown' => ! Imagify_Requirements::is_api_up(), - 'keyIsValid' => Imagify_Requirements::is_api_key_valid(), - 'isOverQuota' => Imagify_Requirements::is_over_quota(), - 'imagifybeatIDs' => [ + 'curlMissing' => ! Imagify_Requirements::supports_curl(), + 'editorMissing' => ! Imagify_Requirements::supports_image_editor(), + 'extHttpBlocked' => Imagify_Requirements::is_imagify_blocked(), + 'apiDown' => ! Imagify_Requirements::is_api_up(), + 'keyIsValid' => Imagify_Requirements::is_api_key_valid(), + 'isOverQuota' => Imagify_Requirements::is_over_quota(), + 'imagifybeatIDs' => [ 'progress' => $imagifybeat_actions->get_imagifybeat_id( 'options_optimization_status' ), 'requirements' => $imagifybeat_actions->get_imagifybeat_id( 'requirements' ), ], - 'ajaxActions' => [ - 'MissingWebp' => 'imagify_missing_webp_generation', + 'ajaxActions' => [ + 'MissingNextGen' => 'imagify_missing_nextgen_generation', ], - 'ajaxNonce' => wp_create_nonce( 'imagify-bulk-optimize' ), - 'contexts' => $contexts, - 'progress_webp' => [ - 'remaining' => OptimizedMediaWithoutWebp::get_instance()->get_stat(), - 'total' => get_transient( 'imagify_missing_webp_total' ), + 'ajaxNonce' => wp_create_nonce( 'imagify-bulk-optimize' ), + 'contexts' => $contexts, + 'progress_next_gen' => [ + 'remaining' => OptimizedMediaWithoutNextGen::get_instance()->get_stat(), + 'total' => get_transient( 'imagify_missing_next_gen_total' ), ], 'labels' => [ 'curlMissing' => __( 'cURL is not available on the server.', 'imagify' ), @@ -113,8 +113,8 @@ function get_imagify_localize_script_translations( $context ) { 'invalidAPIKeyTitle' => __( 'Your API key is not valid!', 'imagify' ), 'overQuotaTitle' => __( 'You have used all your credits!', 'imagify' ), 'nothingToDoTitle' => __( 'Hold on!', 'imagify' ), - 'nothingToDoText' => __( 'All your optimized images already have a WebP version. Congratulations!', 'imagify' ), - 'nothingToDoNoBackupText' => __( 'Because the selected images did not have a backup copy, Imagify was unable to create WebP versions.', 'imagify' ), + 'nothingToDoText' => __( 'All your optimized images already have a next-gen version. Congratulations!', 'imagify' ), + 'nothingToDoNoBackupText' => __( 'Because the selected images did not have a backup copy, Imagify was unable to create next-gen versions.', 'imagify' ), 'error' => __( 'Error', 'imagify' ), 'ajaxErrorText' => __( 'The operation failed.', 'imagify' ), 'getUnoptimizedImagesErrorTitle' => __( 'Oops, There is something wrong!', 'imagify' ), @@ -262,7 +262,7 @@ function get_imagify_localize_script_translations( $context ) { 'nothingToDoTitle' => __( 'Hold on!', 'imagify' ), 'nothingToDoText' => [ 'optimize' => __( 'All your media files have been optimized by Imagify. Congratulations!', 'imagify' ), - 'generate_webp' => __( 'All your optimized images already have a WebP version. Congratulations!', 'imagify' ), + 'generate_webp' => __( 'All your optimized images already have a next-gen version. Congratulations!', 'imagify' ), ], 'optimizing' => __( 'Optimizing', 'imagify' ), 'error' => __( 'Error', 'imagify' ), diff --git a/inc/main.php b/inc/main.php index 3106246e8..eaad6169a 100644 --- a/inc/main.php +++ b/inc/main.php @@ -1,4 +1,5 @@ IMAGIFY_PATH, ) ); - $plugin->init(); + $plugin->init( $providers ); } add_action( 'plugins_loaded', 'imagify_init' ); diff --git a/readme.txt b/readme.txt index 88e886744..7dae8223c 100644 --- a/readme.txt +++ b/readme.txt @@ -1,12 +1,12 @@ -=== Imagify – Optimize Images & Convert WebP | Compress Images Easily === +=== Imagify – Optimize Images & Convert WebP & AVIF | Compress Images Easily === Contributors: wp_rocket, imagify -Tags: optimize images, convert webp, webp converter, image optimization, compress images, image compressor, resize images, reduce image size, performance, image optimizer, core web vitals, best image optimization plugin +Tags: optimize images, convert webp, webp converter, convert AVIF, webp to AVIF, AVIF plugin, AVIF converter, image optimization, compress images, image compressor, resize images, reduce image size, performance, image optimizer, core web vitals, best image optimization plugin Tested up to: 6.4 -Stable tag: 2.1.3.1 +Stable tag: 2.2 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html -Compress images & convert WebP with the best WordPress image optimization plugin. Optimize images in 1-click & resize images with our image optimizer! +Compress images & convert WebP and AVIF with the best WordPress image optimization plugin. Optimize images in 1-click & resize images with our image optimizer! == Description == @@ -16,7 +16,7 @@ Compress images & convert WebP with the best WordPress image optimization plugin Imagify is the best WordPress image optimizer. The plugin lets you optimize images in one go with its asynchronous bulk optimization option. You can resize your images on the fly – they will be automatically optimized at the best compression level. If needed, you can always restore your images to their original versions. -On top of optimizing images, you’ll choose the best WebP plugin for WordPress. Imagify also converts your images to WebP, a next-gen format for lighter images that will speed up your WordPress site, improve user experience, and even SEO. Convert WebP will make a difference in images optimization, you’ll see that from yourself. +On top of optimizing images, you’ll choose the best WebP plugin and AVIF plugin for WordPress. Imagify also converts your images to WebP and AVIF, the next-gen formats for lighter images that will speed up your WordPress site, improve user experience, and even SEO. Convert WebP and convert AVIF will make a difference in image optimization, you’ll see that from yourself. Lastly, it’s easy to use the best image compression plugin for better site performance. Speed up your WordPress site and improve Core Web Vitals thanks to Imagify’s state-of-the-art image optimization process. @@ -35,21 +35,24 @@ With such a great image optimizer like Imagify, you’ll get your images automat When using our image compression tool, Imagify, you will enjoy smaller file sizes and faster loading times. But the best part is that you will optimise images and reduce their weight without losing quality: why should you have to choose between beauty and speed? If you want to compress images, it’s now easier than ever with Imagify! -= Convert Images to the WebP Format = += Convert Images to the WebP and AVIF Formats = -Thanks to Imagify, you can take a step further in your image optimization process. You can also convert all your images to next-gen image formats such as WebP. WebP format offers superior image compression and quality and is a way to optimise images and speed up their loading times on websites. To convert images to next-gen format, using the Imagify plugin will save you a precious amount of time. +Thanks to Imagify, you can take a step further in your image optimization process. You can also convert all your images to next-gen image formats such as WebP and AVIF. WebP format, as well as the AVIF format, offers superior image compression and quality and is a way to optimise images and speed up their loading times on websites. To convert images to next-gen format, using the Imagify plugin will save you a precious amount of time. -With Imagify, WebP compression and conversion are indeed super easy. Wondering how the convert WebP option works? If the "Create WebP versions of images" option is enabled, the WebP versions of optimized images will be created automatically. Simple as that! Thanks to the best WebP plugin for WordPress, you’ll be able to take advantage of the convert WebP feature in no time. +With Imagify, WebP compression and conversion are indeed super easy. Wondering how the convert WebP option works? Imagify will automatically enable WebP and convert your images to WebP. You can also easily convert WebP images to AVIF. -Imagify can also display these WebP images directly on your front-end. But because some browsers don’t support WebP yet, Imagify lets you have both optimized versions of the original format images and the WebP versions ready. Imagify will serve the WebP versions if a visitor's browser supports them and the other optimized format if not. +If the "Create Next-gen formats versions" option is enabled, the AVIF version of optimized images will be created automatically. Simple as that! Thanks to the best WebP and AVIF plugin for WordPress, you’ll be able to take advantage of the automatic convert WebP feature in no time, plus you’ll always have the convert AVIF option. -WebP is definitively an excellent replacement for jpeg, png, and gif images. Imagify offers WebP conversion for all image formats: you can convert gif to webP, jpeg to WebP, and even png to WebP. Whatever your favorite image format, let Imagify optimise your images! +Imagify can also display the AVIF images directly on your front-end. But because some browser versions don’t support AVIF yet, Imagify lets you have both optimized versions of the original format images and the WebP versions ready. Imagify will serve the AVIF version if a visitor's browser supports it and the WebP format if not (if you have previously converted the images to WebP). + +WebP is definitively an excellent replacement for jpeg, png, and gif images, and the same goes for AVIF, which improves your image performance even further. Imagify offers WebP conversion for all image formats: you can convert gif to webP, jpeg to WebP, and even png to WebP. You can also benefit from AVIF conversion and convert gif to AVIF, jpg and jpeg to AVIF, ng to AVIF, and even WebP to AVIF! +Whatever your favorite image format, let Imagify optimise your images! = Optimise Images to Make Your Site Faster and Improve Core Web Vitals = Did you know that image optimization and web performance go hand in hand? In fact, when it comes to web page speed, one of the first things you should do is optimize your images. Large and heavy image sizes will indeed slow down your website and provide a bad user experience to your visitors – and that’s why you should compress images. On the contrary, when you optimise images and improve image loading speed, you should see a direct improvement in your website speed and performance. With Imagify, it’s time to say goodbye to images taking too long to load. -Images are one of the largest influencing factors in the Core Web Vitals. Image compression will ensure that your images load faster and improve your overall website performance, including your Core Web Vitals metrics. If you’re looking to improve user experience and speed up your WordPress site, images are a good place to start. +Images are one of the largest influencing factors in the Core Web Vitals. Image compression will ensure your images load faster and improve your overall website performance, including your Core Web Vitals metrics. If you’re looking to improve user experience and speed up your WordPress site, images are a good place to start. Even Google tells you to take care of your images! If you have ever run a performance audit on PageSpeed Insights, you might have seen the [“serve images in next-gen formats”](https://imagify.io/blog/serve-next-gen-formats-wordpress/) opportunity popping up. Another PageSpeed Insights recommendation related to images is to [“efficiently encode images”](https://imagify.io/blog/efficiently-encode-images-wordpress/). When you use Imagify, you will be able to address both recommendations and fix your images for a faster website. @@ -361,13 +364,13 @@ You can report any security bugs found in the source code of the site-reviews pl = 1.9.5 - 2019/07/16 = * Improvement: Basic Authentication support. If it does not work automatically, you can still define the constants `IMAGIFY_AUTH_USER` and `IMAGIFY_AUTH_PASSWORD` in your `wp-config.php` file. -* Improvement: WebP images are not created for animated gif images by default anymore. Use the filter `imagify_pre_can_create_webp_version` if you still want to create an unanimated WebP version of them. +* Improvement: WebP images are not created for animated gif images by default anymore. Use the filter `imagify_pre_can_create_next_gen_version` if you still want to create an unanimated WebP version of them. * Improvement: when creating WebP images from the settings page, we made more clear when all the images are missing a backup copy. * Improvement: clear the 5 minutes data cache when buying quota from the plugin. * Improvement: when displaying WebP images with the `` tag, allow to use relative URLs (starting with `/`). = 1.9.4 - 2019/07/10 = -* Improvement: if a WebP image is larger than its non-webp version, it is now possible to not keep it. This can be done by using the filter `imagify_keep_large_webp`. +* Improvement: if a next-gen image is larger than its non-next-gen version, it is now possible to not keep it. This can be done by using the filter `imagify_keep_large_next_gen`. * Improvement: compatibility with Pressable. * Improvement: renamed a php class to prevent some hosts to wrongly flag it as "suspicious" and trigger a fatal error. * Improvement: better compatibility with WP Real Media Library plugin. diff --git a/uninstall.php b/uninstall.php index 578392adc..82a9de69c 100755 --- a/uninstall.php +++ b/uninstall.php @@ -26,7 +26,7 @@ delete_transient( 'imagify_large_library' ); delete_transient( 'imagify_max_image_size' ); delete_transient( 'imagify_user' ); -delete_transient( 'imagify_stat_without_webp' ); +delete_transient( 'imagify_stat_without_next_gen' ); // Delete transients. $transients = implode( '" OR option_name LIKE "', array( diff --git a/views/button/delete-webp.php b/views/button/delete-webp.php index 26040c468..527bef571 100755 --- a/views/button/delete-webp.php +++ b/views/button/delete-webp.php @@ -22,7 +22,7 @@ > - + "> diff --git a/views/part-settings-webp-missing-message.php b/views/part-settings-webp-missing-message.php index e6fa69827..182122a3f 100644 --- a/views/part-settings-webp-missing-message.php +++ b/views/part-settings-webp-missing-message.php @@ -5,8 +5,8 @@ sprintf( /* translators: %s is a formatted number (don’t use %d). */ _n( - 'It seems that you have %s optimized image without WebP versions. You can generate it here.', - 'It seems that you have %s optimized images without WebP versions. You can generate them here.', + 'It seems that you have %s optimized image without Next-Gen versions. You can generate it here.', + 'It seems that you have %s optimized images without Next-Gen versions. You can generate them here.', $data['count'], 'imagify' ), diff --git a/views/part-settings-webp.php b/views/part-settings-webp.php index ab1013a8f..6d2e5680f 100755 --- a/views/part-settings-webp.php +++ b/views/part-settings-webp.php @@ -1,39 +1,45 @@
    -

    +

    field_checkbox( [ - 'option_name' => 'convert_to_webp', - 'label' => __( 'Create WebP versions of images', 'imagify' ), + 'option_name' => 'convert_to_avif', + 'label' => __( 'Create AVIF versions of images', 'imagify' ), 'attributes' => [ - 'aria-describedby' => 'describe-convert_to_webp', + 'aria-describedby' => 'describe-convert_to_avif', ], ] ); ?> +
    + + +
    +
    + +
    field_checkbox( [ - 'option_name' => 'display_webp', - 'label' => __( 'Display images in WebP format on the site', 'imagify' ), + 'option_name' => 'display_nextgen', + 'label' => __( 'Display images in Next-Gen format on the site', 'imagify' ), ] ); ?>
    field_radio_list( [ - 'option_name' => 'display_webp_method', + 'option_name' => 'display_nextgen_method', 'values' => [ 'rewrite' => __( 'Use rewrite rules', 'imagify' ), /* translators: 1 and 2 are tag opening and closing. */ @@ -47,7 +53,7 @@
    get_cdn_source(); + $cdn_source = apply_filters( 'imagify_cdn_source_url', '' ); if ( 'option' !== $cdn_source['source'] ) { if ( 'constant' === $cdn_source['source'] ) { @@ -90,7 +96,7 @@
    -
    +
    get_file_path( true ); @@ -130,7 +136,7 @@
    get_cached_stat(); + $count = OptimizedMediaWithoutNextGen::get_instance()->get_cached_stat(); if ( $count ) { ?> @@ -139,18 +145,22 @@ get_stat(); - $total = get_transient( 'imagify_missing_webp_total' ); + $remaining = OptimizedMediaWithoutNextGen::get_instance()->get_stat(); + $total = get_transient( 'imagify_missing_next_gen_total' ); $progress = 0; $aria = ' aria-hidden="true"'; $class = 'hidden'; $style = ''; - if ( false !== $total ) { + if ( + false !== $total + && + $total > 0 + ) { $aria = ''; $class = ''; $processed = $total - $remaining; From 536d89e974be5ac4f34965949152b48dd93d5b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Perona?= Date: Wed, 6 Mar 2024 09:59:38 -0500 Subject: [PATCH 2/9] update changelog for 2.2 --- readme.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/readme.txt b/readme.txt index 7dae8223c..8d34ca67b 100644 --- a/readme.txt +++ b/readme.txt @@ -261,6 +261,9 @@ You can report any security bugs found in the source code of the site-reviews pl 4. Other Media Page == Changelog == += 2.2 = +- New Feature: Introduce AVIF generation feature + = 2.1.3.1 = - Bugfix: missing styling on some banners when using minified versions of the CSS files (#765) From 710489b7587d4ee44ad1d23868c568f1598a5358 Mon Sep 17 00:00:00 2001 From: WordPressFan Date: Thu, 7 Mar 2024 19:09:21 +0200 Subject: [PATCH 3/9] add the code to replace webp settings with nextgen ones --- inc/admin/upgrader.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/inc/admin/upgrader.php b/inc/admin/upgrader.php index 010ac6f09..4c995f9fd 100755 --- a/inc/admin/upgrader.php +++ b/inc/admin/upgrader.php @@ -307,6 +307,11 @@ function _imagify_new_upgrade( $network_version, $site_version ) { if ( version_compare( $site_version, '2.0' ) < 0 ) { Imagify_Options::get_instance()->set( 'optimization_level', 2 ); } + + if ( version_compare( $site_version, '2.2' ) < 0 ) { + Imagify_Options::get_instance()->set( 'display_nextgen', Imagify_Options::get_instance()->get( 'display_webp', 0 ) ); + Imagify_Options::get_instance()->set( 'display_nextgen_method_rewrite', Imagify_Options::get_instance()->get( 'display_webp_method_rewrite', 0 ) ); + } } add_action( 'imagify_upgrade', '_imagify_new_upgrade', 10, 2 ); From a1a283628cbde321b7918289e5bd52fef568ecac Mon Sep 17 00:00:00 2001 From: WordPressFan Date: Thu, 7 Mar 2024 19:15:03 +0200 Subject: [PATCH 4/9] remove default value from get --- inc/admin/upgrader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/admin/upgrader.php b/inc/admin/upgrader.php index 4c995f9fd..5a1f5ed39 100755 --- a/inc/admin/upgrader.php +++ b/inc/admin/upgrader.php @@ -310,7 +310,7 @@ function _imagify_new_upgrade( $network_version, $site_version ) { if ( version_compare( $site_version, '2.2' ) < 0 ) { Imagify_Options::get_instance()->set( 'display_nextgen', Imagify_Options::get_instance()->get( 'display_webp', 0 ) ); - Imagify_Options::get_instance()->set( 'display_nextgen_method_rewrite', Imagify_Options::get_instance()->get( 'display_webp_method_rewrite', 0 ) ); + Imagify_Options::get_instance()->set( 'display_nextgen_method_rewrite', Imagify_Options::get_instance()->get( 'display_webp_method_rewrite' ) ); } } add_action( 'imagify_upgrade', '_imagify_new_upgrade', 10, 2 ); From da020dc6b259d707ec8f38d8aef0584dac81473a Mon Sep 17 00:00:00 2001 From: WordPressFan Date: Fri, 8 Mar 2024 08:06:13 +0200 Subject: [PATCH 5/9] add webp options back to the array --- inc/classes/class-imagify-options.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/inc/classes/class-imagify-options.php b/inc/classes/class-imagify-options.php index 328377625..87bce7bf7 100644 --- a/inc/classes/class-imagify-options.php +++ b/inc/classes/class-imagify-options.php @@ -36,11 +36,14 @@ class Imagify_Options extends Imagify_Abstract_Options { 'resize_larger_w' => 0, 'display_nextgen' => 0, 'display_nextgen_method' => 'picture', + 'display_webp' => 0, + 'display_webp_method' => 'picture', 'cdn_url' => '', 'disallowed-sizes' => [], 'admin_bar_menu' => 0, 'partner_links' => 0, 'convert_to_avif' => 0, + 'convert_to_webp' => 0, ]; /** @@ -131,6 +134,7 @@ public function sanitize_and_validate_value( $key, $value, $default ) { case 'resize_larger': case 'convert_to_webp': case 'display_nextgen': + case 'display_webp': case 'admin_bar_menu': case 'partner_links': case 'convert_to_avif': @@ -160,6 +164,7 @@ public function sanitize_and_validate_value( $key, $value, $default ) { return array_fill_keys( $value, 1 ); case 'display_nextgen_method': + case 'display_webp_method': $values = [ 'picture' => 1, 'rewrite' => 1, From 7fe8a1963640261a6e64acfddf407bc0d8533ad2 Mon Sep 17 00:00:00 2001 From: WordPressFan Date: Fri, 8 Mar 2024 08:07:23 +0200 Subject: [PATCH 6/9] update the plugin's version for testing --- imagify.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imagify.php b/imagify.php index 3a31e6815..25e54174e 100644 --- a/imagify.php +++ b/imagify.php @@ -3,7 +3,7 @@ * Plugin Name: Imagify * Plugin URI: https://wordpress.org/plugins/imagify/ * Description: Dramatically reduce image file sizes without losing quality, make your website load faster, boost your SEO and save money on your bandwidth using Imagify, the new most advanced image optimization tool. - * Version: 2.2 + * Version: 2.2.1 * Requires at least: 5.3 * Requires PHP: 7.0 * Author: Imagify – Optimize Images & Convert WebP & Avif @@ -19,7 +19,7 @@ defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); // Imagify defines. -define( 'IMAGIFY_VERSION', '2.2' ); +define( 'IMAGIFY_VERSION', '2.2.1' ); define( 'IMAGIFY_SLUG', 'imagify' ); define( 'IMAGIFY_FILE', __FILE__ ); define( 'IMAGIFY_PATH', realpath( plugin_dir_path( IMAGIFY_FILE ) ) . '/' ); From e930c49d59721b2ed75daf365041a0a3291246fa Mon Sep 17 00:00:00 2001 From: WordPressFan Date: Fri, 8 Mar 2024 08:14:36 +0200 Subject: [PATCH 7/9] correct the display method option name --- inc/admin/upgrader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/admin/upgrader.php b/inc/admin/upgrader.php index 5a1f5ed39..3ad5eb48c 100755 --- a/inc/admin/upgrader.php +++ b/inc/admin/upgrader.php @@ -310,7 +310,7 @@ function _imagify_new_upgrade( $network_version, $site_version ) { if ( version_compare( $site_version, '2.2' ) < 0 ) { Imagify_Options::get_instance()->set( 'display_nextgen', Imagify_Options::get_instance()->get( 'display_webp', 0 ) ); - Imagify_Options::get_instance()->set( 'display_nextgen_method_rewrite', Imagify_Options::get_instance()->get( 'display_webp_method_rewrite' ) ); + Imagify_Options::get_instance()->set( 'display_nextgen_method', Imagify_Options::get_instance()->get( 'display_webp_method' ) ); } } add_action( 'imagify_upgrade', '_imagify_new_upgrade', 10, 2 ); From a91ab249aa6528c86d1d7dadb729e4b1630481a9 Mon Sep 17 00:00:00 2001 From: WordPressFan Date: Fri, 8 Mar 2024 08:16:13 +0200 Subject: [PATCH 8/9] revert updating the plugin's version number --- imagify.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imagify.php b/imagify.php index 25e54174e..3a31e6815 100644 --- a/imagify.php +++ b/imagify.php @@ -3,7 +3,7 @@ * Plugin Name: Imagify * Plugin URI: https://wordpress.org/plugins/imagify/ * Description: Dramatically reduce image file sizes without losing quality, make your website load faster, boost your SEO and save money on your bandwidth using Imagify, the new most advanced image optimization tool. - * Version: 2.2.1 + * Version: 2.2 * Requires at least: 5.3 * Requires PHP: 7.0 * Author: Imagify – Optimize Images & Convert WebP & Avif @@ -19,7 +19,7 @@ defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); // Imagify defines. -define( 'IMAGIFY_VERSION', '2.2.1' ); +define( 'IMAGIFY_VERSION', '2.2' ); define( 'IMAGIFY_SLUG', 'imagify' ); define( 'IMAGIFY_FILE', __FILE__ ); define( 'IMAGIFY_PATH', realpath( plugin_dir_path( IMAGIFY_FILE ) ) . '/' ); From 478394630ca8c7dd42d5263f097b7d557a51a2b4 Mon Sep 17 00:00:00 2001 From: Mathieu Lamiot Date: Fri, 8 Mar 2024 12:05:36 +0100 Subject: [PATCH 9/9] Update version number --- imagify.php | 4 ++-- readme.txt | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/imagify.php b/imagify.php index 3a31e6815..5174332b3 100644 --- a/imagify.php +++ b/imagify.php @@ -3,7 +3,7 @@ * Plugin Name: Imagify * Plugin URI: https://wordpress.org/plugins/imagify/ * Description: Dramatically reduce image file sizes without losing quality, make your website load faster, boost your SEO and save money on your bandwidth using Imagify, the new most advanced image optimization tool. - * Version: 2.2 + * Version: 2.2.0.1 * Requires at least: 5.3 * Requires PHP: 7.0 * Author: Imagify – Optimize Images & Convert WebP & Avif @@ -19,7 +19,7 @@ defined( 'ABSPATH' ) || die( 'Cheatin’ uh?' ); // Imagify defines. -define( 'IMAGIFY_VERSION', '2.2' ); +define( 'IMAGIFY_VERSION', '2.2.0.1' ); define( 'IMAGIFY_SLUG', 'imagify' ); define( 'IMAGIFY_FILE', __FILE__ ); define( 'IMAGIFY_PATH', realpath( plugin_dir_path( IMAGIFY_FILE ) ) . '/' ); diff --git a/readme.txt b/readme.txt index 8d34ca67b..2bffa2737 100644 --- a/readme.txt +++ b/readme.txt @@ -2,7 +2,7 @@ Contributors: wp_rocket, imagify Tags: optimize images, convert webp, webp converter, convert AVIF, webp to AVIF, AVIF plugin, AVIF converter, image optimization, compress images, image compressor, resize images, reduce image size, performance, image optimizer, core web vitals, best image optimization plugin Tested up to: 6.4 -Stable tag: 2.2 +Stable tag: 2.2.0.1 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -261,6 +261,9 @@ You can report any security bugs found in the source code of the site-reviews pl 4. Other Media Page == Changelog == += 2.2.0.1 = +- Bugfix: Preserve "Display images in webp format" configuration when updating. + = 2.2 = - New Feature: Introduce AVIF generation feature