diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/css/cloudinary.css b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/css/cloudinary.css index 5c14e6684..c55a99c00 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/css/cloudinary.css +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/css/cloudinary.css @@ -1 +1 @@ -@font-face{font-family:cloudinary;src:url(../css/fonts/cloudinary.7b8dc57b5dd3c69880043780bb9bb133.eot);src:url(../css/fonts/cloudinary.7b8dc57b5dd3c69880043780bb9bb133.eot#iefix) format("embedded-opentype"),url(../css/fonts/cloudinary.0bf34bba6c50ef8a00885c94fc39bb81.ttf) format("truetype"),url(../css/fonts/cloudinary.130b5d626b226659422cfb0e20ce30c1.woff) format("woff"),url(../css/cloudinary.svg#cloudinary) format("svg");font-weight:400;font-style:normal}.dashicons-cloudinary{speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.dashicons-cloudinary:before{font-family:cloudinary!important;content:"\e900"}.dashicons-cloudinary.success{color:#558b2f}.dashicons-cloudinary.error{color:#dd2c00}.dashicons-cloudinary.error:before{content:"\e901"}.dashicons-cloudinary.warning{color:#fd9d2c}.dashicons-cloudinary.warning:before{content:"\e902"}.dashicons-cloudinary.info{color:#0071ba}.dashicons-cloudinary.downloading:before{content:"\e903"}.dashicons-cloudinary.syncing:before{content:"\e904"}.column-cld_status{width:5.5em}.column-cld_status .dashicons-cloudinary{display:inline-block}.column-cld_status .dashicons-cloudinary:before{font-size:1.8rem}.form-field .error-notice,.form-table .error-notice{display:none;color:#dd2c00}.form-field input.cld-field:invalid,.form-table input.cld-field:invalid{border-color:#dd2c00}.form-field input.cld-field:invalid+.error-notice,.form-table input.cld-field:invalid+.error-notice{display:inline-block}.cloudinary-welcome{background-image:url(../css/logo.svg);background-repeat:no-repeat;background-size:153px;background-position:top 12px right 20px}.cloudinary-stats{display:inline-block;margin-left:25px}.cloudinary-stat{cursor:help}.cloudinary-percent{font-size:.8em;vertical-align:top;color:#0071ba}.settings-image{max-width:100%;padding-top:5px}.settings-tabs>li{display:inline-block}.settings-tabs>li a{padding:.6em}.settings-tabs>li a.active{background-color:#fff}.settings-tab-section{padding:20px 0 0;max-width:1030px;position:relative}.settings-tab-section.cloudinary-welcome .settings-tab-section-fields-dashboard{display:flex;align-items:flex-start;align-content:flex-start;margin-top:40px}.settings-tab-section.cloudinary-welcome .settings-tab-section-fields-dashboard-description{width:55%;margin:0 auto 0 0}.settings-tab-section.cloudinary-welcome .settings-tab-section-fields-dashboard-content{width:35%;margin:0 auto}.settings-tab-section.cloudinary-welcome .settings-tab-section-fields-dashboard-content .dashicons{color:#9ea3a8}.settings-tab-section.cloudinary-welcome .settings-tab-section-card{margin-top:0}.settings-tab-section-fields .field-heading th{display:block;width:auto;color:#23282d;font-size:1.1em;margin:1em 0}.settings-tab-section-fields .field-heading td{display:none;visibility:hidden}.settings-tab-section-fields .regular-textarea{width:100%;height:60px}.settings-tab-section-fields .dashicons{text-decoration:none;vertical-align:middle}.settings-tab-section-fields a .dashicons{color:#5f5f5f}.settings-tab-section-fields-dashboard-error{font-size:1.2em;color:#5f5f5f}.settings-tab-section-fields-dashboard-error .dashicons{color:#ac0000}.settings-tab-section-fields-dashboard-error .button{font-size:1.1em;height:40px;line-height:40px;padding-right:40px;padding-left:40px}.settings-tab-section-fields-dashboard-success{font-size:1.2em;color:#23282d}.settings-tab-section-fields-dashboard-success.expanded{padding-top:40px}.settings-tab-section-fields-dashboard-success .dashicons{color:#4fb651}.settings-tab-section-fields-dashboard-success .button{font-size:1.1em;height:40px;line-height:40px;padding-right:40px;padding-left:40px}.settings-tab-section-fields-dashboard-success .description{color:#5f5f5f;font-weight:400;margin-top:12px}.settings-tab-section-card{box-sizing:border-box;border:1px solid #e5e5e5;background-color:#fff;box-shadow:0 1px 1px 0 rgba(0,0,0,.07);padding:20px 23px;margin-top:12px}.settings-tab-section-card .dashicons{font-size:1.4em}.settings-tab-section-card h2{font-size:1.8em;font-weight:400;margin-top:0}.settings-tab-section-card.pull-right{width:450px;padding:12px;float:right;position:relative;z-index:10}.settings-tab-section-card.pull-right img.settings-image{box-shadow:0 2px 4px 0 rgba(0,0,0,.5);border:1px solid #979797;margin-top:12px}.settings-tab-section-card.pull-right h3,.settings-tab-section-card.pull-right h4{margin-top:0}.settings-tab-section .field-row-cloudinary_url,.settings-tab-section .field-row-signup{display:block}.settings-tab-section .field-row-cloudinary_url td,.settings-tab-section .field-row-cloudinary_url th,.settings-tab-section .field-row-signup td,.settings-tab-section .field-row-signup th{display:block;width:auto;padding:10px 0 0}.settings-tab-section .field-row-cloudinary_url td .sign-up,.settings-tab-section .field-row-cloudinary_url th .sign-up,.settings-tab-section .field-row-signup td .sign-up,.settings-tab-section .field-row-signup th .sign-up{vertical-align:baseline}.settings-tab-section.connect .form-table{display:inline-block;width:auto;max-width:580px}.settings-valid{color:#558b2f;font-size:30px}.settings-valid-field{border-color:#558b2f!important}.settings-invalid-field{border-color:#dd2c00!important}.settings-warning{display:inline-block;padding:5px 7px;background-color:#e9faff;border:1px solid #ccd0d4;border-left:4px solid #00a0d2;box-shadow:0 1px 1px rgba(0,0,0,.04)}.sync .spinner{display:inline-block;visibility:visible;float:none;margin:0 5px 0 0}.sync-media,.sync-media-progress{display:none}.sync-media-progress-outer{height:20px;margin:20px 0 10px;width:500px;background-color:#e5e5e5;position:relative}.sync-media-progress-outer .progress-bar{width:0;height:20px;background-color:#558b2f;transition:width .25s}.sync-media-progress-notice{color:#dd2c00}.sync-media-resource{width:100px;display:inline-block}.sync-media-error{color:#dd2c00}.sync-count{font-weight:700}.sync-details{margin-top:10px}.sync .button.start-sync,.sync .button.stop-sync{display:none;padding:0 16px}.sync .button.start-sync .dashicons,.sync .button.stop-sync .dashicons{line-height:2.2em}.sync .progress-text{padding:12px 4px 12px 12px;display:inline-block;font-weight:700}.sync .completed{max-width:300px;display:none}.sync-status-disabled{color:#dd2c00}.sync-status-enabled{color:#558b2f}.sync-status-button.button{vertical-align:baseline}.cloudinary-widget{height:100%}.cloudinary-widget-wrapper{height:100%;overflow:hidden;background-image:url("data:image/svg+xml;base64,PHN2ZyBjbGFzcz0ic3Bpbm5lciIgdmlld0JveD0iLTQgLTQgMTUxIDEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48c3R5bGU+QGtleWZyYW1lcyBjb2xvcnN7MCUsdG97c3Ryb2tlOiMwMDc4ZmZ9NTAle3N0cm9rZTojMGUyZjVhfX1Aa2V5ZnJhbWVzIGRhc2h7MCUsdG97c3Ryb2tlLWRhc2hvZmZzZXQ6NTYwfTUwJXtzdHJva2UtZGFzaG9mZnNldDowfX1ALXdlYmtpdC1rZXlmcmFtZXMgY29sb3JzezAlLHRve3N0cm9rZTojMDA3OGZmfTUwJXtzdHJva2U6IzBlMmY1YX19QC13ZWJraXQta2V5ZnJhbWVzIGRhc2h7MCUsdG97c3Ryb2tlLWRhc2hvZmZzZXQ6NTYwfTUwJXtzdHJva2UtZGFzaG9mZnNldDowfX08L3N0eWxlPjxwYXRoIGQ9Ik0xMjEuNjYzIDkwLjYzOGMtMS43OTYgMC05OS4zMy0uNDk4LTEwMS40NzQtMS40NzhDOC42ODUgODMuODc3IDEuMjUgNzIuMTk2IDEuMjUgNTkuMzk2YzAtMTYuNjU2IDEyLjc5Ny0zMC42MSAyOS4wNTItMzIuMzIzIDcuNDktMTUuNzA2IDIzLjE4Ni0yNS43MDcgNDAuNzE0LTI1LjcwNyAyMC45OCAwIDM5LjIxNSAxNC43NTIgNDMuOTQ1IDM0LjkwNyAxNS4wOS4yNDUgMjcuMjkgMTIuNjMgMjcuMjkgMjcuODIyIDAgMTEuOTY4LTcuNzM4IDIyLjU1LTE5LjI1NiAyNi4zMyIgc3Ryb2tlLXdpZHRoPSI5IiBzdHJva2UtbGluZWNhcD0icm91bmQiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3R5bGU9InRyYW5zZm9ybS1vcmlnaW46Y2VudGVyOy13ZWJraXQtYW5pbWF0aW9uOmRhc2ggMnMgZWFzZS1pbi1vdXQgaW5maW5pdGUsY29sb3JzIDhzIGVhc2UtaW4tb3V0IGluZmluaXRlO2FuaW1hdGlvbjpkYXNoIDJzIGVhc2UtaW4tb3V0IGluZmluaXRlLGNvbG9ycyA4cyBlYXNlLWluLW91dCBpbmZpbml0ZSIgc3Ryb2tlLWRhc2hhcnJheT0iMjgwIi8+PC9zdmc+");background-repeat:no-repeat;background-position:50%;background-size:150px}.attachment-actions .button.edit-attachment,.attachment-info .edit-attachment{display:none}.global-transformations-preview{position:relative;max-width:600px}.global-transformations-spinner{display:none}.global-transformations-button.button-primary{display:none;position:absolute;z-index:100}.global-transformations-url{margin-bottom:5px;margin-top:5px}.global-transformations-url-transformation{max-width:100px;overflow:hidden;text-overflow:ellipsis;color:#51a3ff}.global-transformations-url-file{color:#f2d864}.global-transformations-url-link{display:block;padding:16px;background-color:#262c35;text-decoration:none;color:#fff;border-radius:6px;overflow:hidden;text-overflow:ellipsis}.global-transformations-url-link:hover{color:#888;text-decoration:underline}.cld-tax-order-list-item{border:1px solid #efefef;padding:4px;margin:0 0 -1px;background-color:#fff}.cld-tax-order-list-item.no-items{color:#888;text-align:center;display:none}.cld-tax-order-list-item.no-items:last-child{display:block}.cld-tax-order-list-item.ui-sortable-helper{box-shadow:0 2px 5px rgba(0,0,0,.2)}.cld-tax-order-list-item-placeholder{background-color:#efefef;height:45px;margin:0}.cld-tax-order-list-item-handle{cursor:grab;margin-right:4px;color:#999}.cld-tax-order-list-type{width:45%;display:inline-block;margin-right:8px}.cld-tax-order-list-type input{margin-right:4px!important}.cloudinary-media-library{position:relative;margin-left:-20px}@media screen and (max-width:782px){.cloudinary-media-library{margin-left:-10px}} \ No newline at end of file +@font-face{font-family:cloudinary;src:url(../css/fonts/cloudinary.7b8dc57b5dd3c69880043780bb9bb133.eot);src:url(../css/fonts/cloudinary.7b8dc57b5dd3c69880043780bb9bb133.eot#iefix) format("embedded-opentype"),url(../css/fonts/cloudinary.0bf34bba6c50ef8a00885c94fc39bb81.ttf) format("truetype"),url(../css/fonts/cloudinary.130b5d626b226659422cfb0e20ce30c1.woff) format("woff"),url(../css/cloudinary.svg#cloudinary) format("svg");font-weight:400;font-style:normal}.dashicons-cloudinary{speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.dashicons-cloudinary:before{font-family:cloudinary!important;content:"\e900"}.dashicons-cloudinary.success{color:#558b2f}.dashicons-cloudinary.error{color:#dd2c00}.dashicons-cloudinary.error:before{content:"\e901"}.dashicons-cloudinary.uploading{color:#fd9d2c}.dashicons-cloudinary.uploading:before{content:"\e902"}.dashicons-cloudinary.info{color:#0071ba}.dashicons-cloudinary.downloading:before{content:"\e903"}.dashicons-cloudinary.syncing:before{content:"\e904"}.column-cld_status{width:5.5em}.column-cld_status .dashicons-cloudinary{display:inline-block}.column-cld_status .dashicons-cloudinary:before{font-size:1.8rem}.form-field .error-notice,.form-table .error-notice{display:none;color:#dd2c00}.form-field input.cld-field:invalid,.form-table input.cld-field:invalid{border-color:#dd2c00}.form-field input.cld-field:invalid+.error-notice,.form-table input.cld-field:invalid+.error-notice{display:inline-block}.cloudinary-welcome{background-image:url(../css/logo.svg);background-repeat:no-repeat;background-size:153px;background-position:top 12px right 20px}.cloudinary-stats{display:inline-block;margin-left:25px}.cloudinary-stat{cursor:help}.cloudinary-percent{font-size:.8em;vertical-align:top;color:#0071ba}.settings-image{max-width:100%;padding-top:5px}.settings-tabs>li{display:inline-block}.settings-tabs>li a{padding:.6em}.settings-tabs>li a.active{background-color:#fff}.settings-tab-section{padding:20px 0 0;max-width:1030px;position:relative}.settings-tab-section.cloudinary-welcome .settings-tab-section-fields-dashboard{display:flex;align-items:flex-start;align-content:flex-start;margin-top:40px}.settings-tab-section.cloudinary-welcome .settings-tab-section-fields-dashboard-description{width:55%;margin:0 auto 0 0}.settings-tab-section.cloudinary-welcome .settings-tab-section-fields-dashboard-content{width:35%;margin:0 auto}.settings-tab-section.cloudinary-welcome .settings-tab-section-fields-dashboard-content .dashicons{color:#9ea3a8}.settings-tab-section.cloudinary-welcome .settings-tab-section-card{margin-top:0}.settings-tab-section-fields .field-heading th{display:block;width:auto;color:#23282d;font-size:1.1em;margin:1em 0}.settings-tab-section-fields .field-heading td{display:none;visibility:hidden}.settings-tab-section-fields .regular-textarea{width:100%;height:60px}.settings-tab-section-fields .dashicons{text-decoration:none;vertical-align:middle}.settings-tab-section-fields a .dashicons{color:#5f5f5f}.settings-tab-section-fields-dashboard-error{font-size:1.2em;color:#5f5f5f}.settings-tab-section-fields-dashboard-error .dashicons{color:#ac0000}.settings-tab-section-fields-dashboard-error .button{font-size:1.1em;height:40px;line-height:40px;padding-right:40px;padding-left:40px}.settings-tab-section-fields-dashboard-success{font-size:1.2em;color:#23282d}.settings-tab-section-fields-dashboard-success.expanded{padding-top:40px}.settings-tab-section-fields-dashboard-success .dashicons{color:#4fb651}.settings-tab-section-fields-dashboard-success .button{font-size:1.1em;height:40px;line-height:40px;padding-right:40px;padding-left:40px}.settings-tab-section-fields-dashboard-success .description{color:#5f5f5f;font-weight:400;margin-top:12px}.settings-tab-section-card{box-sizing:border-box;border:1px solid #e5e5e5;background-color:#fff;box-shadow:0 1px 1px 0 rgba(0,0,0,.07);padding:20px 23px;margin-top:12px}.settings-tab-section-card .dashicons{font-size:1.4em}.settings-tab-section-card h2{font-size:1.8em;font-weight:400;margin-top:0}.settings-tab-section-card.pull-right{width:450px;padding:12px;float:right;position:relative;z-index:10}.settings-tab-section-card.pull-right img.settings-image{box-shadow:0 2px 4px 0 rgba(0,0,0,.5);border:1px solid #979797;margin-top:12px}.settings-tab-section-card.pull-right h3,.settings-tab-section-card.pull-right h4{margin-top:0}.settings-tab-section .field-row-cloudinary_url,.settings-tab-section .field-row-signup{display:block}.settings-tab-section .field-row-cloudinary_url td,.settings-tab-section .field-row-cloudinary_url th,.settings-tab-section .field-row-signup td,.settings-tab-section .field-row-signup th{display:block;width:auto;padding:10px 0 0}.settings-tab-section .field-row-cloudinary_url td .sign-up,.settings-tab-section .field-row-cloudinary_url th .sign-up,.settings-tab-section .field-row-signup td .sign-up,.settings-tab-section .field-row-signup th .sign-up{vertical-align:baseline}.settings-tab-section.connect .form-table{display:inline-block;width:auto;max-width:580px}.settings-valid{color:#558b2f;font-size:30px}.settings-valid-field{border-color:#558b2f!important}.settings-invalid-field{border-color:#dd2c00!important}.settings-warning{display:inline-block;padding:5px 7px;background-color:#e9faff;border:1px solid #ccd0d4;border-left:4px solid #00a0d2;box-shadow:0 1px 1px rgba(0,0,0,.04)}.sync .spinner{display:inline-block;visibility:visible;float:none;margin:0 5px 0 0}.sync-media,.sync-media-progress{display:none}.sync-media-progress-outer{height:20px;margin:20px 0 10px;width:500px;background-color:#e5e5e5;position:relative}.sync-media-progress-outer .progress-bar{width:0;height:20px;background-color:#558b2f;transition:width .25s}.sync-media-progress-notice{color:#dd2c00}.sync-media-resource{width:100px;display:inline-block}.sync-media-error{color:#dd2c00}.sync-count{font-weight:700}.sync-details{margin-top:10px}.sync .button.start-sync,.sync .button.stop-sync{display:none;padding:0 16px}.sync .button.start-sync .dashicons,.sync .button.stop-sync .dashicons{line-height:2.2em}.sync .progress-text{padding:12px 4px 12px 12px;display:inline-block;font-weight:700}.sync .completed{max-width:300px;display:none}.sync-status-disabled{color:#dd2c00}.sync-status-enabled{color:#558b2f}.sync-status-button.button{vertical-align:baseline}.cloudinary-widget{height:100%}.cloudinary-widget-wrapper{height:100%;overflow:hidden;background-image:url("data:image/svg+xml;base64,PHN2ZyBjbGFzcz0ic3Bpbm5lciIgdmlld0JveD0iLTQgLTQgMTUxIDEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48c3R5bGU+QGtleWZyYW1lcyBjb2xvcnN7MCUsdG97c3Ryb2tlOiMwMDc4ZmZ9NTAle3N0cm9rZTojMGUyZjVhfX1Aa2V5ZnJhbWVzIGRhc2h7MCUsdG97c3Ryb2tlLWRhc2hvZmZzZXQ6NTYwfTUwJXtzdHJva2UtZGFzaG9mZnNldDowfX1ALXdlYmtpdC1rZXlmcmFtZXMgY29sb3JzezAlLHRve3N0cm9rZTojMDA3OGZmfTUwJXtzdHJva2U6IzBlMmY1YX19QC13ZWJraXQta2V5ZnJhbWVzIGRhc2h7MCUsdG97c3Ryb2tlLWRhc2hvZmZzZXQ6NTYwfTUwJXtzdHJva2UtZGFzaG9mZnNldDowfX08L3N0eWxlPjxwYXRoIGQ9Ik0xMjEuNjYzIDkwLjYzOGMtMS43OTYgMC05OS4zMy0uNDk4LTEwMS40NzQtMS40NzhDOC42ODUgODMuODc3IDEuMjUgNzIuMTk2IDEuMjUgNTkuMzk2YzAtMTYuNjU2IDEyLjc5Ny0zMC42MSAyOS4wNTItMzIuMzIzIDcuNDktMTUuNzA2IDIzLjE4Ni0yNS43MDcgNDAuNzE0LTI1LjcwNyAyMC45OCAwIDM5LjIxNSAxNC43NTIgNDMuOTQ1IDM0LjkwNyAxNS4wOS4yNDUgMjcuMjkgMTIuNjMgMjcuMjkgMjcuODIyIDAgMTEuOTY4LTcuNzM4IDIyLjU1LTE5LjI1NiAyNi4zMyIgc3Ryb2tlLXdpZHRoPSI5IiBzdHJva2UtbGluZWNhcD0icm91bmQiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3R5bGU9InRyYW5zZm9ybS1vcmlnaW46Y2VudGVyOy13ZWJraXQtYW5pbWF0aW9uOmRhc2ggMnMgZWFzZS1pbi1vdXQgaW5maW5pdGUsY29sb3JzIDhzIGVhc2UtaW4tb3V0IGluZmluaXRlO2FuaW1hdGlvbjpkYXNoIDJzIGVhc2UtaW4tb3V0IGluZmluaXRlLGNvbG9ycyA4cyBlYXNlLWluLW91dCBpbmZpbml0ZSIgc3Ryb2tlLWRhc2hhcnJheT0iMjgwIi8+PC9zdmc+");background-repeat:no-repeat;background-position:50%;background-size:150px}.attachment-actions .button.edit-attachment,.attachment-info .edit-attachment{display:none}.global-transformations-preview{position:relative;max-width:600px}.global-transformations-spinner{display:none}.global-transformations-button.button-primary{display:none;position:absolute;z-index:100}.global-transformations-url{margin-bottom:5px;margin-top:5px}.global-transformations-url-transformation{max-width:100px;overflow:hidden;text-overflow:ellipsis;color:#51a3ff}.global-transformations-url-file{color:#f2d864}.global-transformations-url-link{display:block;padding:16px;background-color:#262c35;text-decoration:none;color:#fff;border-radius:6px;overflow:hidden;text-overflow:ellipsis}.global-transformations-url-link:hover{color:#888;text-decoration:underline}.cld-tax-order-list-item{border:1px solid #efefef;padding:4px;margin:0 0 -1px;background-color:#fff}.cld-tax-order-list-item.no-items{color:#888;text-align:center;display:none}.cld-tax-order-list-item.no-items:last-child{display:block}.cld-tax-order-list-item.ui-sortable-helper{box-shadow:0 2px 5px rgba(0,0,0,.2)}.cld-tax-order-list-item-placeholder{background-color:#efefef;height:45px;margin:0}.cld-tax-order-list-item-handle{cursor:grab;margin-right:4px;color:#999}.cld-tax-order-list-type{width:45%;display:inline-block;margin-right:8px}.cld-tax-order-list-type input{margin-right:4px!important}.cloudinary-media-library{position:relative;margin-left:-20px}@media screen and (max-width:782px){.cloudinary-media-library{margin-left:-10px}} \ No newline at end of file diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/css/src/components/_brand.scss b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/css/src/components/_brand.scss index caecd2e68..940b50532 100755 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/css/src/components/_brand.scss +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/css/src/components/_brand.scss @@ -40,7 +40,7 @@ } } - &.warning { + &.uploading { color : $color-orange; &:before { diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-connect.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-connect.php index 9dcf3a68a..32d01eda1 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-connect.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-connect.php @@ -256,6 +256,15 @@ public function get_credentials() { return $this->credentials; } + /** + * Get the cloud name if set. + * + * @return string|null + */ + public function get_cloud_name() { + return $this->credentials['cloud_name'] ? $this->credentials['cloud_name'] : null; + } + /** * Set the config credentials from an array. * @@ -364,7 +373,7 @@ public function get_usage_stat( $type, $stat = null ) { $value = $this->usage[ $type ]['usage']; } elseif ( 'used_percent' === $stat && isset( $this->usage[ $type ]['credits_usage'] ) ) { // Calculate percentage based on credit limit and usage. - $value = round( $this->usage[ $type ]['credits_usage']/$this->usage['credits']['limit'] * 100, 2 ); + $value = round( $this->usage[ $type ]['credits_usage'] / $this->usage['credits']['limit'] * 100, 2 ); } } } diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-media.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-media.php index 2ffd89d18..b1349f763 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-media.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-media.php @@ -8,6 +8,7 @@ namespace Cloudinary; use Cloudinary\Component\Setup; +use Cloudinary\Connect\Api; use Cloudinary\Media\Filter; use Cloudinary\Media\Upgrade; use Cloudinary\Media\Global_Transformations; @@ -89,6 +90,13 @@ class Media implements Setup { */ public $video; + /** + * Sync instance. + * + * @var \Cloudinary\Sync + */ + public $sync; + /** * Flag if in image_downsize function to prevent overload. * @@ -115,23 +123,42 @@ public function __construct( Plugin $plugin ) { /** * Check if the attachment is a media file. * - * @param int|\WP_Post $attachment The attachment to check. + * @param int $attachment_id The attachment ID to check. * * @return bool */ - public function is_media( $attachment ) { + public function is_media( $attachment_id ) { $is_media = false; - if ( 'attachment' === get_post_type( $attachment ) ) { - $is_media = $this->get_post_meta( $attachment, 'is_media', true ); - if ( '' === $is_media ) { - $is_media = wp_attachment_is( 'image', $attachment ) || wp_attachment_is( 'video', $attachment ); - $this->update_post_meta( $attachment, 'is_media', $is_media ); - } + if ( 'attachment' === get_post_type( $attachment_id ) && wp_get_attachment_metadata( $attachment_id ) ) { + /** + * Filter the default Cloudinary Media Types. + * + * @param array $types The default media types array. + * + * @return array + */ + $media_types = apply_filters( 'cloudinary_media_types', array( 'image', 'video', 'audio' ) ); + $type = $this->get_media_type( $attachment_id ); + $is_media = in_array( $type, $media_types ); } return $is_media; } + /** + * Get a resource type based on file.(Cloudinary v1 remove mime type in post data). + * + * @param \WP_Post|int $attachment_id The attachment ID or object. + * + * @return string + */ + public function get_media_type( $attachment_id ) { + $file = pathinfo( get_attached_file( $attachment_id ), PATHINFO_BASENAME ); + $mime = wp_check_filetype( $file ); + + return strstr( $mime['type'], '/', true ); + } + /** * Remove the crop size from a url. * @@ -465,9 +492,14 @@ public function get_transformations_from_string( $str, $type = 'image' ) { public function attachment_url( $url, $attachment_id ) { if ( ! doing_action( 'wp_insert_post_data' ) && false === $this->in_downsize ) { $cloudinary_id = $this->cloudinary_id( $attachment_id ); - if ( false !== $cloudinary_id ) { + if ( $cloudinary_id ) { $url = $this->cloudinary_url( $attachment_id ); } + // Previous v1. + $previous_url = strpos( $url, untrailingslashit( $this->base_url ) ); + if ( false !== $previous_url ) { + $url = substr( $url, $previous_url ); + } } return $url; @@ -553,17 +585,17 @@ public function apply_default_transformations( array $transformations, $type = ' */ public function cloudinary_url( $attachment_id, $size = array(), $transformations = array(), $cloudinary_id = null, $overwrite_transformations = false, $clean = false ) { - if ( empty( $cloudinary_id ) ) { + if ( ! ( $cloudinary_id ) ) { $cloudinary_id = $this->cloudinary_id( $attachment_id ); - } - if ( false === $cloudinary_id ) { - return false; + if ( ! $cloudinary_id ) { + return null; + } } if ( empty( $transformations ) ) { $transformations = $this->get_transformation_from_meta( $attachment_id ); } // Get the attachment resource type. - $resource_type = wp_attachment_is( 'image', $attachment_id ) ? 'image' : 'video'; + $resource_type = $this->get_media_type( $attachment_id ); // Setup initial args for cloudinary_url. $pre_args = array( 'secure' => is_ssl(), @@ -617,48 +649,80 @@ public function cloudinary_url( $attachment_id, $size = array(), $transformation * @return array Altered array of paths. */ public function upload_dir( $dirs ) { - $folder = $this->cloudinary_folder; - $dirs['cloudinary_folder'] = trailingslashit( $folder ); + + $dirs['cloudinary_folder'] = $this->get_cloudinary_folder(); return $dirs; } + /** + * Get the setup Cloudinary Folder. + * + * @return string + */ + public function get_cloudinary_folder() { + $folder = ''; + if ( ! empty( $this->cloudinary_folder ) ) { + $folder = trailingslashit( $this->cloudinary_folder ); + } + + return $folder; + } + /** * Get a public ID. * - * @param int $attachment_id The Attachment ID. + * @param int $attachment_id The Attachment ID. + * @param bool $suffixed Flag to get suffixed version of ID. * - * @return string A cloudinary public id. + * @return string */ - public function get_public_id( $attachment_id ) { + public function get_public_id( $attachment_id, $suffixed = false ) { // Check for a public_id. - $public_id = $this->get_post_meta( $attachment_id, Sync::META_KEYS['public_id'], true ); - if ( empty( $public_id ) ) { - // No public_id is an up-sync (WP->CLD). - // Build a public_id based on cloudinary folder, and filename. - $file = get_attached_file( $attachment_id ); - $info = pathinfo( $file ); - $cloudinary_folder = trailingslashit( $this->cloudinary_folder ); - $public_id = $cloudinary_folder . $info['filename']; + if ( $this->has_public_id( $attachment_id ) ) { + $public_id = $this->get_post_meta( $attachment_id, Sync::META_KEYS['public_id'], true ); + if ( $this->is_folder_synced( $attachment_id ) ) { + $public_id = $this->get_cloudinary_folder() . pathinfo( $public_id, PATHINFO_BASENAME ); + } + if ( true === $suffixed ) { + $public_id .= $this->get_post_meta( $attachment_id, Sync::META_KEYS['suffix'], true ); + } + } else { + $public_id = $this->sync->generate_public_id( $attachment_id ); } return $public_id; } /** - * Get a Cloudinary ID. + * Check if an attachment has a public ID. * * @param int $attachment_id The Attachment ID. * - * @return string A cloudinary id. + * @return bool + */ + public function has_public_id( $attachment_id ) { + return ! empty( $this->get_post_meta( $attachment_id, Sync::META_KEYS['public_id'], true ) ); + } + + /** + * Get a Cloudinary ID which includes the file format extension. + * + * @param int $attachment_id The Attachment ID. + * + * @return string|null */ public function get_cloudinary_id( $attachment_id ) { + $cloudinary_id = null; // A cloudinary_id is a public_id with a file extension. - $public_id = $this->get_public_id( $attachment_id ); - $file = get_attached_file( $attachment_id ); - $info = pathinfo( $file ); - $cloudinary_id = $public_id . '.' . $info['extension']; + if ( $this->has_public_id( $attachment_id ) ) { + $public_id = $this->get_public_id( $attachment_id, true ); + $file = get_attached_file( $attachment_id ); + // @todo: Make this use the globals, overrides, and application conversion. + $extension = pathinfo( $file, PATHINFO_EXTENSION ); + $cloudinary_id = $public_id . '.' . $extension; + } return $cloudinary_id; } @@ -668,22 +732,28 @@ public function get_cloudinary_id( $attachment_id ) { * * @param int $attachment_id The ID to get Cloudinary id for. * - * @return string|bool the ID or false if not existing. + * @return string|null the ID or null if not existing. */ public function cloudinary_id( $attachment_id ) { if ( ! $this->is_media( $attachment_id ) ) { - return false; + return null; } // Return cached ID if we've already gotten it before. - if ( ! empty( $this->cloudinary_ids[ $attachment_id ] ) ) { + if ( isset( $this->cloudinary_ids[ $attachment_id ] ) ) { return $this->cloudinary_ids[ $attachment_id ]; } - $cloudinary_id = false; - if ( $this->plugin->components['sync']->is_synced( $attachment_id ) ) { - $cloudinary_id = $this->get_cloudinary_id( $attachment_id ); + if ( ! $this->sync->is_synced( $attachment_id ) && ! defined( 'REST_REQUEST' ) ) { + $sync_type = $this->sync->maybe_prepare_sync( $attachment_id ); + // Check sync type allows for continued rendering. i.e meta update, breakpoints etc, will still allow the URL to work, + // Where is type "file" will not since it's still being uploaded. + if ( ! is_null( $sync_type ) && $this->sync->is_required( $sync_type ) ) { + return null; // Return and render local URLs. + } } + $cloudinary_id = $this->get_cloudinary_id( $attachment_id ); + /** * Filter to validate the Cloudinary ID to allow extending it's availability. * @@ -702,9 +772,7 @@ public function cloudinary_id( $attachment_id ) { */ do_action( 'cloudinary_id', $cloudinary_id, $attachment_id ); // Cache ID to prevent multiple lookups. - if ( false !== $cloudinary_id ) { - $this->cloudinary_ids[ $attachment_id ] = $cloudinary_id; - } + $this->cloudinary_ids[ $attachment_id ] = $cloudinary_id; return $cloudinary_id; } @@ -728,7 +796,7 @@ public function filter_downsize( $image, $attachment_id, $size ) { $cloudinary_id = $this->cloudinary_id( $attachment_id ); - if ( false !== $cloudinary_id ) { + if ( $cloudinary_id ) { $this->in_downsize = true; $intermediate = image_get_intermediate_size( $attachment_id, $size ); if ( is_array( $intermediate ) ) { @@ -779,7 +847,7 @@ public function convert_url( $url, $attachment_id, $transformations = array(), $ */ public function image_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) { $cloudinary_id = $this->cloudinary_id( $attachment_id ); - if ( false === $cloudinary_id ) { + if ( ! $cloudinary_id ) { return $sources; // Return WordPress default sources. } // Get transformations from URL. @@ -960,6 +1028,8 @@ private function create_attachment( $asset, $public_id ) { $sync_key = $public_id; // Capture public_id. Use core update_post_meta since this attachment data doesnt exist yet. update_post_meta( $attachment_id, Sync::META_KEYS['public_id'], $public_id ); + // Capture version number. + update_post_meta( $attachment_id, Sync::META_KEYS['version'], $asset['version'] ); if ( ! empty( $asset['transformations'] ) ) { // Save a combined key. $sync_key .= wp_json_encode( $asset['transformations'] ); @@ -1027,7 +1097,7 @@ public function down_sync_asset() { } $transformations = $this->get_transformations_from_string( $url ); if ( ! empty( $transformations ) ) { - $sync_key .= wp_json_encode( $transformations ); + $sync_key .= wp_json_encode( $transformations ); $asset['transformations'] = $transformations; } // Check Format and url extension. @@ -1097,7 +1167,7 @@ public function media_column_value( $column_name, $attachment_id ) { 'note' => esc_html__( 'Not Synced', 'cloudinary' ), ); add_filter( 'cloudinary_flag_sync', '__return_true' ); - if ( false === $this->cloudinary_id( $attachment_id ) ) { + if ( ! $this->cloudinary_id( $attachment_id ) ) { // If false, lets check why by seeing if the file size is too large. $file = get_attached_file( $attachment_id ); // Get the file size to make sure it can exist in cloudinary. $max_size = ( wp_attachment_is_image( $attachment_id ) ? 'image_max_size_bytes' : 'video_max_size_bytes' ); @@ -1209,29 +1279,6 @@ public function get_transformation_from_meta( $post_id ) { return $transformations; } - /** - * Add a full Cloudinary full size with stored file name to the sizes array. - * - * @param array $data Image meta data array. - * @param int $post_id Attachment post ID. - * - * @return mixed - */ - public function add_cloudinary_full_size( $data, $post_id ) { - // Add a full Cloudinary filename size. - if ( ! empty( $data['file'] ) && ! empty( $data['sizes'] ) ) { - $info = pathinfo( $data['file'] ); - $cld_file = $this->get_public_id( $post_id ) . '.' . $info['extension']; - $data['sizes']['_cld_full'] = array( - 'file' => basename( $cld_file ), - 'width' => $data['width'], - 'height' => $data['height'], - ); - } - - return $data; - } - /** * Get Cloudinary related Post meta. * @@ -1286,13 +1333,16 @@ public function build_cached_meta( $post_id, $key, $single ) { */ public function update_post_meta( $post_id, $key, $data ) { $meta_data = wp_get_attachment_metadata( $post_id, true ); - if ( is_array( $meta_data ) && isset( $meta_data[ Sync::META_KEYS['cloudinary'] ] ) && is_array( $meta_data[ Sync::META_KEYS['cloudinary'] ] ) ) { - // Only do this side if has been set before. + if ( is_array( $meta_data ) ) { + if ( ! isset( $meta_data[ Sync::META_KEYS['cloudinary'] ] ) ) { + $meta_data[ Sync::META_KEYS['cloudinary'] ] = array(); + } $meta_data[ Sync::META_KEYS['cloudinary'] ][ $key ] = $data; wp_update_attachment_metadata( $post_id, $meta_data ); + } else { + // Update core mete data for consistency. + update_post_meta( $post_id, $key, $data ); } - // Update core mete data for consistency. - update_post_meta( $post_id, $key, $data ); } /** @@ -1312,15 +1362,156 @@ public function delete_post_meta( $post_id, $key ) { delete_post_meta( $post_id, $key ); } + /** + * Get the breakpoint generation options for an attachment. + * + * @param int $attachment_id The attachment ID. + * + * @return array + */ + public function get_breakpoint_options( $attachment_id ) { + // Add breakpoints if we have an image. + $breakpoints = array(); + $settings = $this->plugin->config['settings']['global_transformations']; + + if ( 'off' !== $settings['enable_breakpoints'] && wp_attachment_is_image( $attachment_id ) ) { + $meta = wp_get_attachment_metadata( $attachment_id ); + // Get meta image size if non exists. + if ( empty( $meta ) ) { + $meta = array(); + $imagesize = getimagesize( get_attached_file( $attachment_id ) ); + $meta['width'] = $imagesize[0] ? $imagesize[0] : 0; + } + $max_width = $this->get_max_width(); + // Add breakpoints request options. + $breakpoint_options = array( + 'create_derived' => true, + 'bytes_step' => $settings['bytes_step'], + 'max_images' => $settings['breakpoints'], + 'max_width' => $meta['width'] < $max_width ? $meta['width'] : $max_width, + 'min_width' => $settings['min_width'], + ); + $transformations = $this->get_transformation_from_meta( $attachment_id ); + if ( ! empty( $transformations ) ) { + $breakpoints['transformation'] = Api::generate_transformation_string( $transformations ); + } + $breakpoints = array( + 'public_id' => $this->get_public_id( $attachment_id ), + 'type' => 'upload', + 'responsive_breakpoints' => $breakpoint_options, + 'context' => $this->get_context_options( $attachment_id ), + ); + + } + + return $breakpoints; + } + + /** + * Get the context options for an asset. + * + * @param int $attachment_id The ID of the attachment. + * + * @return array + */ + public function get_context_options( $attachment_id ) { + + $context_options = array( + 'caption' => esc_attr( get_the_title( $attachment_id ) ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + 'guid' => md5( get_the_guid( $attachment_id ) ), + ); + + // Check if this asset is a folder sync. + $folder_sync = $this->get_post_meta( $attachment_id, Sync::META_KEYS['folder_sync'], true ); + if ( ! empty( $folder_sync ) ) { + $context_options['wp_id'] = $attachment_id; + } + + /** + * Filter the options to allow other plugins to add requested options for uploading. + * + * @param array $options The options array. + * @param \WP_Post $post The attachment post. + * @param \Cloudinary\Sync The sync object instance. + * + * @return array + */ + $context_options = apply_filters( 'cloudinary_context_options', $context_options, get_post( $attachment_id ), $this ); + + return http_build_query( $context_options, null, '|' ); + } + + /** + * Check if an asset is folder synced. + * + * @param int $attachment_id The attachment ID. + * + * @return bool + */ + public function is_folder_synced( $attachment_id ) { + + $return = true; // By default all assets in WordPress will be synced. + if ( $this->sync->been_synced( $attachment_id ) ) { + $return = ! empty( $this->get_post_meta( $attachment_id, Sync::META_KEYS['folder_sync'], true ) ); + } + + return $return; + } + + /** + * Get the media upload options as connected to Cloudinary. + * + * @param int $attachment_id The attachment ID. + * + * @return array + */ + public function get_upload_options( $attachment_id ) { + + // Prepare upload options. + $public_id = $this->get_public_id( $attachment_id ); + $folder = ltrim( dirname( $public_id ), '.' ); + $options = array( + 'unique_filename' => true, + 'overwrite' => false, + 'resource_type' => $this->get_media_type( $attachment_id ), + 'public_id' => basename( $public_id ), + 'context' => $this->get_context_options( $attachment_id ), + ); + + /** + * Filter the options to allow other plugins to add requested options for uploading. + * + * @param array $options The options array. + * @param \WP_Post $post The attachment post. + * @param \Cloudinary\Sync The sync object instance. + * + * @return array + */ + $options = apply_filters( 'cloudinary_upload_options', $options, get_post( $attachment_id ), $this ); + // Add folder to prevent folder contamination. + if ( $this->is_folder_synced( $attachment_id ) ) { + $options['public_id'] = $this->get_cloudinary_folder() . basename( $options['public_id'] ); + } elseif ( ! empty( $folder ) ) { + // add in folder if not empty (not in root). + $options['public_id'] = trailingslashit( $folder ) . basename( $options['public_id'] ); + } + + return $options; + } + /** * Setup the hooks and base_url if configured. */ public function setup() { if ( $this->plugin->config['connect'] ) { - $this->base_url = $this->plugin->components['connect']->api->cloudinary_url( '/' ); - $this->credentials = $this->plugin->components['connect']->get_credentials(); - $this->cloudinary_folder = $this->plugin->config['settings']['sync_media']['cloudinary_folder'] ? $this->plugin->config['settings']['sync_media']['cloudinary_folder'] : ''; + $this->base_url = $this->plugin->components['connect']->api->cloudinary_url( '/' ); + $this->credentials = $this->plugin->components['connect']->get_credentials(); + $this->cloudinary_folder = $this->plugin->config['settings']['sync_media']['cloudinary_folder'] ? $this->plugin->config['settings']['sync_media']['cloudinary_folder'] : ''; + $this->sync = $this->plugin->components['sync']; + + // Internal components. $this->filter = new Filter( $this ); $this->upgrade = new Upgrade( $this ); $this->global_transformations = new Global_Transformations( $this ); @@ -1340,7 +1531,6 @@ public function setup() { // Filter live URLS. (functions that return a URL). add_filter( 'wp_calculate_image_srcset', array( $this, 'image_srcset' ), 10, 5 ); add_filter( 'wp_calculate_image_srcset_meta', array( $this, 'match_responsive_sources' ), 10, 4 ); - add_filter( 'wp_get_attachment_metadata', array( $this, 'add_cloudinary_full_size' ), 10, 2 ); add_filter( 'wp_get_attachment_url', array( $this, 'attachment_url' ), 10, 2 ); add_filter( 'image_downsize', array( $this, 'filter_downsize' ), 10, 3 ); diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-plugin.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-plugin.php index 2f07275ba..4e590d899 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-plugin.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-plugin.php @@ -22,7 +22,7 @@ class Plugin { * * @since 0.1 * - * @var array + * @var Media[]|Sync[]|Settings_Page[]|REST_API[]|Connect[] */ public $components; /** diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-rest-api.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-rest-api.php index c857cc0dd..5ec043129 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-rest-api.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-rest-api.php @@ -36,6 +36,15 @@ public function __construct( Plugin $plugin ) { */ public function rest_api_init() { foreach ( $this->endpoints as $route => $endpoint ) { + $endpoint = wp_parse_args( + $endpoint, + array( + 'method' => 'GET', + 'callback' => null, + 'arg' => array(), + 'permission_callback' => '__return_true', + ) + ); register_rest_route( static::BASE, $route, @@ -73,20 +82,21 @@ public function background_request( $endpoint, $params, $method = 'POST' ) { if ( is_user_logged_in() ) { // Setup cookie. $logged_cookie = wp_parse_auth_cookie( '', 'logged_in' ); - array_pop( $logged_cookie ); // remove the scheme. + if ( ! empty( $logged_cookie ) ) { + array_pop( $logged_cookie ); // remove the scheme. - // Add logged in cookie to request. - $args['cookies'] = array( - new \WP_Http_Cookie( - array( - 'name' => LOGGED_IN_COOKIE, - 'value' => implode( '|', $logged_cookie ), - 'expires' => '+ 1 min', // Expire after a min only. + // Add logged in cookie to request. + $args['cookies'] = array( + new \WP_Http_Cookie( + array( + 'name' => LOGGED_IN_COOKIE, + 'value' => implode( '|', $logged_cookie ), + 'expires' => '+ 1 min', // Expire after a min only. + ), + $url ), - $url - ), - ); - + ); + } } $args['headers']['X-WP-Nonce'] = $params['nonce']; diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-settings-page.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-settings-page.php index 6a1c6200d..84e9eab57 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-settings-page.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-settings-page.php @@ -224,8 +224,8 @@ public function render() { * * @since 0.1 * - * @param array $field The field to render. - * @param string|null $value The value to render. + * @param array $field The field to render. + * @param string|null $value The value to render. * @param bool $show_description Whether to render the description. */ public function render_field( $field, $value = null, $show_description = true ) { @@ -415,7 +415,7 @@ public function sanitize( $setting, $fields ) { if ( is_array( $field ) ) { array_walk_recursive( $field, - static function( $field_value ) { + static function ( $field_value ) { // WP 4.9 compatibility, as _sanitize_text_fields() didn't have this check yet, and this prevents an error. // @see https://github.com/WordPress/wordpress-develop/blob/b30baca3ca2feb7f44b3615262ca55fcd87ae232/src/wp-includes/formatting.php#L5307 if ( is_object( $field_value ) || is_array( $field_value ) ) { @@ -726,6 +726,8 @@ private function register_tab_assets( $tab_slug ) { * * @param array $set The array of assets to register. * @param callable $call The function to call to register asset. + * + * @return array */ public function register_tab_asset( $set, $call ) { foreach ( $set as $key => &$asset ) { @@ -914,18 +916,4 @@ public function set_active_page( $page_slug ) { } } - /** - * Checks if auto sync feature is enabled. - * - * @return bool - */ - public function is_auto_sync_enabled() { - $settings = $this->get_config(); - - if ( ! empty( $settings['sync_media']['auto_sync'] ) && 'on' === $settings['sync_media']['auto_sync'] ) { - return true; - } - - return false; - } } diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-sync.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-sync.php index 7845b1904..02e0d4d57 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-sync.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-sync.php @@ -12,7 +12,7 @@ use Cloudinary\Sync\Delete_Sync; use Cloudinary\Sync\Download_Sync; use Cloudinary\Sync\Push_Sync; -use Cloudinary\Sync\Upload_Queue; +use Cloudinary\Sync\Sync_Queue; use Cloudinary\Sync\Upload_Sync; /** @@ -36,6 +36,27 @@ class Sync implements Setup, Assets { */ public $managers; + /** + * Contains the sync base structure and callbacks. + * + * @var array + */ + protected $sync_base_struct; + + /** + * Contains the sync types and callbacks. + * + * @var array + */ + protected $sync_types; + + /** + * Holds a list of unsynced images to push on end. + * + * @var array + */ + private $to_sync = array(); + /** * Holds the meta keys for sync meta to maintain consistency. */ @@ -43,14 +64,17 @@ class Sync implements Setup, Assets { 'pending' => '_cloudinary_pending', 'signature' => '_sync_signature', 'version' => '_cloudinary_version', + 'plugin_version' => '_plugin_version', 'breakpoints' => '_cloudinary_breakpoints', 'public_id' => '_public_id', 'transformation' => '_transformations', 'sync_error' => '_sync_error', 'cloudinary' => '_cloudinary_v2', 'folder_sync' => '_folder_sync', + 'suffix' => '_suffix', 'syncing' => '_cloudinary_syncing', 'downloading' => '_cloudinary_downloading', + 'process_log' => '_process_log', ); /** @@ -64,7 +88,7 @@ public function __construct( Plugin $plugin ) { $this->managers['upload'] = new Upload_Sync( $this->plugin ); $this->managers['download'] = new Download_Sync( $this->plugin ); $this->managers['delete'] = new Delete_Sync( $this->plugin ); - $this->managers['queue'] = new Upload_Queue( $this->plugin ); + $this->managers['queue'] = new Sync_Queue( $this->plugin ); } /** @@ -84,6 +108,12 @@ public function enqueue_assets() { * Register Assets. */ public function register_assets() { + if ( $this->plugin->config['connect'] ) { + // Setup the sync_base_structure. + $this->setup_sync_base_struct(); + // Setup sync types. + $this->setup_sync_types(); + } } @@ -94,6 +124,21 @@ public function is_active() { return $this->plugin->components['settings']->is_active() && 'sync_media' === $this->plugin->components['settings']->active_tab(); } + /** + * Checks if an asset has been synced and up to date. + * + * @param int $attachment_id The attachment id to check. + * + * @return bool + */ + public function been_synced( $attachment_id ) { + + $public_id = $this->managers['media']->has_public_id( $attachment_id ); + $meta = wp_get_attachment_metadata( $attachment_id ); + + return ! empty( $public_id ) || ! empty( $meta['cloudinary'] ); // From v1. + } + /** * Checks if an asset is synced and up to date. * @@ -102,83 +147,662 @@ public function is_active() { * @return bool */ public function is_synced( $post_id ) { - $signature = $this->get_signature( $post_id ); $expecting = $this->generate_signature( $post_id ); - if ( ! empty( $signature ) && ! empty( $expecting ) && $expecting === $signature ) { - return true; - } - - if ( $this->plugin->components['settings']->is_auto_sync_enabled() && apply_filters( 'cloudinary_flag_sync', '__return_false' ) && ! get_post_meta( $post_id, Sync::META_KEYS['downloading'], true ) ) { - update_post_meta( $post_id, Sync::META_KEYS['syncing'], true ); + if ( ! is_wp_error( $expecting ) ) { + $signature = $this->get_signature( $post_id ); + // Sort to align orders for comparison. + ksort( $signature ); + ksort( $expecting ); + if ( ! empty( $signature ) && ! empty( $expecting ) && $expecting === $signature ) { + return true; + } } return false; } + /** + * Check if sync type is required for rendering a Cloudinary URL. + * + * @param string $type The type to check. + * + * @return bool + */ + public function is_required( $type ) { + return ! empty( $this->sync_base_struct[ $type ]['required'] ); + } + /** * Generate a signature based on whats required for a full sync. * - * @param int $post_id The post id to generate a signature for. + * @param int $attachment_id The Attachment id to generate a signature for. + * @param bool $cache Flag to specify if a cached signature is to be used or build a new one. * * @return string|bool */ - public function generate_signature( $post_id ) { - $upload = $this->managers['push']->prepare_upload( $post_id ); - // Check if has an error (ususally due to file quotas). - if ( is_wp_error( $upload ) ) { - $this->plugin->components['media']->get_post_meta( $post_id, self::META_KEYS['sync_error'], $upload->get_error_message() ); + public function generate_signature( $attachment_id, $cache = true ) { + static $signatures = array(); // cache signatures. + if ( ! empty( $signatures[ $attachment_id ] ) && true === $cache ) { + $return = $signatures[ $attachment_id ]; + } else { + $return = $this->sync_base( $attachment_id ); + // Add to signature cache. + $signatures[ $attachment_id ] = $return; + } + + return $return; + } + + /** + * Check if an asset can be synced. + * + * @param int $attachment_id The attachment ID to check if it can be synced. + * @param string $type The type of sync to attempt. + * + * @return bool + */ + public function can_sync( $attachment_id, $type = 'file' ) { + + $can = $this->is_auto_sync_enabled(); - return false; + if ( $this->is_pending( $attachment_id ) ) { + $can = false; + } elseif ( $this->been_synced( $attachment_id ) ) { + $can = true; } - $credentials = $this->plugin->components['connect']->get_credentials(); - $upload['cloud_name'] = $credentials['cloud_name']; - $return = array_map( - function ( $item ) { - if ( is_array( $item ) ) { - $item = wp_json_encode( $item ); - } - return md5( $item ); - }, - $upload - ); + /** + * Filter to allow changing if an asset is allowed to be synced. + * Return a WP Error with reason why it can't be synced. + * + * @param int $attachment_id The attachment post ID. + * + * @return bool|\WP_Error + */ + return apply_filters( 'cloudinary_can_sync_asset', $can, $attachment_id, $type ); + } - return $return; + /** + * Get the last version this asset was synced with. + * + * @param int $attachment_id The attachment ID. + * + * @return mixed + */ + public function get_sync_version( $attachment_id ) { + $version = $this->managers['media']->get_post_meta( $attachment_id, self::META_KEYS['plugin_version'], true ); + + return $version; } /** * Get the current sync signature of an asset. * - * @param int $post_id The post ID. + * @param int $attachment_id The attachment ID. + * @param bool $cached Flag to specify if a cached signature is to be used or build a new one. * - * @return array|bool + * @return array */ - public function get_signature( $post_id ) { + public function get_signature( $attachment_id, $cached = true ) { static $signatures = array(); // Cache signatures already fetched. - $return = false; - if ( ! empty( $signatures[ $post_id ] ) ) { - $return = $signatures[ $post_id ]; + $return = array(); + if ( ! empty( $signatures[ $attachment_id ] ) && true === $cached ) { + $return = $signatures[ $attachment_id ]; } else { - $signature = $this->plugin->components['media']->get_post_meta( $post_id, self::META_KEYS['signature'], true ); - if ( ! empty( $signature ) ) { - $base_signatures = $this->generate_signature( $post_id ); - $signatures[ $post_id ] = wp_parse_args( $signature, $base_signatures ); - $return = $signatures[ $post_id ]; + $signature = $this->managers['media']->get_post_meta( $attachment_id, self::META_KEYS['signature'], true ); + if ( empty( $signature ) ) { + $signature = array(); } + + // Remove any old or outdated signature items. against the expected. + $signature = array_intersect_key( $signature, $this->sync_types ); + $signatures[ $attachment_id ] = $return; + $return = wp_parse_args( $signature, $this->sync_types ); } return $return; } + /** + * Generate a new Public ID for an asset. + * + * @param int $attachment_id The attachment ID for the new public ID. + * + * @return string|null + */ + public function generate_public_id( $attachment_id ) { + + $cld_folder = $this->managers['media']->get_cloudinary_folder(); + $file = get_attached_file( $attachment_id ); + $file_info = pathinfo( $file ); + $public_id = $cld_folder . $file_info['filename']; + + return ltrim( $public_id, '/' ); + } + + /** + * Register a new sync type. + * + * @param string $type Sync type key. Must not exceed 20 characters and may + * only contain lowercase alphanumeric characters, dashes, + * and underscores. See sanitize_key() + * @param array $structure { + * Array of arguments for registering a sync type. + * + * @type callable $generate Callback method that generates the values to be used to sign a state. + * Returns a string or array. + * + * @type callable $validate Optional Callback method that validates the need to have the sync type applied. + * returns Bool. + * + * @type int $priority Priority in which the type takes place. Lower is higher priority. + * i.e a download should happen before an upload so download is lower in the chain. + * + * @type callable $sync Callback method that handles the sync. i.e uploads the file, adds meta data, etc.. + * + * @type string $state State class to be added to the status icon in media library. + * + * @type string|callback $note The status text displayed next to a syncing asset in the media library. + * Can be a callback if the note needs to be dynamic. see type folder. + * + * } + */ + public function register_sync_type( $type, $structure ) { + + // Apply a default to ensure parts exist. + $default = array( + 'generate' => '__return_null', + 'validate' => null, + 'priority' => 50, + 'sync' => '__return_null', + 'state' => 'sync', + 'note' => __( 'Synchronizing asset with Cloudinary', 'cloudinary' ), + ); + + $this->sync_base_struct[ $type ] = wp_parse_args( $structure, $default ); + } + + /** + * Get built-in structures that form an assets entire sync state. This holds methods for building signatures for each state of synchronization. + * These can be extended via 3rd parties by adding to the structures with custom types and generation and sync methods. + */ + public function setup_sync_base_struct() { + + $base_struct = array( + 'upgrade' => array( + 'generate' => array( $this, 'get_sync_version' ), // Method to generate a signature. Which + 'validate' => array( $this, 'been_synced' ), + 'priority' => 0, + 'sync' => array( $this->managers['media']->upgrade, 'convert_cloudinary_version' ), + 'state' => 'info syncing', + 'note' => __( 'Upgrading from previous version', 'cloudinary' ), + 'realtime' => true, + ), + 'download' => array( + 'generate' => '__return_false', + 'validate' => function ( $attachment_id ) { + $file = get_attached_file( $attachment_id ); + + return ! file_exists( $file ); + }, + 'priority' => 1, + 'sync' => array( $this->managers['download'], 'download_asset' ), + 'state' => 'info downloading', + 'note' => __( 'Downloading from Cloudinary', 'cloudinary' ), + ), + 'file' => array( + 'generate' => 'get_attached_file', + 'priority' => 5.1, + 'sync' => array( $this->managers['upload'], 'upload_asset' ), + 'state' => 'uploading', + 'note' => __( 'Uploading to Cloudinary', 'cloudinary' ), + 'required' => true, // Required to complete URL render flag. + ), + 'folder' => array( + 'generate' => array( $this->managers['media'], 'get_cloudinary_folder' ), + 'validate' => array( $this->managers['media'], 'is_folder_synced' ), + 'priority' => 10, + 'sync' => array( $this->managers['upload'], 'upload_asset' ), + 'state' => 'info syncing', + 'note' => function () { + return sprintf( __( 'Copying to folder %s.', 'cloudinary' ), untrailingslashit( $this->managers['media']->get_cloudinary_folder() ) ); + }, + 'required' => true, // Required to complete URL render flag. + ), + 'public_id' => array( + 'generate' => array( $this->managers['media'], 'get_public_id' ), + 'validate' => function ( $attachment_id ) { + $public_id = $this->managers['media']->has_public_id( $attachment_id ); + + return false === $public_id; + }, + 'priority' => 20, + 'sync' => array( $this->managers['media']->upgrade, 'convert_cloudinary_version' ), // Rename + 'state' => 'info syncing', + 'note' => __( 'Updating metadata', 'cloudinary' ), + 'required' => true, + ), + 'breakpoints' => array( + 'generate' => array( $this->managers['media'], 'get_breakpoint_options' ), + 'priority' => 25, + 'sync' => array( $this->managers['upload'], 'explicit_update' ), + 'state' => 'info syncing', + 'note' => __( 'Updating breakpoints', 'cloudinary' ), + ), + 'options' => array( + 'generate' => array( $this->managers['media'], 'get_upload_options' ), + 'priority' => 30, + 'sync' => array( $this->managers['upload'], 'context_update' ), + 'state' => 'info syncing', + 'note' => __( 'Updating metadata', 'cloudinary' ), + ), + 'cloud_name' => array( + 'generate' => array( $this->managers['connect'], 'get_cloud_name' ), + 'priority' => 5.5, + 'sync' => array( $this->managers['upload'], 'upload_asset' ), + 'state' => 'uploading', + 'note' => __( 'Uploading to new cloud name.', 'cloudinary' ), + 'required' => true, + ), + ); + + /** + * Filter the sync base structure to allow other plugins to sync component callbacks. + * + * @param array $base_struct The base sync structure. + * + * @return array + */ + $base_struct = apply_filters( 'cloudinary_sync_base_struct', $base_struct ); + + // Register each sync type. + foreach ( $base_struct as $type => $structure ) { + $this->register_sync_type( $type, $structure ); + } + + /** + * Do action for setting up sync types. + * + * @param \Cloudinary\Sync $this The sync object. + */ + do_action( 'cloudinary_register_sync_types', $this ); + } + + /** + * Setup the sync types in priority order based on sync struct. + */ + public function setup_sync_types() { + + $sync_types = array(); + foreach ( $this->sync_base_struct as $type => $struct ) { + if ( is_callable( $struct['sync'] ) ) { + $sync_types[ $type ] = floatval( $struct['priority'] ); + } + } + + asort( $sync_types ); + + $this->sync_types = $sync_types; + } + + /** + * Get a method from a sync type. + * + * @param string $type The sync type to get from. + * @param string $method The method to get from the sync type. + * + * @return callable|null + */ + public function get_sync_type_method( $type, $method ) { + $return = null; + if ( isset( $this->sync_base_struct[ $type ][ $method ] ) && is_callable( $this->sync_base_struct[ $type ][ $method ] ) ) { + $return = $this->sync_base_struct[ $type ][ $method ]; + } + + return $return; + } + + /** + * Run a sync method on and attachment_id. + * + * @param string $type The sync type to run. + * @param string $method The method to run. + * @param int $attachment_id The attachment ID to run method against. + * + * @return mixed + */ + public function run_sync_method( $type, $method, $attachment_id ) { + $return = null; + $run_method = $this->get_sync_type_method( $type, $method ); + if ( $run_method ) { + $return = call_user_func( $run_method, $attachment_id ); + } + + return $return; + } + + /** + * Generate a single sync type signature for an asset. + * + * @param string $type The sync type to run. + * @param int $attachment_id The attachment ID to run method against. + * + * @return mixed + */ + public function generate_type_signature( $type, $attachment_id ) { + $return = null; + $run_method = $this->get_sync_type_method( $type, 'generate' ); + if ( $run_method ) { + $value = call_user_func( $run_method, $attachment_id ); + if ( ! is_wp_error( $value ) ) { + if ( is_array( $value ) ) { + $value = wp_json_encode( $value ); + } + $return = md5( $value ); + } + } + + return $return; + } + + /** + * Prepares and asset for sync comparison by getting all sync types + * and running the generate methods for each type. + * + * @param int|\WP_Post $post The attachment to prepare. + * + * @return array|\WP_Error + */ + public function sync_base( $post ) { + + if ( ! $this->managers['media']->is_media( $post ) ) { + return new \WP_Error( 'attachment_post_expected', __( 'An attachment post was expected.', 'cloudinary' ) ); + } + + $return = array(); + foreach ( array_keys( $this->sync_types ) as $type ) { + $return[ $type ] = $this->generate_type_signature( $type, $post ); + } + + /** + * Filter the sync base to allow other plugins to add requested sync components for the sync signature. + * + * @param array $options The options array. + * @param \WP_Post $post The attachment post. + * @param \Cloudinary\Sync The sync object instance. + * + * @return array + */ + $return = apply_filters( 'cloudinary_sync_base', $return, $post ); + + return $return; + } + + /** + * Prepare an asset to be synced, maybe. + * + * @param int $attachment_id The attachment ID. + * + * @return string | null + */ + public function maybe_prepare_sync( $attachment_id ) { + + $type = $this->get_sync_type( $attachment_id ); + if ( $type && $this->can_sync( $attachment_id, $type ) ) { + $this->add_to_sync( $attachment_id ); + } + + return $type; + } + + /** + * Get the type of sync, with the lowest priority for this asset. + * + * @param int $attachment_id The attachment ID. + * @param bool $cached Flag to specify if a cached signature is to be used or build a new one. + * + * @return string|null + */ + public function get_sync_type( $attachment_id, $cached = true ) { + if ( ! $this->managers['media']->is_media( $attachment_id ) ) { + return null; // Ignore non media items. + } + $return = null; + $required_signature = $this->generate_signature( $attachment_id, $cached ); + $attachment_signature = $this->get_signature( $attachment_id, $cached ); + if ( is_array( $required_signature ) ) { + $sync_items = array_filter( + $attachment_signature, + function ( $item, $key ) use ( $required_signature ) { + return $item !== $required_signature[ $key ]; + }, + ARRAY_FILTER_USE_BOTH + ); + $ordered = array_intersect_key( $this->sync_types, $sync_items ); + if ( ! empty( $ordered ) ) { + $types = array_keys( $ordered ); + $type = array_shift( $types ); + $return = $this->validate_sync_type( $type, $attachment_id ); + } + } + + return $return; + } + + /** + * Validate the asset needs the sync type to be run, and generate a valid signature if not. + * + * @param string $type The sync type to validate. + * @param int $attachment_id The attachment ID to validate against. + * + * @return string|null + */ + public function validate_sync_type( $type, $attachment_id ) { + // Validate that this sync type applied (for optional types like upgrade). + if ( false === $this->run_sync_method( $type, 'validate', $attachment_id ) ) { + // If invalid, save the new signature + $this->set_signature_item( $attachment_id, $type ); + + $type = $this->get_sync_type( $attachment_id, false ); // Set cache to false to get the new signature. + } else { + // Check if this is a realtime process. + if ( ! empty( $this->sync_base_struct[ $type ]['realtime'] ) ) { + $this->run_sync_method( $type, 'sync', $attachment_id ); + $type = $this->get_sync_type( $attachment_id, false ); // Set cache to false to get the new signature. + } + } + + return $type; + } + + /** + * Checks the status of the media item. + * + * @param array $status Array of state and note. + * @param int $attachment_id The attachment id. + * + * @return array + */ + public function filter_status( $status, $attachment_id ) { + + if ( $this->been_synced( $attachment_id ) || ( $this->is_pending( $attachment_id ) && $this->get_sync_type( $attachment_id ) ) ) { + $sync_type = $this->get_sync_type( $attachment_id ); + if ( ! empty( $sync_type ) && isset( $this->sync_base_struct[ $sync_type ] ) ) { + // check process log in case theres an error. + $log = $this->managers['media']->get_post_meta( $attachment_id, Sync::META_KEYS['process_log'] ); + if ( ! empty( $log[ $sync_type ] ) && is_wp_error( $log[ $sync_type ] ) ) { + // Use error instead of sync note. + $status['state'] = 'error'; + $status['note'] = $log[ $sync_type ]->get_error_message(); + } else { + $status['state'] = $this->sync_base_struct[ $sync_type ]['state']; + $status['note'] = $this->sync_base_struct[ $sync_type ]['note']; + if ( is_callable( $status['note'] ) ) { + $status['note'] = call_user_func( $status['note'], $attachment_id ); + } + } + } + + + // Check if there's an error. + $has_error = $this->managers['media']->get_post_meta( $attachment_id, Sync::META_KEYS['sync_error'], true ); + if ( ! empty( $has_error ) && $this->get_sync_type( $attachment_id ) ) { + $status['state'] = 'error'; + $status['note'] = $has_error; + } + } + + return $status; + } + + /** + * Add media state to display syncing info. + * + * @param array $media_states List of the states. + * @param \WP_Post $post The current attachment post. + * + * @return array + */ + public function filter_media_states( $media_states, $post ) { + + $status = apply_filters( 'cloudinary_media_status', array(), $post->ID ); + if ( ! empty( $status ) ) { + $media_states[] = $status['note']; + } + + return $media_states; + } + + /** + * Check if the attachment is pending an upload sync. + * + * @param int $attachment_id The attachment ID to check. + * + * @return bool + */ + public function is_pending( $attachment_id ) { + // Check if it's not already in the to sync array. + if ( ! in_array( $attachment_id, $this->to_sync, true ) ) { + $is_pending = $this->managers['media']->get_post_meta( $attachment_id, Sync::META_KEYS['pending'], true ); + if ( empty( $is_pending ) || $is_pending < time() - 5 * 60 ) { + // No need to delete pending meta, since it will be updated with the new timestamp anyway. + return false; + } + } + + return true; + } + + /** + * Add an attachment ID to the to_sync array. + * + * @param int $attachment_id The attachment ID to add. + */ + public function add_to_sync( $attachment_id ) { + if ( ! in_array( $attachment_id, $this->to_sync, true ) ) { + // Flag image as pending to prevent duplicate upload. + $this->managers['media']->update_post_meta( $attachment_id, Sync::META_KEYS['pending'], time() ); + $this->to_sync[] = $attachment_id; + } + } + + /** + * Update signatures of types that match the specified types sync method. This prevents running the same method repeatedly. + * + * @param int $attachment_id The attachment ID. + * @param string $type The type of sync. + */ + public function sync_signature_by_type( $attachment_id, $type ) { + $current_sync_method = $this->sync_base_struct[ $type ]['sync']; + + // Go over all other types that share the same sync method and include them here. + foreach ( $this->sync_base_struct as $sync_type => $struct ) { + if ( $struct['sync'] === $current_sync_method ) { + $this->set_signature_item( $attachment_id, $sync_type ); + } + } + } + + /** + * Set an item to the signature set. + * + * @param int $attachment_id The attachment ID. + * @param string $type The sync type. + * @param null $value + */ + public function set_signature_item( $attachment_id, $type, $value = null ) { + + // Get the core meta. + $meta = wp_get_attachment_metadata( $attachment_id, true ); + if ( empty( $meta[ Sync::META_KEYS['cloudinary'] ] ) ) { + $meta[ Sync::META_KEYS['cloudinary'] ] = array(); + } + // Set the specific value. + if ( is_null( $value ) ) { + // Generate a new value based on generator. + $value = $this->generate_type_signature( $type, $attachment_id ); + } + $meta[ Sync::META_KEYS['cloudinary'] ][ Sync::META_KEYS['signature'] ][ $type ] = $value; + wp_update_attachment_metadata( $attachment_id, $meta ); + } + + /** + * Initialize the background sync on requested images needing to be synced. + */ + public function init_background_upload() { + if ( ! empty( $this->to_sync ) ) { + + $threads = $this->managers['push']->queue->threads; + $chunk_size = ceil( count( $this->to_sync ) / count( $threads ) ); // Max of 3 threads to prevent server overload. + $chunks = array_chunk( $this->to_sync, $chunk_size ); + $token = uniqid(); + foreach ( $chunks as $key => $ids ) { + $params = array( + 'process_key' => $token . '-' . $threads[ $key ], + ); + set_transient( $params['process_key'], $ids, 120 ); + $this->plugin->components['api']->background_request( 'process', $params ); + } + } + } + + /** + * Checks if auto sync feature is enabled. + * + * @return bool + */ + public function is_auto_sync_enabled() { + $settings = $this->plugin->config['settings']; + + if ( ! empty( $settings['sync_media']['auto_sync'] ) && 'on' === $settings['sync_media']['auto_sync'] ) { + return true; + } + + return false; + } + /** * Additional component setup. */ public function setup() { if ( $this->plugin->config['connect'] ) { + + // Show sync status. + add_filter( 'cloudinary_media_status', array( $this, 'filter_status' ), 10, 2 ); + add_filter( 'display_media_states', array( $this, 'filter_media_states' ), 10, 2 ); + // Hook for on demand upload push. + add_action( 'shutdown', array( $this, 'init_background_upload' ) ); + $this->managers['upload']->setup(); $this->managers['delete']->setup(); + $this->managers['download']->setup(); + $this->managers['push']->setup(); + // Setup additional components. + $this->managers['media'] = $this->plugin->components['media']; + $this->managers['connect'] = $this->plugin->components['connect']; + $this->managers['api'] = $this->plugin->components['api']; } } } diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/connect/class-api.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/connect/class-api.php index 279a7da93..a95b262c3 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/connect/class-api.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/connect/class-api.php @@ -177,7 +177,8 @@ public function url( $resource, $function = null, $endpoint = false ) { /** * Generate a transformation string. * - * @param array $options The transformation options to generate from. + * @param array $options The transformation options to generate from. + * @param string $type The asset Type. * * @return string */ @@ -186,7 +187,7 @@ public static function generate_transformation_string( array $options, $type = ' $transformations = array_map( function ( $item ) use ( $transformation_index ) { $transform = array(); - if ( is_string ( $item ) ) { + if ( is_string( $item ) ) { return $item; } @@ -229,17 +230,19 @@ function ( $item ) use ( $transformation_index ) { public function cloudinary_url( $public_id, $args = array(), $size = array(), $clean = false ) { $defaults = array( 'resource_type' => 'image', + 'version' => 'v1', ); $args = wp_parse_args( array_filter( $args ), $defaults ); - + // Correct Audio to Video. + if ( 'audio' === $args['resource_type'] ) { + $args['resource_type'] = 'video'; + } // check for version. if ( ! empty( $args['version'] ) && is_numeric( $args['version'] ) ) { $args['version'] = 'v' . $args['version']; - } else { - $args['version'] = 'v1'; } - // Determine if we're dealing with a fetched + // Determine if we're dealing with a fetched. // ...or uploaded image and update the URL accordingly. $asset_endpoint = filter_var( $public_id, FILTER_VALIDATE_URL ) ? 'fetch' : 'upload'; @@ -269,20 +272,38 @@ public function cloudinary_url( $public_id, $args = array(), $size = array(), $c return implode( '/', $url_parts ); } + /** + * Get the details of an asset by public ID. + * + * @param string $public_id The public_id to check. + * @param string $type The asset type. + * + * @return array|\WP_Error + */ + public function get_asset_details( $public_id, $type ) { + $url = $this->url( 'resources', $type . '/upload/' . $public_id, true ); + + return $this->call( $url, array( 'body' => array() ), 'get' ); + } + /** * Upload a large asset in chunks. * - * @param string $file Path to the file. - * @param array $args Array of upload options. + * @param int $attachment_id The attachment ID. + * @param array $args Array of upload options. * * @return array|\WP_Error */ - public function upload_large( $file, $args ) { + public function upload_large( $attachment_id, $args ) { + // Ensure we have the right file. + if ( empty( $args['file'] ) ) { + $args['file'] = get_attached_file( $attachment_id ); + } $tempfile = false; - if ( false !== strpos( $file, 'vip://' ) ) { - $file = $this->create_local_copy( $file ); - if ( is_wp_error( $file ) ) { - return $file; + if ( false !== strpos( $args['file'], 'vip://' ) ) { + $args['file'] = $this->create_local_copy( $args['file'] ); + if ( is_wp_error( $args['file'] ) ) { + return $args['file']; } $tempfile = true; } @@ -299,12 +320,12 @@ public function upload_large( $file, $args ) { } // Since WP_Filesystem doesn't have a fread, we need to do it manually. However we'll still use it for writing. - $src = fopen( $file, 'r' ); // phpcs:ignore - $temp_file_name = wp_tempnam( uniqid( time() ) . '.' . pathinfo( $file, PATHINFO_EXTENSION ) ); + $src = fopen( $args['file'], 'r' ); // phpcs:ignore + $temp_file_name = wp_tempnam( uniqid( time() ) . '.' . pathinfo( $args['file'], PATHINFO_EXTENSION ) ); $upload_id = substr( sha1( uniqid( $this->credentials['api_secret'] . wp_rand() ) ), 0, 16 ); $chunk_size = 20000000; $index = 0; - $file_size = filesize( $file ); + $file_size = filesize( $args['file'] ); while ( ! feof( $src ) ) { $current_loc = $index * $chunk_size; if ( $current_loc >= $file_size ) { @@ -318,12 +339,12 @@ public function upload_large( $file, $args ) { $temp_file_size = filesize( $temp_file_name ); $range = 'bytes ' . $current_loc . '-' . ( $current_loc + $temp_file_size - 1 ) . '/' . $file_size; - $headers = array( + $headers = array( 'Content-Range' => $range, 'X-Unique-Upload-Id' => $upload_id, ); - - $result = $this->upload( $temp_file_name, $args, $headers ); + $args['file'] = $temp_file_name; + $result = $this->upload( $temp_file_name, $args, $headers ); if ( is_wp_error( $result ) ) { break; } @@ -332,7 +353,7 @@ public function upload_large( $file, $args ) { fclose( $src ); //phpcs:ignore unlink( $temp_file_name ); //phpcs:ignore if ( true === $tempfile ) { - unlink( $file ); //phpcs:ignore + unlink( $args['file'] ); //phpcs:ignore } return $result; @@ -342,30 +363,50 @@ public function upload_large( $file, $args ) { /** * Upload an asset. * - * @param string $file Path to the file. - * @param array $args Array of upload options. - * @param array $headers Additional headers to use in upload. + * @param int $attachment_id Attachment ID to upload. + * @param array $args Array of upload options. + * @param array $headers Additional headers to use in upload. * * @return array|\WP_Error */ - public function upload( $file, $args, $headers = array() ) { - $tempfile = false; - if ( false !== strpos( $file, 'vip://' ) ) { - $file = $this->create_local_copy( $file ); - if ( is_wp_error( $file ) ) { - return $file; - } - $tempfile = true; - } - $resource = ! empty( $args['resource_type'] ) ? $args['resource_type'] : 'image'; - $url = $this->url( $resource, 'upload', true ); - $args = $this->clean_args( $args ); - // Attach File. - if ( function_exists( 'curl_file_create' ) ) { - $args['file'] = curl_file_create( $file ); // phpcs:ignore - $args['file']->setPostFilename( $file ); + public function upload( $attachment_id, $args, $headers = array() ) { + + $resource = ! empty( $args['resource_type'] ) ? $args['resource_type'] : 'image'; + $url = $this->url( $resource, 'upload', true ); + $args = $this->clean_args( $args ); + $disable_https_fetch = get_transient( '_cld_disable_http_upload' ); + + // Check if we can try http file upload. + if ( empty( $headers ) && empty( $disable_https_fetch ) ) { + $args['file'] = wp_get_attachment_url( $attachment_id ); } else { - $args['file'] = '@' . $file; + // We should have the file in args at this point, but if the transient was set, it will be defaulting here. + if ( empty( $args['file'] ) ) { + $args['file'] = get_attached_file( $attachment_id ); + } + // Headers indicate chunked upload. + if ( empty( $headers ) ) { + $size = filesize( $args['file'] ); + if ( 'video' === $resource || $size > 100000000 ) { + return $this->upload_large( $attachment_id, $args ); + } + } + $tempfile = false; + if ( false !== strpos( $args['file'], 'vip://' ) ) { + $args['file'] = $this->create_local_copy( $args['file'] ); + if ( is_wp_error( $args['file'] ) ) { + return $args['file']; + } + $tempfile = true; + } + // Attach File. + if ( function_exists( 'curl_file_create' ) ) { + $file = $args['file']; + $args['file'] = curl_file_create( $file ); // phpcs:ignore + $args['file']->setPostFilename( $file ); + } else { + $args['file'] = '@' . $args['file']; + } } $call_args = array( @@ -374,8 +415,25 @@ public function upload( $file, $args, $headers = array() ) { ); $result = $this->call( $url, $call_args, 'post' ); + // Hook in flag to allow for non accessible URLS. + if ( is_wp_error( $result ) ) { + $error = $result->get_error_message(); + /** + * If there's an error and the file is a URL in the error message, + * it's likely due to CURL or the location does not support URL file attachments. + * In this case, we'll flag and disable it and try again with a local file. + */ + if ( empty( $disable_https_fetch ) && false !== strpos( $error, $args['file'] ) ) { + // URLS are not remotely available, try again as a file. + set_transient( '_cld_disable_http_upload', true, DAY_IN_SECONDS ); + // Remove URL file. + unset( $args['file'] ); + + return $this->upload( $attachment_id, $args ); + } + } if ( true === $tempfile ) { - unlink( $file ); //phpcs:ignore + unlink( $args['file'] ); //phpcs:ignore } return $result; @@ -434,12 +492,11 @@ public function destroy( $type, $options ) { /** * Context update of an asset. * - * @param string $file Filename of file. Ignored in this instance since it's a dynamic method call. - * @param array $args Array of options to update. + * @param array $args Array of options to update. * * @return array|\WP_Error */ - public function context( $file, $args ) { + public function context( $args ) { $url = $this->url( $args['resource_type'], 'context', true ); $options = array( diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/media/class-filter.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/media/class-filter.php index 4bfab0b95..d1edde186 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/media/class-filter.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/media/class-filter.php @@ -372,16 +372,16 @@ public function filter_out_local( $content ) { /** * Return a Cloudinary URL for an attachment used in JS. * - * @uses filter:wp_prepare_attachment_for_js - * - * @param array $attachment The attachment array to be used in JS. + * @param array $attachment The attachment response array. * * @return array + * @uses filter:wp_prepare_attachment_for_js + * */ public function filter_attachment_for_js( $attachment ) { - $cloudinary_id = $this->media->cloudinary_id( $attachment['id'] ); + $cloudinary_id = $this->media->get_cloudinary_id( $attachment['id'] ); - if ( false !== $cloudinary_id ) { + if ( $cloudinary_id ) { $transformations = array(); if ( ! empty( $attachment['transformations'] ) ) { @@ -392,13 +392,20 @@ public function filter_attachment_for_js( $attachment ) { $attachment['url'] = $this->media->cloudinary_url( $attachment['id'], false, $transformations ); $attachment['public_id'] = $attachment['type'] . '/upload/' . $this->media->get_public_id( $attachment['id'] ); - } - if ( empty( $attachment['transformations'] ) ) { - $transformations = $this->media->get_transformation_from_meta( $attachment['id'] ); - - if ( $transformations ) { - $attachment['transformations'] = $transformations; + if ( empty( $attachment['transformations'] ) ) { + $transformations = $this->media->get_transformation_from_meta( $attachment['id'] ); + + if ( $transformations ) { + $attachment['transformations'] = $transformations; + } + } + + // Ensure the sizes has the transformations and are converted URLS. + if ( ! empty( $attachment['sizes'] ) ) { + foreach ( $attachment['sizes'] as &$size ) { + $size['url'] = $this->media->convert_url( basename( $size['url'] ), $attachment['id'], $transformations ); + } } } @@ -421,14 +428,14 @@ public function filter_attachment_for_rest( $attachment ) { $cloudinary_id = $this->media->cloudinary_id( $attachment->data['id'] ); - if ( false !== $cloudinary_id ) { + if ( $cloudinary_id ) { $attachment->data['source_url'] = $this->media->cloudinary_url( $attachment->data['id'], false ); } - + if ( $has_transformations = ! empty( $this->media->get_transformation_from_meta( $attachment->data['id'] ) ) ) { $attachment->data['transformations'] = $has_transformations; } - + return $attachment; } @@ -671,45 +678,6 @@ public function filter_image_block_pre_render( $block, $source_block ) { return $block; } - /** - * Attempt to set the width and height for SVGs. - * - * @param array|false $image The image details. - * @param int $attachment_id The attachment ID. - * @param string|int[] $size The requested image size. - * - * @return array|false - */ - public function filter_svg_image_size( $image, $attachment_id, $size ) { - if ( is_array( $image ) && preg_match('/\.svg$/i', $image[0] ) && $image[1] <= 1 ) { - $image[1] = $image[2] = null; - - if ( is_array( $size ) ) { - $image[1] = $size[0]; - $image[2] = $size[1]; - } elseif ( false !== ( $xml = simplexml_load_file( $image[0] ) ) ) { - $attr = $xml->attributes(); - $viewbox = explode( ' ', $attr->viewBox ); - - // Get width - if ( isset( $attr->width ) && preg_match( '/\d+/', $attr->width, $value ) ) { - $image[1] = (int) $value[0]; - } elseif ( 4 === count( $viewbox ) ) { - $image[1] = (int) $viewbox[2]; - } - - // Get height - if ( isset( $attr->height ) && preg_match( '/\d+/', $attr->height, $value ) ) { - $image[2] = (int) $value[0]; - } elseif ( 4 === count( $viewbox ) ) { - $image[2] = (int) $viewbox[3]; - } - } - } - - return $image; - } - /** * Setup hooks for the filters. */ @@ -719,7 +687,7 @@ public function setup_hooks() { add_filter( 'the_editor_content', array( $this, 'filter_out_local' ) ); add_filter( 'the_content', array( $this, 'filter_out_local' ), 9 ); // Early to hook before responsive srcsets. - add_filter( 'wp_prepare_attachment_for_js', array( $this, 'filter_attachment_for_js' ) ); + add_filter( 'wp_prepare_attachment_for_js', array( $this, 'filter_attachment_for_js' ), 11 ); // Add support for custom header. add_filter( 'get_header_image_tag', array( $this, 'filter_out_local' ) ); @@ -747,8 +715,5 @@ function ( $type ) use ( $filter ) { // Filter for block rendering. add_filter( 'render_block_data', array( $this, 'filter_image_block_pre_render' ), 10, 2 ); - - // Try to get SVGs size. - add_filter( 'wp_get_attachment_image_src', array( $this, 'filter_svg_image_size' ), 10, 3 ); } } diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/media/class-upgrade.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/media/class-upgrade.php index 29430a9c4..6dafffa47 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/media/class-upgrade.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/media/class-upgrade.php @@ -34,20 +34,6 @@ class Upgrade { */ private $sync; - /** - * The cron frequency to ensure that the queue is progressing. - * - * @var int - */ - protected $cron_frequency; - - /** - * The cron offset since the last update. - * - * @var int - */ - protected $cron_start_offset; - /** * Filter constructor. * @@ -56,87 +42,9 @@ class Upgrade { public function __construct( \Cloudinary\Media $media ) { $this->media = $media; $this->sync = $media->plugin->components['sync']; - - $this->cron_frequency = apply_filters( 'cloudinary_cron_frequency', 600 ); - $this->cron_start_offset = apply_filters( 'cloudinary_cron_start_offset', 60 ); - $this->setup_hooks(); } - /** - * Check's if an attachment is from a previous version of Cloudinary. - * - * @param string|bool $cloudinary_id The cloudinary ID. Should be false. - * @param int $attachment_id The attachment ID to convert. - * - * @return string|bool - */ - public function check_cloudinary_version( $cloudinary_id, $attachment_id ) { - - if ( false === $cloudinary_id ) { - // Backwards compat. - $meta = wp_get_attachment_metadata( $attachment_id ); - if ( ! empty( $meta[ Sync::META_KEYS['cloudinary'] ] ) ) { - return $cloudinary_id; // Current version. - } - $public_id = $this->media->get_post_meta( $attachment_id, Sync::META_KEYS['public_id'], true ); - - /* - * If we still have $meta['cloudinary'] but already have $public_id, then conversion process is already underway. - * In that case, don't call convert_cloudinary_version() again, since it would make a duplicate background_request. - */ - if ( ! empty( $meta['cloudinary'] ) && empty( $public_id ) ) { - $cloudinary_id = $this->convert_cloudinary_version( $attachment_id ); - } elseif ( ! empty( $public_id ) ) { - // Has public ID, but not fully down synced. - $cloudinary_id = $public_id; - } - } else { - // Backwards compat. - $folder_sync = $this->media->get_post_meta( $attachment_id, Sync::META_KEYS['folder_sync'], true ); - if ( 0 === strlen( $folder_sync ) ) { - // Does not exist, add it to be compatible with v1.2.2. - $public_id = $this->media->get_post_meta( $attachment_id, Sync::META_KEYS['public_id'], true ); - // Set the folder sync to 0 to flag it by default as not synced. - $this->media->update_post_meta( $attachment_id, Sync::META_KEYS['folder_sync'], '0' ); - if ( false !== strpos( $public_id, '/' ) ) { - $path = pathinfo( $public_id ); - $asset_folder = trailingslashit( $path['dirname'] ); - $cloudinary_folder = trailingslashit( $this->media->plugin->config['settings']['sync_media']['cloudinary_folder'] ); - if ( $asset_folder === $cloudinary_folder ) { - // The asset folder matches the defined cloudinary folder, flag it as being in a folder sync. - $this->media->update_post_meta( $attachment_id, Sync::META_KEYS['folder_sync'], '1' ); - } - } - } - } - - return $cloudinary_id; - } - - /** - * Checks the status of the media item. - * - * @param array $status Array of state and note. - * @param int $attachment_id The attachment id. - * - * @return array - */ - public function filter_status( $status, $attachment_id ) { - - if ( get_post_meta( $attachment_id, Sync::META_KEYS['downloading'] ) ) { - $status['state'] = 'info downloading'; - $status['note'] = __( 'Downloading', 'cloudinary' ); - } - - if ( get_post_meta( $attachment_id, Sync::META_KEYS['syncing'] ) ) { - $status['state'] = 'info syncing'; - $status['note'] = __( 'Syncing metadata', 'cloudinary' ); - } - - return $status; - } - /** * Convert an image post that was created from Cloudinary v1. * @@ -146,91 +54,86 @@ public function filter_status( $status, $attachment_id ) { */ public function convert_cloudinary_version( $attachment_id ) { - $file = get_post_meta( $attachment_id, '_wp_attached_file', true ); - $path = wp_parse_url( $file, PHP_URL_PATH ); - $media = $this->media; - $parts = explode( '/', $path ); - $parts = array_map( - function ( $val ) use ( $media ) { + $file = get_post_meta( $attachment_id, '_wp_attached_file', true ); + if ( wp_http_validate_url( $file ) ) { + // Version 1 upgrade. + $path = wp_parse_url( $file, PHP_URL_PATH ); + $media = $this->media; + $parts = explode( '/', ltrim( $path, '/' ) ); + $cloud_name = null; + $asset_version = 1; + $asset_transformations = array(); + $id_parts = array(); + foreach ( $parts as $val ) { if ( empty( $val ) ) { - return false; + continue; } - if ( $val === $media->credentials['cloud_name'] ) { - return false; + if ( is_null( $cloud_name ) ) { + // Cloudname will always be the first item. + $cloud_name = md5( $val ); + continue; } if ( in_array( $val, [ 'image', 'video', 'upload' ], true ) ) { - return false; + continue; } $transformation_maybe = $media->get_transformations_from_string( $val ); if ( ! empty( $transformation_maybe ) ) { - return false; + $asset_transformations = $transformation_maybe; + continue; } if ( substr( $val, 0, 1 ) === 'v' && is_numeric( substr( $val, 1 ) ) ) { - return false; + $asset_version = substr( $val, 1 ); + continue; } - return $val; - }, - $parts - ); - // Build public_id. - $parts = array_filter( $parts ); - $public_id = implode( '/', $parts ); - // Remove extension. - $path = pathinfo( $public_id ); - $public_id = strstr( $public_id, '.' . $path['extension'], true ); - $this->media->update_post_meta( $attachment_id, Sync::META_KEYS['public_id'], $public_id ); - - // Flag the download - update_post_meta( $attachment_id, Sync::META_KEYS['downloading'], true ); - delete_post_meta( $attachment_id, Sync::META_KEYS['syncing'] ); - - if ( ! wp_next_scheduled( 'cloudinary_resume_upgrade' ) ) { - wp_schedule_single_event( time() + $this->cron_frequency, 'cloudinary_resume_upgrade' ); - } - - if ( ! defined( 'DOING_BULK_SYNC' ) ) { - $this->sync->managers['upload']->add_to_sync( $attachment_id ); // Auto sync if upgrading outside of bulk sync. + $id_parts[] = $val; + } + // Build public_id. + $parts = array_filter( $id_parts ); + $public_id = implode( '/', $parts ); + // Remove extension. + $path = pathinfo( $public_id ); + $public_id = str_replace( $path['basename'], $path['filename'], $public_id ); + $this->media->update_post_meta( $attachment_id, Sync::META_KEYS['public_id'], $public_id ); + $this->media->update_post_meta( $attachment_id, Sync::META_KEYS['version'], $asset_version ); + if ( ! empty( $asset_transformations ) ) { + $this->media->update_post_meta( $attachment_id, Sync::META_KEYS['transformation'], $asset_transformations ); + } + $this->sync->set_signature_item( $attachment_id, 'cloud_name', $cloud_name ); + } else { + // v2 upgrade. + $public_id = $this->media->get_public_id( $attachment_id ); + // Check folder sync in order. + if ( $this->media->is_folder_synced( $attachment_id ) ) { + $public_id_folder = ltrim( dirname( $this->media->get_post_meta( $attachment_id, Sync::META_KEYS['public_id'], true ) ), '.' ); + $test_signature = md5( false ); + $folder_signature = md5( $public_id_folder ); + $signature = $this->sync->get_signature( $attachment_id ); + if ( $folder_signature !== $test_signature && $test_signature === $signature['folder'] ) { + // The test signature is a hashed false, which is how non-folder-synced items got hashed. + // Indicating this is broken link. + $this->media->delete_post_meta( $attachment_id, Sync::META_KEYS['folder_sync'] ); + $this->media->delete_post_meta( $attachment_id, Sync::META_KEYS['sync_error'] ); // Remove any errors from upgrade. they are outdated. + $this->sync->set_signature_item( $attachment_id, 'folder' ); + } + // Since it had a folder sync, it was already matched with existing, so set the suffix. + $this->sync->set_signature_item( $attachment_id, 'suffix' ); + } } + $this->media->update_post_meta( $attachment_id, Sync::META_KEYS['plugin_version'], $this->media->plugin->version ); + $this->sync->set_signature_item( $attachment_id, 'upgrade' ); + $this->sync->set_signature_item( $attachment_id, 'public_id' ); + // Get a new uncached signature. + $this->sync->get_signature( $attachment_id, true ); return $public_id; } - /** - * Maybe resume the upgrading assets. - * This is a fallback mechanism to resume the upgrade when it stops unexpectedly. - * - * @return void - */ - public function maybe_resume_upgrade() { - global $wpdb; - - $assets = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery - $wpdb->prepare( - "SELECT post_id - FROM $wpdb->postmeta - WHERE meta_key = %s", - Sync::META_KEYS['downloading'] - ) - ); - - if ( ! empty( $assets ) ) { - wp_schedule_single_event( time() + $this->cron_frequency, 'cloudinary_resume_upgrade' ); - - foreach ( $assets as $asset ) { - $this->sync->managers['upload']->add_to_sync( $asset ); - } - } - } /** * Setup hooks for the filters. */ public function setup_hooks() { - add_filter( 'validate_cloudinary_id', array( $this, 'check_cloudinary_version' ), 10, 2 ); // Priority 10, to allow prep_on_demand_upload. - - // Show sync status. - add_filter( 'cloudinary_media_status', array( $this, 'filter_status' ), 20, 2 ); // Add a redirection to the new plugin settings, from the old plugin. if ( is_admin() ) { @@ -242,7 +145,5 @@ public function setup_hooks() { } } ); } - - add_action( 'cloudinary_resume_upgrade', array( $this, 'maybe_resume_upgrade' ) ); } } diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-delete-sync.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-delete-sync.php index 0d8c4f817..9881e2c78 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-delete-sync.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-delete-sync.php @@ -57,15 +57,15 @@ public function can_delete_asset( $all_caps, $caps, $args ) { // The args are indexed, list them in named variables to better understand. list( $request_cap, , $post_id ) = $args; - if ( 'delete_post' === $request_cap && ! empty( $all_caps['delete_posts'] ) && 'attachment' === get_post_type( $post_id ) ) { + if ( $this->plugin->components['media']->is_media( $post_id ) && 'delete_post' === $request_cap && ! empty( $all_caps['delete_posts'] ) ) { // Check if is pending. - if ( ! $this->plugin->components['sync']->is_synced( $post_id ) && $this->plugin->components['sync']->managers['upload']->is_pending( $post_id ) ) { + if ( ! $this->plugin->components['sync']->is_synced( $post_id ) && $this->plugin->components['sync']->is_pending( $post_id ) ) { // Check for errors. $has_error = $this->plugin->components['media']->get_post_meta( $post_id, Sync::META_KEYS['sync_error'], true ); if ( empty( $has_error ) ) { $all_caps['delete_posts'] = false; - $action = filter_input( INPUT_GET, 'action', FILTER_SANITIZE_STRING ); + $action = filter_input( INPUT_GET, 'action', FILTER_SANITIZE_STRING ); if ( ! empty( $action ) && '-1' !== $action ) { wp_die( esc_html__( 'Sorry, you can’t delete an asset until it has fully synced with Cloudinary. Try again once syncing is complete.', 'cloudinary' ) ); } @@ -100,7 +100,7 @@ public function delete_asset( $post_id ) { $parts = explode( '/', $public_id ); $cloudinary_folder = $this->plugin->config['settings']['sync_media']['cloudinary_folder'] ? $this->plugin->config['settings']['sync_media']['cloudinary_folder'] : ''; if ( $cloudinary_folder === $parts[0] ) { - $type = $this->plugin->components['sync']->managers['push']->get_resource_type( $post_id ); + $type = $this->plugin->components['media']->get_media_type( $post_id ); $options = array( 'public_id' => $public_id, 'invalidate' => true, // clear from CDN cache as well. diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-download-sync.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-download-sync.php index cf7c65a99..d2b0eaca1 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-download-sync.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-download-sync.php @@ -25,6 +25,20 @@ class Download_Sync { */ private $plugin; + /** + * Holds the Media component. + * + * @var \Cloudinary\Media + */ + protected $media; + + /** + * Holds the Sync component. + * + * @var \Cloudinary\Sync + */ + protected $sync; + /** * Download_Sync constructor. * @@ -80,8 +94,6 @@ public function rest_can_upload_files( \WP_REST_Request $request ) { * * @param int $attachment_id The attachment ID. * @param string $error The error text to return. - * - * @return \WP_Error */ public function handle_failed_download( $attachment_id, $error ) { // @todo: Place a handler to catch the error for logging. @@ -103,10 +115,9 @@ public function rest_download_asset( \WP_REST_Request $request ) { $attachment_id = $request->get_param( 'attachment_id' ); $file_path = $request->get_param( 'src' ); - $file_name = $request->get_param( 'filename' ); $transformations = (array) $request->get_param( 'transformations' ); - $response = $this->download_asset( $attachment_id, $file_path, $file_name, $transformations ); + $response = $this->import_asset( $attachment_id, $file_path, $transformations ); if ( is_wp_error( $response ) ) { $this->handle_failed_download( $attachment_id, $response->get_error_message() ); } @@ -168,40 +179,34 @@ function ( $val ) use ( $media ) { } } - return $this->download_asset( $attachment_id, $file, basename( $file ), $media->get_transformations_from_string( $file ) ); + return $this->download_asset( $attachment_id, $file ); } /** * Download an attachment source to the file system. * - * @param int $attachment_id The attachment ID. - * @param string $file_path The path of the file. - * @param string $file_name The filename. - * @param array|null $transformations + * @param int $attachment_id The attachment ID. + * @param string $source The optional source to download. * * @return array|\WP_Error */ - public function download_asset( $attachment_id, $file_path, $file_name, $transformations = null ) { - - // Get the image and update the attachment. - require_once ABSPATH . WPINC . '/class-http.php'; - require_once ABSPATH . 'wp-admin/includes/file.php'; + public function download_asset( $attachment_id, $source = null ) { require_once ABSPATH . 'wp-admin/includes/image.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; - - // Fetch the asset. + if ( empty( $source ) ) { + $cloudinary_id = $this->media->get_cloudinary_id( $attachment_id ); + $source = $this->media->cloudinary_url( $attachment_id, array(), array(), $cloudinary_id ); + } + $file_name = basename( $source ); try { // Prime a file to stream to. $upload = wp_upload_bits( $file_name, null, 'temp' ); if ( ! empty( $upload['error'] ) ) { return new \WP_Error( 'download_error', $upload['error'] ); } - // If the public_id of an asset includes a file extension, a derived item will have the extension duplicated, but not in the source URL. - // This creates a 404. So, instead, we get the actual file name, and use that over the file name that the source url has. - $source_path = dirname( $file_path ); // Stream file to primed file. $response = wp_safe_remote_get( - $source_path . '/' . $file_name, + $source, array( 'timeout' => 300, // phpcs:ignore 'stream' => true, @@ -225,21 +230,65 @@ public function download_asset( $attachment_id, $file_path, $file_name, $transfo // Prepare the asset. update_attached_file( $attachment_id, $upload['file'] ); + $old_meta = wp_get_attachment_metadata( $attachment_id ); ob_start(); // Catch possible errors in WordPress's ID3 module when setting meta for transformed videos. $meta = wp_generate_attachment_metadata( $attachment_id, $upload['file'] ); - $captured_errors = ob_get_clean(); + $captured_errors = ob_get_clean(); // Capture issues. + // Be sure to record v2 meta. + if ( ! empty( $old_meta[ Sync::META_KEYS['cloudinary'] ] ) ) { + $meta[ Sync::META_KEYS['cloudinary'] ] = $old_meta[ Sync::META_KEYS['cloudinary'] ]; + } wp_update_attachment_metadata( $attachment_id, $meta ); + // Update the folder synced flag. + $public_id = $this->media->get_public_id( $attachment_id ); + $asset_folder = strpos( $public_id, '/' ) ? dirname( $public_id ) : '/'; + $cloudinary_folder = untrailingslashit( $this->media->get_cloudinary_folder() ); + if ( $asset_folder === $cloudinary_folder ) { + $this->media->update_post_meta( $attachment_id, Sync::META_KEYS['folder_sync'], true ); + } + // Generate signatures. + $this->sync->set_signature_item( $attachment_id, 'options' ); + $this->sync->set_signature_item( $attachment_id, 'cloud_name' ); + $this->sync->set_signature_item( $attachment_id, 'suffix' ); + $this->sync->set_signature_item( $attachment_id, 'download' ); + $this->sync->set_signature_item( $attachment_id, 'file' ); + $this->sync->set_signature_item( $attachment_id, 'folder' ); } catch ( \Exception $e ) { return new \WP_Error( 'download_error', $e->getMessage() ); } - $attachment = wp_prepare_attachment_for_js( $attachment_id ); + return $upload; + } + + /** + * Download an attachment source to the file system. + * + * @param int $attachment_id The attachment ID. + * @param string $file_path The path of the file. + * @param array|null $transformations + * + * @return array|\WP_Error + */ + public function import_asset( $attachment_id, $file_path, $transformations = null ) { + + // Get the image and update the attachment. + require_once ABSPATH . WPINC . '/class-http.php'; + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/image.php'; + require_once ABSPATH . 'wp-admin/includes/media.php'; - // Log errors if captured. - if ( ! empty( $captured_errors ) ) { - $attachment['_captured_errors'] = $captured_errors; + // Fetch the asset. + try { + // Prime a file to stream to. + $upload = $this->download_asset( $attachment_id, $file_path ); + + } catch ( \Exception $e ) { + return new \WP_Error( 'download_error', $e->getMessage() ); } + + $attachment = wp_prepare_attachment_for_js( $attachment_id ); + // Do transformations. if ( 'image' === $attachment['type'] ) { // Get the cloudinary_id from public_id not Media::cloudinary_id(). @@ -257,4 +306,13 @@ public function download_asset( $attachment_id, $file_path, $file_name, $transfo return $response; } + + /** + * Setup this component. + */ + public function setup() { + // Setup components. + $this->media = $this->plugin->components['media']; + $this->sync = $this->plugin->components['sync']; + } } diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-push-sync.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-push-sync.php index cd48f0c6e..add6cd69a 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-push-sync.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-push-sync.php @@ -7,7 +7,6 @@ namespace Cloudinary\Sync; -use Cloudinary\Connect\Api; use Cloudinary\Sync; /** @@ -27,25 +26,46 @@ class Push_Sync { public $plugin; /** - * Holds a list of already built options array, since it can be heavy action. + * Holds the ID of the last attachment synced. * - * @var array + * @var int */ - private $upload_options = array(); + protected $post_id; /** - * Hold the list or available sync types. + * Holds the media component. * - * @var array + * @var \Cloudinary\Media */ - private $sync_types; + protected $media; /** - * Holds the ID of the last attachment synced. + * Holds the sync component. * - * @var int + * @var \Cloudinary\Sync */ - protected $post_id; + protected $sync; + + /** + * Holds the connect component. + * + * @var \Cloudinary\Connect + */ + protected $connect; + + /** + * Holds the Rest_API component. + * + * @var \Cloudinary\REST_API + */ + protected $api; + + /** + * Holds the sync queue object. + * + * @var \Cloudinary\Sync\Sync_Queue + */ + public $queue; /** * Push_Sync constructor. @@ -54,18 +74,6 @@ class Push_Sync { */ public function __construct( \Cloudinary\Plugin $plugin ) { $this->plugin = $plugin; - - // Define the sync types and their option keys. - $sync_types = array( - 'cloud_name' => 'upload', - 'folder' => 'upload', - 'file' => 'upload', - 'public_id' => 'rename', - 'breakpoints' => 'explicit', - 'options' => 'context', - ); - $this->sync_types = apply_filters( 'cloudinary_sync_types', $sync_types ); - $this->register_hooks(); } @@ -76,6 +84,21 @@ private function register_hooks() { add_filter( 'cloudinary_api_rest_endpoints', array( $this, 'rest_endpoints' ) ); } + /** + * Setup this component. + */ + public function setup() { + // Setup components. + $this->media = $this->plugin->components['media']; + $this->sync = $this->plugin->components['sync']; + $this->connect = $this->plugin->components['connect']; + $this->api = $this->plugin->components['api']; + $this->queue = $this->sync->managers['queue']; + + add_action( 'cloudinary_run_queue', array( $this, 'process_queue' ) ); + add_action( 'cloudinary_sync_items', array( $this, 'process_assets' ) ); + } + /** * Add endpoints to the \Cloudinary\REST_API::$endpoints array. * @@ -100,26 +123,18 @@ public function rest_endpoints( $endpoints ) { ); $endpoints['process'] = array( - 'method' => \WP_REST_Server::CREATABLE, - 'callback' => array( $this, 'rest_push_attachments' ), - 'args' => array(), - 'permission_callback' => array( $this, 'rest_verify_nonce' ), + 'method' => \WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'process_sync' ), + 'args' => array(), ); - return $endpoints; - } - - /** - * General nonce based auth. - * - * @param \WP_REST_Request $request The request. - * - * @return bool - */ - public function rest_verify_nonce( \WP_REST_Request $request ) { - $nonce = $request->get_param( 'nonce' ); + $endpoints['queue'] = array( + 'method' => \WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'process_queue' ), + 'args' => array(), + ); - return wp_verify_nonce( $nonce, 'wp_rest' ); + return $endpoints; } /** @@ -143,7 +158,7 @@ public function rest_get_queue_status() { return rest_ensure_response( array( 'success' => true, - 'data' => $this->plugin->components['sync']->managers['queue']->get_queue_status(), + 'data' => $this->queue->get_queue_status(), ) ); } @@ -153,531 +168,112 @@ public function rest_get_queue_status() { * * @param \WP_REST_Request $request The request. * - * @return mixed|\WP_REST_Response + * @return \WP_REST_Response */ public function rest_start_sync( \WP_REST_Request $request ) { - $stop = $request->get_param( 'stop' ); - $queue = $this->plugin->components['sync']->managers['queue']->get_queue(); - if ( empty( $queue['pending'] ) || ! empty( $stop ) ) { - $this->plugin->components['sync']->managers['queue']->stop_queue(); + $stop = $request->get_param( 'stop' ); + $status = $this->queue->get_queue_status(); + if ( empty( $status['pending'] ) || ! empty( $stop ) ) { + $this->queue->stop_queue(); return $this->rest_get_queue_status(); // Nothing to sync. } - $this->plugin->components['sync']->managers['queue']->start_queue(); - return $this->call_thread(); - - } - - /** - * Pushes attachments via WP REST API. - * - * @param \WP_REST_Request $request The request. - * - * @return mixed|\WP_REST_Response - */ - public function rest_push_attachments( \WP_REST_Request $request ) { - // Get thread ID. - $last_id = $request->get_param( 'last_id' ); - $last_result = $request->get_param( 'last_result' ); - - // Get attachment_id in case this is a single direct request to upload. - $attachments = $request->get_param( 'attachment_ids' ); - - // If not a single request, process based on queue. - if ( ! empty( $attachments ) ) { - - // If a single specified ID, push and return response. - $ids = array_map( 'intval', $attachments ); - $stat = $this->push_attachments( $ids ); - - return rest_ensure_response( - array( - 'success' => true, - 'data' => $stat, - ) - ); - } - - // Process queue based. - if ( ! empty( $last_id ) && ! empty( $last_result ) ) { - $this->plugin->components['sync']->managers['queue']->mark( $last_id, $last_result ); - } - - if ( ! $this->plugin->components['sync']->managers['queue']->is_running() ) { // Check it wasn't stopped. - return $this->rest_get_queue_status(); - } - - $this->post_id = $this->plugin->components['sync']->managers['queue']->get_post(); - - // No post, end of queue. - if ( empty( $this->post_id ) ) { - $this->plugin->components['sync']->managers['queue']->stop_queue(); - - return $this->rest_get_queue_status(); - } - - add_action( 'shutdown', array( $this, 'resume_queue' ) ); + $this->queue->start_queue(); return $this->rest_get_queue_status(); } /** - * Resume the bulk sync. + * Process asset sync. * - * @return bool|\WP_REST_Response - */ - public function resume_queue() { - // Check if there is a Cloudinary ID in case this was synced on-demand before being processed by the queue. - add_filter( 'cloudinary_on_demand_sync_enabled', '__return_false' ); // Disable the on-demand sync since we want the status. - define( 'DOING_BULK_SYNC', true ); // Define bulk sync in action. - - if ( ! $this->plugin->components['sync']->is_synced( $this->post_id ) ) { - $stat = $this->push_attachments( array( $this->post_id ) ); - if ( ! empty( $stat['processed'] ) ) { - $result = 'done'; - } else { - $result = 'error'; - } - } else { - /** - * If a Cloudinary ID was found, set as done. - * In the case of an upgrade, This would have been pushed to a background conversion. - */ - $result = 'done'; - } - - return $this->call_thread( $this->post_id, $result ); - } - - /** - * Start a new call. - * - * @param int $last_id The last ID that was processed. - * @param string $last_result The last result. + * @param int|array $attachments An attachment ID or an array of ID's. * - * @return mixed|\WP_REST_Response. - */ - private function call_thread( $last_id = null, $last_result = null ) { - - // Add last results. - $params = array(); - if ( null !== $last_id && null !== $last_result ) { - $params['last_id'] = $last_id; - $params['last_result'] = $last_result; - } - - // Setup background call to continue the queue. - $this->plugin->components['api']->background_request( 'process', $params ); - - return $this->rest_get_queue_status(); - } - - /** - * @param int|\WP_Post $attachment The attachment post or id to get resource type for. - * - * @return string - */ - public function get_resource_type( $attachment ) { - if ( is_numeric( $attachment ) ) { - $attachment = get_post( $attachment ); - } - - // Deal with compound mime types. - $type = explode( '/', $attachment->post_mime_type ); - - return array_shift( $type ); - } - - /** - * Get the type of sync the attachment should do. - * - * @param int|\WP_Post $attachment The attachment to get the required sync type for. - * - * @return string + * @return array */ - private function get_sync_type( $attachment ) { - if ( is_numeric( $attachment ) ) { - $attachment = get_post( $attachment ); - } - - $type = 'upload'; - // Check for explicit (has public_id, but no breakpoints). - $attachment_signature = $this->plugin->components['sync']->get_signature( $attachment->ID ); - if ( empty( $attachment_signature ) ) { - if ( ! empty( $attachment->{Sync::META_KEYS['public_id']} ) ) { - // Has a public id but no signature, explicit update to complete download. - $type = 'explicit'; - } - // fallback to upload. - } else { - // Has signature find differences and use specific sync method. - $required_signature = $this->plugin->components['sync']->generate_signature( $attachment->ID ); - foreach ( $required_signature as $key => $signature ) { - if ( ( ! isset( $attachment_signature[ $key ] ) || $attachment_signature[ $key ] !== $signature ) && isset( $this->sync_types[ $key ] ) ) { - return $this->sync_types[ $key ]; + public function process_assets( $attachments = array() ) { + + $stat = array(); + // If a single specified ID, push and return response. + $ids = array_map( 'intval', (array) $attachments ); + // Handle based on Sync Type. + foreach ( $ids as $attachment_id ) { + // Flag attachment as being processed. + update_post_meta( $attachment_id, Sync::META_KEYS['syncing'], time() ); + while ( $type = $this->sync->get_sync_type( $attachment_id, false ) ) { + if ( isset( $stat[ $attachment_id ][ $type ] ) ) { + // Loop prevention. + break; } + $stat[ $attachment_id ][ $type ] = $this->sync->run_sync_method( $type, 'sync', $attachment_id ); + } + // remove pending. + if ( $this->sync->is_pending( $attachment_id ) ) { + $this->media->delete_post_meta( $attachment_id, Sync::META_KEYS['pending'] ); } - // If it gets here, the signature is identical, so set as no update. - $type = new \WP_Error( 'attachment_synced', __( 'Attachment is already fully synced.', 'cloudinary' ) ); + // Record Process log. + $this->media->update_post_meta( $attachment_id, Sync::META_KEYS['process_log'], $stat[ $attachment_id ] ); + // Remove processing flag. + delete_post_meta( $attachment_id, Sync::META_KEYS['syncing'] ); + + // Create synced post meta as a way to search for synced / unsynced items. + update_post_meta( $attachment_id, Sync::META_KEYS['public_id'], $this->media->get_public_id( $attachment_id ) ); } - // Return the type. - return $type; + return $stat; } /** - * Prepare an attachment for upload. + * Process assets to sync vai WP REST API. * - * @param int|\WP_Post $post The attachment to prepare. - * @param bool $down_sync Flag to determine if a missing file starts a downsync. + * @param \WP_REST_Request $request The request. * - * @return array|\WP_Error + * @return mixed|\WP_REST_Response */ - public function prepare_upload( $post, $down_sync = false ) { - - if ( is_numeric( $post ) ) { - $post = get_post( $post ); - } - - if ( empty( $post ) ) { - return new \WP_Error( 'attachment_post_get', __( 'Could not retrieve the attachment post.', 'cloudinary' ) ); - } - - if ( empty( $this->upload_options[ $post->ID ] ) ) { - - if ( 'attachment' !== $post->post_type ) { - return new \WP_Error( 'attachment_post_expected', __( 'An attachment post was expected.', 'cloudinary' ) ); - } - - // Get the media component. - $media = $this->plugin->components['media']; - - // First check if this has a file and it can be uploaded. - $file = get_attached_file( $post->ID ); - $file_size = 0; - $downsync = false; - if ( empty( $file ) ) { - return new \WP_Error( 'attachment_no_file', __( 'Attachment did not have a file.', 'cloudinary' ) ); - } elseif ( ! file_exists( $file ) ) { - // May be an old upload type. - $src = get_post_meta( $post->ID, '_wp_attached_file', true ); - if ( $media->is_cloudinary_url( $src ) ) { - // Download first maybe. - if ( true === $down_sync ) { - $download = $this->plugin->components['sync']->managers['download']->down_sync( $post->ID ); - if ( is_wp_error( $download ) ) { - delete_post_meta( $post->ID, Sync::META_KEYS['downloading'] ); - update_post_meta( $post->ID, Sync::META_KEYS['sync_error'], $download->get_error_message() ); - - return new \WP_Error( 'attachment_download_error', $download->get_error_message() ); - } - $file = get_attached_file( $post->ID ); - $file_size = filesize( $file ); - $downsync = true; - } - } - } else { - $file_size = filesize( $file ); - } - - $resource_type = $this->get_resource_type( $post ); - $max_size = ( 'image' === $resource_type ? 'max_image_size' : 'max_video_size' ); - - if ( ! empty( $this->plugin->components['connect']->usage[ $max_size ] ) && $file_size > $this->plugin->components['connect']->usage[ $max_size ] ) { - $max_size_hr = size_format( $this->plugin->components['connect']->usage[ $max_size ] ); - - // translators: variable is file size. - $error = sprintf( __( 'File size exceeds the maximum of %s. This media asset will be served from WordPress.', 'cloudinary' ), $max_size_hr ); - $media->delete_post_meta( $post->ID, Sync::META_KEYS['pending'] ); // Remove Flag. - - // Cleanup flags - delete_post_meta( $post->ID, Sync::META_KEYS['syncing'] ); - delete_post_meta( $post->ID, Sync::META_KEYS['downloading'] ); - - return new \WP_Error( 'upload_error', $error ); - } - - // If it's got a public ID, then this is an explicit update. - $settings = $this->plugin->config['settings']; - $public_id = $post->{Sync::META_KEYS['public_id']}; // use the __get method on the \WP_Post to get post_meta. - $cld_folder = trailingslashit( $settings['sync_media']['cloudinary_folder'] ); - if ( empty( $public_id ) ) { - $file_info = pathinfo( $file ); - $public_id = $cld_folder . $file_info['filename']; - } - - // Assume that the public_id is a root item. - $public_id_folder = ''; - $public_id_file = $public_id; - - // Check if in a lower level. - if ( false !== strpos( $public_id, '/' ) ) { - // Split the public_id into path and filename to allow filtering just the ID and not giving access to the path. - $public_id_info = pathinfo( $public_id ); - $public_id_folder = trailingslashit( $public_id_info['dirname'] ); - $public_id_file = $public_id_info['filename']; - } - // Check if this asset is a folder sync. - $folder_sync = $media->get_post_meta( $post->ID, Sync::META_KEYS['folder_sync'], true ); - if ( ! empty( $folder_sync ) && false === $downsync ) { - $public_id_folder = $cld_folder; // Ensure the public ID folder is constant. - } else { - // Not folder synced, so set the folder to the folder that the asset originally came from. - $cld_folder = $public_id_folder; - } - // Prepare upload options. - $options = array( - 'unique_filename' => false, - 'resource_type' => $resource_type, - 'public_id' => $public_id_file, - 'context' => array( - 'caption' => esc_attr( $post->post_title ), - 'alt' => $post->_wp_attachment_image_alt, - ), - ); - - // If in cloudinary folder, add the posts id to context. - if ( false !== $cld_folder ) { - $options['context']['wp_id'] = $post->ID; - } - - // Add breakpoints if we have an image. - $breakpoints = false; - if ( 'image' === $resource_type ) { - $meta = wp_get_attachment_metadata( $post->ID ); - // Get meta image size if non exists. - if ( empty( $meta ) ) { - $meta = array(); - $imagesize = getimagesize( $file ); - $meta['width'] = $imagesize[0]; - } - $max_width = $media->get_max_width(); - // Add breakpoints request options. - if ( ! empty( $settings['global_transformations']['enable_breakpoints'] ) ) { - $options['responsive_breakpoints'] = array( - 'create_derived' => true, - 'bytes_step' => $settings['global_transformations']['bytes_step'], - 'max_images' => $settings['global_transformations']['breakpoints'], - 'max_width' => $meta['width'] < $max_width ? $meta['width'] : $max_width, - 'min_width' => $settings['global_transformations']['min_width'], - ); - $transformations = $media->get_transformation_from_meta( $post->ID ); - if ( ! empty( $transformations ) ) { - $options['responsive_breakpoints']['transformation'] = Api::generate_transformation_string( $transformations ); - } - $breakpoints = array( - 'public_id' => $options['public_id'], - 'type' => 'upload', - 'responsive_breakpoints' => $options['responsive_breakpoints'], - 'context' => $options['context'], - ); - } - } - - /** - * Filter the options to allow other plugins to add requested options for uploading. - * - * @param array $options The options array. - * @param \WP_Post $post The attachment post. - * @param \Cloudinary\Sync The sync object instance. - * - * @return array - */ - $options = apply_filters( 'cloudinary_upload_options', $options, $post, $this ); - - // Combine context option to string. - $options['context'] = http_build_query( $options['context'], null, '|' ); - if ( ! empty( $breakpoints ) && is_array( $breakpoints['context'] ) ) { - $breakpoints['context'] = http_build_query( $breakpoints['context'], null, '|' ); - } - - // Restructure the path to the filename to allow correct placement in Cloudinary. - $public_id = ltrim( $public_id_folder . $options['public_id'], '/' ); - $return = array( - 'file' => $file, - 'folder' => ltrim( $cld_folder, '/' ), - 'public_id' => $public_id, - 'breakpoints' => array(), - 'options' => $options, - ); - $return['options']['public_id'] = $public_id; - if ( ! empty( $breakpoints ) ) { - $return['breakpoints'] = $breakpoints; - $return['breakpoints']['public_id'] = $public_id; // Stage public ID to folder for breakpoints. + public function process_sync( \WP_REST_Request $request ) { + $process_key = $request->get_param( 'process_key' ); + $note = 'no process key'; + if ( ! empty( $process_key ) ) { + $attachments = get_transient( $process_key ); + if ( ! empty( $attachments ) ) { + delete_transient( $process_key ); + + return rest_ensure_response( + array( + 'success' => true, + 'data' => $this->process_assets( $attachments ), + ) + ); } - $this->upload_options[ $post->ID ] = $return; - + $note = 'no attachments'; } - return $this->upload_options[ $post->ID ]; + return rest_ensure_response( + array( + 'success' => false, + 'note' => $note, + ) + ); } /** - * Push \WP_Post items in array to Cloudinary. - * - * Provides $large param for supporting video uploads. If post_mime_type is 'video' - * then large will be assumed. - * - * @see https://cloudinary.com/documentation/php_image_and_video_upload#php_image_upload - * @see https://cloudinary.com/documentation/php_image_and_video_upload#php_video_upload + * Resume the bulk sync. * - * @param mixed $attachments Array of \WP_Post (hopefully with `post_type='attachment'`. + * @param \WP_REST_Request $request The request. * - * @return array|\WP_Error + * @return void */ - public function push_attachments( $attachments ) { - - /** - * Holds the file names for successful and failed attachments. - * - * Also includes `start` time, `end` time and `duration`. - * - * @var array $stats - */ - $stats = array( - 'success' => array(), - 'fail' => array(), - 'start' => time(), - 'total' => count( $attachments ), - 'processed' => 0, - ); - // Get media component. - $media = $this->plugin->components['media']; - - // Go over each attachment. - foreach ( $attachments as $attachment ) { - $attachment = get_post( $attachment ); - // Clear upload option cache for this item to allow down sync. - $this->upload_options[ $attachment->ID ] = false; - - $upload = $this->prepare_upload( $attachment->ID, true ); - - // Filter out any attachments with problematic options. - if ( is_wp_error( $upload ) ) { - - /** - * Attachment is an error. - * - * @var \WP_Error $upload - */ - $stats['fail'][] = $upload->get_error_message(); - continue; + public function process_queue( \WP_REST_Request $request ) { + $thread = $request->get_param( 'thread' ); + $queue = $this->queue->get_thread_queue( $thread ); + + if ( ! empty( $queue ) && $this->queue->is_running() ) { + while ( $attachment_id = $this->queue->get_post( $thread ) ) { + $this->process_assets( $attachment_id ); + $this->queue->mark( $attachment_id, 'done' ); } - - - // Determine Sync type needed. - $sync_type = $this->get_sync_type( $attachment ); - - // Skip if sync type is an error. - if ( is_wp_error( $sync_type ) ) { - /** - * Sync type is an error. - * - * @var \WP_Error $sync_type - */ - $stats['fail'][] = $sync_type->get_error_message(); - continue; - } - - // 100MB restriction on normal upload. - $do_large = 'video' === $upload['options']['resource_type'] ? true : false; - - if ( ! $do_large ) { - - if ( 'explicit' === $sync_type ) { - // Explicit update. - $args = array( - 'public_id' => $upload['public_id'], - 'type' => 'upload', - ); - if ( ! empty( $upload['options']['responsive_breakpoints'] ) ) { - $args['responsive_breakpoints'] = $upload['options']['responsive_breakpoints']; - } - if ( ! empty( $upload['options']['context'] ) ) { - $args['context'] = $upload['options']['context']; - } - $result = $this->plugin->components['connect']->api->explicit( $args ); - } elseif ( 'rename' === $sync_type ) { - // Rename an asset. - $args = array( - 'from_public_id' => $media->get_post_meta( $attachment->ID, Sync::META_KEYS['public_id'] ), - 'to_public_id' => $upload['public_id'], - ); - $result = $this->plugin->components['connect']->api->{$upload['options']['resource_type']}( 'rename', 'POST', $args ); - } else { - // dynamic sync type.. - $result = $this->plugin->components['connect']->api->{$sync_type}( $upload['file'], $upload['options'] ); - } - } else { - // Large Upload. - $result = $this->plugin->components['connect']->api->upload_large( $upload['file'], $upload['options'] ); - } - - // Exceptions are handled by the Upload wrapper class and returned as \WP_Error, so check for it. - if ( is_wp_error( $result ) ) { - $error = $result->get_error_message(); - $stats['fail'][] = $error; - $media->update_post_meta( $attachment->ID, Sync::META_KEYS['sync_error'], $error ); - delete_post_meta( $attachment->ID, Sync::META_KEYS['syncing'] ); - continue; - } - - // Successful upload, so lets update meta. - if ( is_array( $result ) && ( array_key_exists( 'public_id', $result ) || array_key_exists( 'public_ids', $result ) ) ) { - $meta_data = array( - Sync::META_KEYS['signature'] => $this->plugin->components['sync']->generate_signature( $attachment->ID ), - ); - - // We only have a version if updating a file or an upload. - if ( ! empty( $result['version'] ) ) { - $meta_data[ Sync::META_KEYS['version'] ] = $result['version']; - } - $media->delete_post_meta( $attachment->ID, Sync::META_KEYS['pending'] ); - - // Cleanup flags - delete_post_meta( $attachment->ID, Sync::META_KEYS['downloading'] ); - delete_post_meta( $attachment->ID, Sync::META_KEYS['syncing'] ); - - $media->delete_post_meta( $attachment->ID, Sync::META_KEYS['sync_error'], false ); - if ( ! empty( $this->plugin->config['settings']['global_transformations']['enable_breakpoints'] ) ) { - if ( ! empty( $result['responsive_breakpoints'] ) ) { // Images only. - $meta_data[ Sync::META_KEYS['breakpoints'] ] = $result['responsive_breakpoints'][0]['breakpoints']; - } elseif ( 'image' === $upload['options']['resource_type'] && 'explicit' === $sync_type ) { - // Remove records of breakpoints. - delete_post_meta( $attachment->ID, Sync::META_KEYS['breakpoints'] ); - } - } - if ( ! empty( $upload['options']['public_id'] ) ) { - // a transformation breakpoints only ever happens on a down sync. - $sync_key = '_' . md5( $upload['options']['public_id'] ); - $meta_data['sync_key'] = true; - - // Add base ID. - update_post_meta( $attachment->ID, $sync_key, true ); - } - $stats['success'][] = $attachment->post_title; - $meta = wp_get_attachment_metadata( $attachment->ID, true ); - $meta[ Sync::META_KEYS['cloudinary'] ] = $meta_data; - wp_update_attachment_metadata( $attachment->ID, $meta ); - $media->update_post_meta( $attachment->ID, Sync::META_KEYS['public_id'], $upload['options']['public_id'] ); - // Search and update link references in content. - $content_search = new \WP_Query( array( 's' => 'wp-image-' . $attachment->ID, 'fields' => 'ids', 'posts_per_page' => 1000 ) ); - if ( ! empty( $content_search->found_posts ) ) { - $content_posts = array_unique( $content_search->get_posts() ); // ensure post only gets updated once. - foreach ( $content_posts as $content_id ) { - wp_update_post( array( 'ID' => $content_id ) ); // Trigger an update, internal filters will filter out remote URLS. - } - } - } - - $stats['processed'] += 1; + $this->queue->stop_maybe(); } - - $stats['end'] = time(); - $stats['duration'] = (int) $stats['end'] - (int) $stats['start']; - - return $stats; } } diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-upload-queue.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-sync-queue.php similarity index 51% rename from cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-upload-queue.php rename to cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-sync-queue.php index 930cbf1a6..ee65e2228 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-upload-queue.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-sync-queue.php @@ -1,6 +1,6 @@ cron_frequency = apply_filters( 'cloudinary_cron_frequency', 10 * MINUTE_IN_SECONDS ); $this->cron_start_offset = apply_filters( 'cloudinary_cron_start_offset', MINUTE_IN_SECONDS ); - + $this->threads = apply_filters( 'cloudinary_queue_threads', array( 'thread_0', 'thread_1', 'thread_2' ) ); $this->load_hooks(); } @@ -82,6 +82,7 @@ public function load_hooks() { * @return array|mixed */ public function get_queue() { + wp_cache_delete( self::$queue_key, 'options' ); $queue = get_option( self::$queue_key, array() ); if ( empty( $queue ) ) { $queue = $this->build_queue(); @@ -98,22 +99,32 @@ public function get_queue() { * @return bool */ public function set_queue( $queue ) { - return update_option( self::$queue_key, $queue ); + + return update_option( self::$queue_key, $queue, false ); } /** * Get a set of pending items. * + * @param string $thread The thread ID. + * * @return bool */ - public function get_post() { - $id = 0; - if ( $this->is_running() ) { + public function get_post( $thread ) { + $id = false; + if ( $this->is_running() && in_array( $thread, $this->threads, true ) ) { $queue = $this->get_queue(); - if ( ! empty( $queue['pending'] ) ) { - $id = array_shift( $queue['pending'] ); + if ( ! empty( $queue[ $thread ] ) ) { + $id = array_shift( $queue[ $thread ] ); $queue['processing'][] = $id; $queue['last_update'] = current_time( 'timestamp' ); + + if ( ! empty( $queue['run_status'][ $thread ]['last_update'] ) ) { + $queue['run_status'][ $thread ]['posts'][] = current_time( 'timestamp' ) - $queue['run_status'][ $thread ]['last_update']; + $queue['run_status'][ $thread ]['average'] = round( array_sum( $queue['run_status'][ $thread ]['posts'] ) / count( $queue['run_status'][ $thread ]['posts'] ), 2 ); + } + $queue['run_status'][ $thread ]['last_update'] = current_time( 'timestamp' ); + $this->set_queue( $queue ); } } @@ -121,6 +132,25 @@ public function get_post() { return $id; } + /** + * Check if a thread is running. + * + * @param string $thread Thread ID to check. + * + * @return bool + */ + protected function thread_running( $thread ) { + $running = false; + if ( in_array( $thread, $this->threads, true ) ) { + $queue = $this->get_queue(); + $now = current_time( 'timestamp' ); + if ( $this->is_running() && ! empty( $queue[ $thread ] ) && $now - $queue['run_status'][ $thread ]['last_update'] < $this->cron_start_offset ) { + $running = true; + } + } + + return $running; + } /** * Mark an id as done or error. @@ -129,25 +159,17 @@ public function get_post() { * @param string $type The type of marking to apply. */ public function mark( $id, $type = 'done' ) { - $queue = $this->get_queue(); - $key = array_search( (int) $id, $queue['processing'], true ); + $queue = $this->get_queue(); + $queue['last_update'] = current_time( 'timestamp' ); + $key = array_search( (int) $id, $queue['processing'], true ); if ( false !== $key ) { unset( $queue['processing'][ $key ] ); - if ( 'error' === $type ) { - $state = $this->plugin->components['sync']->managers['push']->prepare_upload( $id ); - if ( is_wp_error( $state ) ) { - $file = get_attached_file( $id ); - $queue[ $type ][] = '
' . basename( $file ) . ': ' . $state->get_error_message() . '
'; - // Add a flag that this file had an error as to not try process it again. - update_post_meta( $id, Sync::META_KEYS['sync_error'], $state->get_error_message() ); - delete_post_meta( $id, Sync::META_KEYS['syncing'], $state->get_error_message() ); - } - } else { - if ( ! in_array( $id, $queue[ $type ], true ) ) { - $queue[ $type ][] = $id; - } + if ( ! in_array( $id, $queue[ $type ], true ) ) { + $queue[ $type ][] = $id; } + } + $this->set_queue( $queue ); } @@ -157,12 +179,9 @@ public function mark( $id, $type = 'done' ) { * @return bool */ public function is_running() { - if ( false === $this->running ) { - $queue = $this->get_queue(); - $this->running = empty( $queue['started'] ) ? false : true; - } + $queue = $this->get_queue(); - return $this->running; + return ! empty( $queue['started'] ); } /** @@ -171,8 +190,11 @@ public function is_running() { * @return array */ public function get_queue_status() { - $queue = $this->get_queue(); - $pending = count( $queue['pending'] ); + $queue = $this->get_queue(); + $pending = 0; + foreach ( $this->threads as $thread ) { + $pending += count( $queue[ $thread ] ); + } $done = count( $queue['done'] ); $processing = count( $queue['processing'] ); $error = count( $queue['error'] ); @@ -240,20 +262,40 @@ public function build_queue() { $ids = $attachments->get_posts(); // Transform attachments. $return = array( - 'pending' => array(), 'done' => array(), 'processing' => array(), 'error' => array(), + 'run_status' => array(), ); + foreach ( $this->threads as $thread ) { + $return[ $thread ] = array(); + $return['run_status'][ $thread ] = array(); + } // Add items to pending queue. - $return['pending'] = $ids; + if ( ! empty( $ids ) ) { + $chunk_size = ceil( count( $ids ) / count( $this->threads ) ); + $chunks = array_chunk( $ids, $chunk_size ); + foreach ( $chunks as $index => $chunk ) { + $return[ $this->threads[ $index ] ] = $chunk; + } + } $this->set_queue( $return ); return $return; } + /** + * Maybe stop the queue. + */ + public function stop_maybe() { + $status = $this->get_queue_status(); + if ( empty( $status['pending'] ) ) { + $this->stop_queue(); + } + } + /** * Stop the queue by removing the started flag. */ @@ -261,27 +303,81 @@ public function stop_queue() { $queue = $this->get_queue(); if ( ! empty( $queue['started'] ) ) { unset( $queue['started'] ); + unset( $queue['last_update'] ); $this->set_queue( $queue ); } - $this->running = false; - wp_clear_scheduled_hook( 'cloudinary_resume_queue' ); + wp_unschedule_hook( 'cloudinary_resume_queue' ); } /** * Start the queue by setting the started flag. * - * @return void + * @return array */ public function start_queue() { - $queue = $this->get_queue(); - $queue['started'] = $queue['last_update'] = current_time( 'timestamp' ); + $queue = $this->get_queue(); + if ( ! empty( $queue['processing'] ) ) { + // In case it stopped mid process, push back to the first thread. + $queue['thread_0'] = array_merge( $queue['thread_0'], $queue['processing'] ); + } + // Count how many are pending. + $status = $this->get_queue_status(); + if ( empty( $status['pending'] ) ) { + // Dont start if theres nothing pending. + return $status; + } + // Mark as started. + $queue['started'] = current_time( 'timestamp' ); + $queue['last_update'] = current_time( 'timestamp' ); + $this->set_queue( $queue ); - $this->running = true; - if ( ! wp_next_scheduled( 'cloudinary_resume_queue' ) ) { - wp_schedule_single_event( time() + $this->cron_frequency, 'cloudinary_resume_queue' ); + foreach ( $this->threads as $thread ) { + if ( ! empty( $queue[ $thread ] ) ) { + $this->start_thread( $thread ); + sleep( 2 ); // Slight pause to prevent server overload. + } + } + $this->schedule_resume(); + + return $status; + } + + /** + * Start a thread to process. + * + * @param int $thread Thread ID. + */ + public function start_thread( $thread ) { + + $this->plugin->components['api']->background_request( 'queue', array( 'thread' => $thread ) ); + } + + + /** + * Get a threads queue. + * + * @param int $thread Thread ID. + * + * @return array + */ + public function get_thread_queue( $thread ) { + $queue = $this->get_queue(); + $return = array(); + if ( in_array( $thread, $this->threads, true ) && ! empty( $queue[ $thread ] ) ) { + $return = $queue[ $thread ]; } + + return $return; + } + + /** + * Schedule a resume queue check. + */ + protected function schedule_resume() { + $now = current_time( 'timestamp' ); + wp_schedule_single_event( $now + $this->cron_frequency, 'cloudinary_resume_queue' ); } /** @@ -291,13 +387,29 @@ public function start_queue() { * @return void */ public function maybe_resume_queue() { - $now = current_time( 'timestamp' ); - $queue = $this->get_queue(); + $stopped = array(); + if ( $this->is_running() ) { + // Check each thread. + foreach ( $this->threads as $thread ) { + if ( ! $this->thread_running( $thread ) ) { + // Possible that thread has stopped. + $stopped[] = $thread; + } + } - if ( $now - $queue['last_update'] > $this->cron_start_offset && $this->is_running() ) { - $this->plugin->components['api']->background_request( 'process', array() ); + if ( count( $stopped ) === count( $this->threads ) ) { + // All threads have stopped. Stop Queue to prevent overload in case of a slow sync. + $this->stop_queue(); + sleep( 5 ); // give it 5 seconds to allow the stop and maybe threads to catchup. + // Start a new sync. + $this->start_queue(); + } elseif ( ! empty( $stopped ) ) { + // Just start the threads that have stopped. + array_map( array( $this, 'start_thread' ), $stopped ); + $this->schedule_resume(); + } else { + $this->schedule_resume(); + } } - - wp_schedule_single_event( $now + $this->cron_frequency, 'cloudinary_resume_queue' ); } } diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-upload-sync.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-upload-sync.php index 0e7c2f7ed..2cb4b4331 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-upload-sync.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-upload-sync.php @@ -33,18 +33,32 @@ class Upload_Sync { private $pusher; /** - * This feature is enabled. + * Holds the main Sync Class. * - * @var bool + * @var \Cloudinary\Sync */ - private $enabled; + protected $sync; + + /** + * Holds the Connect Class. + * + * @var \Cloudinary\Connect + */ + protected $connect; /** - * Holds a list of unsynced images to push on end. + * Holds the Media Class. * - * @var array + * @var \Cloudinary\Media + */ + protected $media; + + /** + * This feature is enabled. + * + * @var bool */ - private $to_sync = array(); + private $enabled; /** * Upload_Sync constructor. @@ -63,14 +77,6 @@ public function __construct( \Cloudinary\Plugin $plugin, $enabled = false, $push * Register any hooks that this component needs. */ private function register_hooks() { - // Add action to upload. - add_action( 'add_attachment', array( $this, 'push_on_upload' ), 10 ); - // Action Cloudinary id for on-demand upload sync. - add_action( 'cloudinary_id', array( $this, 'prep_on_demand_upload' ), 9, 2 ); - // Show sync status. - add_filter( 'cloudinary_media_status', array( $this, 'filter_status' ), 10, 2 ); - // Hook for on demand upload push. - add_action( 'shutdown', array( $this, 'init_background_upload' ) ); // Hook into auto upload sync. add_filter( 'cloudinary_on_demand_sync_enabled', array( $this, 'auto_sync_enabled' ), 10, 2 ); // Handle bulk and inline actions. @@ -97,7 +103,7 @@ private function register_hooks() { * @return array */ function add_inline_action( $actions, $post ) { - if ( current_user_can( 'delete_post', $post->ID ) ) { + if ( $this->media->is_media( $post->ID ) && current_user_can( 'delete_post', $post->ID ) ) { $action_url = add_query_arg( array( 'action' => 'cloudinary-push', @@ -110,17 +116,15 @@ function add_inline_action( $actions, $post ) { $actions['cloudinary-push'] = sprintf( '%s', $action_url, - /* translators: %s: Attachment title. */ - esc_attr( sprintf( __( 'Push to Cloudinary “%s”' ), 'asd' ) ), - __( 'Push to Cloudinary', 'cloudinary' ) + esc_attr__( 'Push to Cloudinary', 'cloudinary' ), + esc_html__( 'Push to Cloudinary', 'cloudinary' ) ); } else { $actions['cloudinary-push'] = sprintf( '%s', $action_url, - /* translators: %s: Attachment title. */ - esc_attr( sprintf( __( 'Push to Cloudinary “%s”' ), 'asd' ) ), - __( 'Re-sync to Cloudinary', 'cloudinary' ) + esc_attr__( 'Re-sync to Cloudinary', 'cloudinary' ), + esc_html__( 'Re-sync to Cloudinary', 'cloudinary' ) ); } } @@ -142,14 +146,8 @@ public function handle_bulk_actions( $location, $action, $post_ids ) { switch ( $action ) { case 'cloudinary-push' : foreach ( $post_ids as $post_id ) { - delete_post_meta( $post_id, Sync::META_KEYS['sync_error'] ); - delete_post_meta( $post_id, Sync::META_KEYS['public_id'] ); - delete_post_meta( $post_id, Sync::META_KEYS['pending'] ); - delete_post_meta( $post_id, Sync::META_KEYS['downloading'] ); - delete_post_meta( $post_id, Sync::META_KEYS['syncing'] ); - $file = get_attached_file( $post_id ); - wp_generate_attachment_metadata( $post_id, $file ); - $this->prep_upload( $post_id ); + $this->sync->set_signature_item( $post_id, 'file', '' ); + $this->sync->add_to_sync( $post_id ); } break; } @@ -179,155 +177,157 @@ public function auto_sync_enabled( $enabled, $post_id ) { return $enabled; } - /** - * Push new attachment to Cloudinary on upload. - * - * @param int $post_id The post. - */ - public function push_on_upload( $post_id ) { - - // Only if this is a media file and feature is enabled. - if ( $this->plugin->components['media']->is_media( $post_id ) && apply_filters( 'cloudinary_upload_sync_enabled', $this->enabled ) ) { - // Lets do the background upload to keep the upload window as fast as possible. - update_post_meta( $post_id, '_cloudinary_pending', time() ); // Make sure it doesn't get resynced. - $params = array( - 'attachment_ids' => array( $post_id ), - ); - $this->plugin->components['api']->background_request( 'process', $params ); - } - } - /** * Setup this component. */ public function setup() { if ( empty( $this->pusher ) ) { - $this->pusher = $this->plugin->components['sync']->managers['push']; + $this->pusher = $this->plugin->components['sync']->managers['push']; + $this->sync = $this->plugin->components['sync']; + $this->connect = $this->plugin->components['connect']; + $this->media = $this->plugin->components['media']; } $this->register_hooks(); } /** - * Prepare an attachment without a cloudinary id, for background, on-demand push. + * Upload an asset to Cloudinary. * - * @param string|bool $cloudinary_id The public ID for a cloudinary asset. - * @param int $attachment_id The local attachment ID. + * @param int $attachment_id The attachment ID. * - * @return string + * @return array|\WP_Error */ - public function prep_on_demand_upload( $cloudinary_id, $attachment_id ) { - $attachment_id = intval( $attachment_id ); - if ( $attachment_id && false === $cloudinary_id ) { - // Check that this has not already been prepared for upload. - if ( ! $this->is_pending( $attachment_id ) && apply_filters( 'cloudinary_on_demand_sync_enabled', $this->enabled, $attachment_id ) ) { - $max_size = ( wp_attachment_is_image( $attachment_id ) ? 'max_image_size' : 'max_video_size' ); - $file = get_attached_file( $attachment_id ); - // Get the file size to make sure it can exist in cloudinary. - if ( ! empty( $this->plugin->components['connect']->usage[ $max_size ] ) && file_exists( $file ) && filesize( $file ) < $this->plugin->components['connect']->usage[ $max_size ] ) { - $this->add_to_sync( $attachment_id ); - } else { - // Check if the src is a url. - $file = get_post_meta( $attachment_id, '_wp_attached_file', true ); - if ( $this->plugin->components['media']->is_cloudinary_url( $file ) ) { - // Download sync. - $this->add_to_sync( $attachment_id ); - } - } - } - } + public function upload_asset( $attachment_id ) { - return $cloudinary_id; - } + $type = $this->sync->get_sync_type( $attachment_id ); + $options = $this->media->get_upload_options( $attachment_id ); + $public_id = $options['public_id']; - /** - * Prep an attachment for upload. - * - * @param int $attachment_id The attachment ID to prep for upload. - */ - public function prep_upload( $attachment_id ) { - $max_size = ( wp_attachment_is_image( $attachment_id ) ? 'max_image_size' : 'max_video_size' ); - $file = get_attached_file( $attachment_id ); - // Get the file size to make sure it can exist in cloudinary. - if ( file_exists( $file ) && filesize( $file ) < $this->plugin->components['connect']->usage[ $max_size ] ) { - $this->add_to_sync( $attachment_id ); + // Add the suffix before uploading. + if ( $this->media->get_public_id( $attachment_id ) === $public_id ) { + // Only apply the saved suffix if the public_id is the same. This is to allow filtered ID's a change to have a suffix removed. + $options['public_id'] .= $this->media->get_post_meta( $attachment_id, Sync::META_KEYS['suffix'], true ); } else { - // Check if the src is a url. - $file = get_post_meta( $attachment_id, '_wp_attached_file', true ); - if ( $this->plugin->components['media']->is_cloudinary_url( $file ) ) { - // Download sync. - $this->add_to_sync( $attachment_id ); + // If the public_id has been changed, remove the saved suffix. + $this->media->delete_post_meta( $attachment_id, Sync::META_KEYS['suffix'] ); + } + + // Run the upload Call. + $result = $this->connect->api->upload( $attachment_id, $options ); + + if ( ! is_wp_error( $result ) ) { + + // Check that this wasn't an existing. + if ( ! empty( $result['existing'] ) ) { + // Check to see if this is the same image. + $version = (int) $this->media->get_post_meta( $attachment_id, Sync::META_KEYS['version'], true ); + if ( $version !== $result['version'] ) { + // New image with the same name. + // Add a suffix and try again. + $suffix = '_' . $attachment_id . substr( strrev( uniqid() ), 0, 5 ); + $this->media->update_post_meta( $attachment_id, Sync::META_KEYS['suffix'], $suffix ); + + return $this->upload_asset( $attachment_id ); + } } + + + // Set folder Synced. + $this->media->update_post_meta( $attachment_id, Sync::META_KEYS['folder_sync'], $this->media->is_folder_synced( $attachment_id ) ); + // Set public_id. + $this->media->update_post_meta( $attachment_id, Sync::META_KEYS['public_id'], $public_id ); + // Set version. + $this->media->update_post_meta( $attachment_id, Sync::META_KEYS['version'], $result['version'] ); + // Update signature for all that use the same method. + $this->sync->sync_signature_by_type( $attachment_id, $type ); + // Update options and public_id as well (full sync) + $this->sync->set_signature_item( $attachment_id, 'options' ); + $this->sync->set_signature_item( $attachment_id, 'public_id' ); + + $this->update_breakpoints( $attachment_id, $result ); + $this->update_content( $attachment_id ); } + + return $result; } /** - * Add an attachment ID to the to_sync array. + * Update an assets context.. + * + * @param int $attachment_id The attachment ID. * - * @param int $attachment_id The attachment ID to add. + * @return array|\WP_Error */ - public function add_to_sync( $attachment_id ) { - if ( ! in_array( $attachment_id, $this->to_sync, true ) ) { - // Flag image as pending to prevent duplicate upload. - update_post_meta( $attachment_id, Sync::META_KEYS['pending'], time() ); - $this->plugin->components['media']->update_post_meta( $attachment_id, Sync::META_KEYS['folder_sync'], true ); - $this->to_sync[] = $attachment_id; + public function context_update( $attachment_id ) { + // dynamic sync type.. + $type = $this->sync->get_sync_type( $attachment_id ); + $options = $this->media->get_upload_options( $attachment_id ); + $result = $this->connect->api->context( $options ); + + if ( ! is_wp_error( $result ) ) { + $this->sync->set_signature_item( $attachment_id, $type ); + $this->media->update_post_meta( $attachment_id, Sync::META_KEYS['public_id'], $options['public_id'] ); } + + return $result; } /** - * Check if the attachment is pending an upload sync. + * Perform an explicit update to Cloudinary. * - * @param int $attachment_id The attachment ID to check. + * @param int $attachment_id The attachment ID. * - * @return bool + * @return array|\WP_Error|bool */ - public function is_pending( $attachment_id ) { - // Check if it's not already in the to sync array. - if ( ! in_array( $attachment_id, $this->to_sync, true ) ) { - $is_pending = $this->plugin->components['media']->get_post_meta( $attachment_id, Sync::META_KEYS['pending'], true ); - if ( empty( $is_pending ) || $is_pending < time() - 5 * 60 ) { - // No need to delete pending meta, since it will be updated with the new timestamp anyway. - return false; + public function explicit_update( $attachment_id ) { + // Explicit update. + $type = $this->sync->get_sync_type( $attachment_id ); + $args = $this->media->get_breakpoint_options( $attachment_id ); + if ( ! empty( $args ) ) { + $result = $this->connect->api->explicit( $args ); + if ( ! is_wp_error( $result ) ) { + $this->update_breakpoints( $attachment_id, $result ); } + } else { + $this->update_breakpoints( $attachment_id, array() ); + $result = true; } + $this->sync->set_signature_item( $attachment_id, $type ); - return true; + return $result; } /** - * Checks the status of the media item. + * Update breakpoints for an asset. * - * @param array $status Array of state and note. - * @param int $attachment_id The attachment id. - * - * @return array + * @param int $attachment_id The attachment ID. + * @param array $breakpoints Structure of the breakpoints. */ - public function filter_status( $status, $attachment_id ) { - - if ( $this->is_pending( $attachment_id ) ) { - $status['state'] = 'warning'; - $status['note'] = __( 'Upload sync pending', 'cloudinary' ); - } + public function update_breakpoints( $attachment_id, $breakpoints ) { - // Check if there's an error. - $has_error = $this->plugin->components['media']->get_post_meta( $attachment_id, Sync::META_KEYS['sync_error'], true ); - if ( ! empty( $has_error ) ) { - $status['state'] = 'error'; - $status['note'] = $has_error; + if ( ! empty( $this->plugin->config['settings']['global_transformations']['enable_breakpoints'] ) ) { + if ( ! empty( $breakpoints['responsive_breakpoints'] ) ) { // Images only. + $this->media->update_post_meta( $attachment_id, Sync::META_KEYS['breakpoints'], $breakpoints['responsive_breakpoints'][0]['breakpoints'] ); + } elseif ( wp_attachment_is_image( $attachment_id ) ) { + $this->media->delete_post_meta( $attachment_id, Sync::META_KEYS['breakpoints'] ); + } + $this->sync->set_signature_item( $attachment_id, 'breakpoints' ); } - - return $status; } /** - * Initialize the background sync on requested images needing to be synced. + * Trigger an update on content that contains the same attachment ID to allow filters to capture and process. + * + * @param int $attachment_id The attachment id to find and init an update. */ - public function init_background_upload() { - if ( ! empty( $this->to_sync ) ) { - $params = array( - 'attachment_ids' => $this->to_sync, - ); - $this->plugin->components['api']->background_request( 'process', $params ); + private function update_content( $attachment_id ) { + // Search and update link references in content. + $content_search = new \WP_Query( array( 's' => 'wp-image-' . $attachment_id, 'fields' => 'ids', 'posts_per_page' => 1000 ) ); + if ( ! empty( $content_search->found_posts ) ) { + $content_posts = array_unique( $content_search->get_posts() ); // ensure post only gets updated once. + foreach ( $content_posts as $content_id ) { + wp_update_post( array( 'ID' => $content_id ) ); // Trigger an update, internal filters will filter out remote URLS. + } } } } diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/ui-definitions/tabs/sync-media-content.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/ui-definitions/tabs/sync-media-content.php index a37756fc0..9abefb378 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/ui-definitions/tabs/sync-media-content.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/ui-definitions/tabs/sync-media-content.php @@ -5,7 +5,7 @@ * @package Cloudinary */ -$autosync = $this->plugin->components['settings']->is_auto_sync_enabled(); +$autosync = $this->plugin->components['sync']->is_auto_sync_enabled(); ?> plugin->config['connect'] ) ) : ?>