diff --git a/.check-author.yml b/.check-author.yml new file mode 100644 index 00000000..56b8a0cf --- /dev/null +++ b/.check-author.yml @@ -0,0 +1,4 @@ +mapping: + "Marius Cristea ": + - "selu91 " + - "selul " \ No newline at end of file diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 00000000..37d7cd72 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,41 @@ +version: "2" # required to adjust maintainability checks +plugins: + duplication: + enabled: true + config: + languages: + php: + mass_threshold: 95 + phpcodesniffer: + enabled: false + phpmd: + enabled: false + sonar-php: + enabled: false +checks: + argument-count: + config: + threshold: 5 + complex-logic: + config: + threshold: 4 + file-lines: + config: + threshold: 500 + method-complexity: + config: + threshold: 25 + method-count: + config: + threshold: 20 + method-lines: + config: + threshold: 50 + nested-control-flow: + config: + threshold: 4 + return-statements: + config: + threshold: 5 +exclude_patterns: +- "inc/old_replacer.php" \ No newline at end of file diff --git a/.distignore b/.distignore index 3f6cc959..bf00d33a 100644 --- a/.distignore +++ b/.distignore @@ -4,6 +4,7 @@ .travis.yml .jshintrc Gruntfile.js +code_quality grunt phpcs.xml node_modules diff --git a/.gitignore b/.gitignore index 6f4d5d2c..ac9b7355 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,7 @@ artifact vendor bin composer.lock -package-lock.json \ No newline at end of file +package-lock.json +code_quality +build +cc-test-reporter \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 47e40537..99484a52 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,57 +1,70 @@ language: php php: -- '7.0' + - '7.2' + - '7.1' + - '7.0' + - '5.6' + - '5.5' + - '5.4' sudo: false branches: except: - - "/^*-v[0-9]/" + - "/^*-v[0-9]/" env: matrix: - - WP_VERSION=latest WP_MULTISITE=0 + - WP_VERSION=latest WP_MULTISITE=0 global: - - MASTER_BRANCH=master UPSTREAM_REPO=Codeinwp/optimole-wp DEPLOY_BUILD=7.0 + - MASTER_BRANCH=master UPSTREAM_REPO=Codeinwp/optimole-wp DEPLOY_BUILD=7.0 + - CC_TEST_REPORTER_ID=b6145e78ee9b35206e3eec359227b1645923db67fe0459b437cfe1589b5ab2b8 + - GIT_COMMITTED_AT=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then git log -1 --pretty=format:%ct; else git log -1 --skip 1 --pretty=format:%ct; fi) before_install: -- mkdir -p bin && cd bin -- wget "$PIRATE_FLEET"load.sh -- cd .. && chmod +x bin/load.sh -- ". ./bin/load.sh" + - mkdir -p bin && cd bin + - wget "$PIRATE_FLEET"load.sh + - cd .. && chmod +x bin/load.sh + - ". ./bin/load.sh" install: -- chmod +x bin/install-dependencies.sh -- ". ./bin/install-dependencies.sh" + - chmod +x bin/install-dependencies.sh + - ". ./bin/install-dependencies.sh" +before_script: + - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + - chmod +x ./cc-test-reporter + - if [ $(phpenv version-name) = "7.1" ]; then ./cc-test-reporter before-build; fi script: -- if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then grunt travis; fi; + - if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then grunt travis; fi; +after_script: + - if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT; fi before_deploy: -- chmod +x bin/prepare-deploy.sh -- ". ./bin/prepare-deploy.sh" + - chmod +x bin/prepare-deploy.sh + - ". ./bin/prepare-deploy.sh" deploy: -- provider: s3 - access_key_id: "$AWS_ACCESS_KEY" - secret_access_key: "$AWS_SECRET_KEY" - bucket: "$AWS_BUCKET" - skip_cleanup: true - acl: public_read - overwrite: true - local-dir: artifact/ - upload-dir: "$AWS_PRODUCTS_FOLDER/$THEMEISLE_REPO/latest" - on: - branch: "$MASTER_BRANCH" - repo: "$UPSTREAM_REPO" - condition: "$TRAVIS_PHP_VERSION = $DEPLOY_BUILD" -- provider: s3 - access_key_id: "$AWS_ACCESS_KEY" - secret_access_key: "$AWS_SECRET_KEY" - bucket: "$AWS_BUCKET" - skip_cleanup: true - acl: public_read - overwrite: true - local-dir: artifact/ - upload-dir: "$AWS_PRODUCTS_FOLDER/$THEMEISLE_REPO/$THEMEISLE_VERSION" - on: - repo: "$UPSTREAM_REPO" - branch: "$MASTER_BRANCH" - condition: "$TRAVIS_PHP_VERSION = $DEPLOY_BUILD" + - provider: s3 + access_key_id: "$AWS_ACCESS_KEY" + secret_access_key: "$AWS_SECRET_KEY" + bucket: "$AWS_BUCKET" + skip_cleanup: true + acl: public_read + overwrite: true + local-dir: artifact/ + upload-dir: "$AWS_PRODUCTS_FOLDER/$THEMEISLE_REPO/latest" + on: + branch: "$MASTER_BRANCH" + repo: "$UPSTREAM_REPO" + condition: "$TRAVIS_PHP_VERSION = $DEPLOY_BUILD" + - provider: s3 + access_key_id: "$AWS_ACCESS_KEY" + secret_access_key: "$AWS_SECRET_KEY" + bucket: "$AWS_BUCKET" + skip_cleanup: true + acl: public_read + overwrite: true + local-dir: artifact/ + upload-dir: "$AWS_PRODUCTS_FOLDER/$THEMEISLE_REPO/$THEMEISLE_VERSION" + on: + repo: "$UPSTREAM_REPO" + branch: "$MASTER_BRANCH" + condition: "$TRAVIS_PHP_VERSION = $DEPLOY_BUILD" after_deploy: -- chmod +x bin/deploy.sh -- ". ./bin/deploy.sh" + - chmod +x bin/deploy.sh + - ". ./bin/deploy.sh" after_failure: -- cat logs/phpcs.log \ No newline at end of file + - cat logs/phpcs.log \ No newline at end of file diff --git a/assets/js/bundle.js b/assets/js/bundle.js index 379d24ec..fa409166 100644 --- a/assets/js/bundle.js +++ b/assets/js/bundle.js @@ -67,56 +67,56 @@ /* 0 */ /***/ (function(module, exports) { -/* - MIT License http://www.opensource.org/licenses/mit-license.php - Author Tobias Koppers @sokra -*/ -// css base code, injected by the css-loader -module.exports = function() { - var list = []; - - // return the list of modules as css string - list.toString = function toString() { - var result = []; - for(var i = 0; i < this.length; i++) { - var item = this[i]; - if(item[2]) { - result.push("@media " + item[2] + "{" + item[1] + "}"); - } else { - result.push(item[1]); - } - } - return result.join(""); - }; - - // import a list of modules into the list - list.i = function(modules, mediaQuery) { - if(typeof modules === "string") - modules = [[null, modules, ""]]; - var alreadyImportedModules = {}; - for(var i = 0; i < this.length; i++) { - var id = this[i][0]; - if(typeof id === "number") - alreadyImportedModules[id] = true; - } - for(i = 0; i < modules.length; i++) { - var item = modules[i]; - // skip already imported module - // this implementation is not 100% perfect for weird media query combinations - // when a module is imported multiple times with different media queries. - // I hope this will never occur (Hey this way we have smaller bundles) - if(typeof item[0] !== "number" || !alreadyImportedModules[item[0]]) { - if(mediaQuery && !item[2]) { - item[2] = mediaQuery; - } else if(mediaQuery) { - item[2] = "(" + item[2] + ") and (" + mediaQuery + ")"; - } - list.push(item); - } - } - }; - return list; -}; +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ +// css base code, injected by the css-loader +module.exports = function() { + var list = []; + + // return the list of modules as css string + list.toString = function toString() { + var result = []; + for(var i = 0; i < this.length; i++) { + var item = this[i]; + if(item[2]) { + result.push("@media " + item[2] + "{" + item[1] + "}"); + } else { + result.push(item[1]); + } + } + return result.join(""); + }; + + // import a list of modules into the list + list.i = function(modules, mediaQuery) { + if(typeof modules === "string") + modules = [[null, modules, ""]]; + var alreadyImportedModules = {}; + for(var i = 0; i < this.length; i++) { + var id = this[i][0]; + if(typeof id === "number") + alreadyImportedModules[id] = true; + } + for(i = 0; i < modules.length; i++) { + var item = modules[i]; + // skip already imported module + // this implementation is not 100% perfect for weird media query combinations + // when a module is imported multiple times with different media queries. + // I hope this will never occur (Hey this way we have smaller bundles) + if(typeof item[0] !== "number" || !alreadyImportedModules[item[0]]) { + if(mediaQuery && !item[2]) { + item[2] = mediaQuery; + } else if(mediaQuery) { + item[2] = "(" + item[2] + ") and (" + mediaQuery + ")"; + } + list.push(item); + } + } + }; + return list; +}; /***/ }), @@ -11342,27 +11342,27 @@ Vue.compile = compileToFunctions; /* 3 */ /***/ (function(module, exports) { -var g; - -// This works in non-strict mode -g = (function() { - return this; -})(); - -try { - // This works if eval is allowed (see CSP) - g = g || Function("return this")() || (1,eval)("this"); -} catch(e) { - // This works if the window reference is available - if(typeof window === "object") - g = window; -} - -// g can still be undefined, but nothing to do about it... -// We return undefined, instead of nothing here, so it's -// easier to handle this case. if(!global) { ...} - -module.exports = g; +var g; + +// This works in non-strict mode +g = (function() { + return this; +})(); + +try { + // This works if eval is allowed (see CSP) + g = g || Function("return this")() || (1,eval)("this"); +} catch(e) { + // This works if the window reference is available + if(typeof window === "object") + g = window; +} + +// g can still be undefined, but nothing to do about it... +// We return undefined, instead of nothing here, so it's +// easier to handle this case. if(!global) { ...} + +module.exports = g; /***/ }), @@ -11569,7 +11569,7 @@ if (false) {(function () { module.hot.accept() var hotAPI = require("vue-hot-reload-api") hotAPI.install(require("vue"), true) if (!hotAPI.compatible) return - var id = "D:\\local\\optimolewp\\app\\public\\wp-content\\plugins\\optimole-wp\\assets\\vue\\components\\api-key-form.vue" + var id = "/var/www/html/wp-minions/wp-content/plugins/optimole-wp/assets/vue/components/api-key-form.vue" if (!module.hot.data) { hotAPI.createRecord(id, module.exports) } else { @@ -13466,7 +13466,7 @@ if (false) {(function () { module.hot.accept() var hotAPI = require("vue-hot-reload-api") hotAPI.install(require("vue"), true) if (!hotAPI.compatible) return - var id = "D:\\local\\optimolewp\\app\\public\\wp-content\\plugins\\optimole-wp\\assets\\vue\\components\\main.vue" + var id = "/var/www/html/wp-minions/wp-content/plugins/optimole-wp/assets/vue/components/main.vue" if (!module.hot.data) { hotAPI.createRecord(id, module.exports) } else { @@ -13490,8 +13490,8 @@ if(content.locals) module.exports = content.locals; if(false) { // When the styles change, update the + +}; // // // - // - -}; /***/ }), /* 14 */ @@ -13663,7 +13676,7 @@ if (false) {(function () { module.hot.accept() var hotAPI = require("vue-hot-reload-api") hotAPI.install(require("vue"), true) if (!hotAPI.compatible) return - var id = "D:\\local\\optimolewp\\app\\public\\wp-content\\plugins\\optimole-wp\\assets\\vue\\components\\app-header.vue" + var id = "/var/www/html/wp-minions/wp-content/plugins/optimole-wp/assets/vue/components/app-header.vue" if (!module.hot.data) { hotAPI.createRecord(id, module.exports) } else { @@ -13687,8 +13700,8 @@ if(content.locals) module.exports = content.locals; if(false) { // When the styles change, update the + +}; // +// +// + + \ No newline at end of file diff --git a/assets/vue/store/actions.js b/assets/vue/store/actions.js index 2195ab5a..176f0031 100644 --- a/assets/vue/store/actions.js +++ b/assets/vue/store/actions.js @@ -8,160 +8,228 @@ Vue.use( VueResource ); const connectOptimole = function ( {commit, state}, data ) { commit( 'toggleConnecting', true ); commit( 'restApiNotWorking', false ); - Vue.http( { - url: optimoleDashboardApp.root + '/connect', - method: 'POST', - headers: {'X-WP-Nonce': optimoleDashboardApp.nonce}, - params: {'req': data.req}, - body: { - 'api_key': data.apiKey, + Vue.http( + { + url: optimoleDashboardApp.root + '/connect', + method: 'POST', + headers: {'X-WP-Nonce': optimoleDashboardApp.nonce}, + params: {'req': data.req}, + body: { + 'api_key': data.apiKey, + }, + responseType: 'json', + emulateJSON: true, + } + ).then( + function ( response ) { + commit( 'toggleConnecting', false ); + if ( response.body.code === 'success' ) { + commit( 'toggleKeyValidity', true ); + commit( 'toggleConnectedToOptml', true ); + commit( 'updateApiKey', data.apiKey ); + commit( 'updateUserData', response.body.data ); + console.log( '%c OptiMole API connection successful.', 'color: #59B278' ); + + } else { + commit( 'toggleKeyValidity', false ); + console.log( '%c Invalid API Key.', 'color: #E7602A' ); + } }, - responseType: 'json', - emulateJSON: true, - } ).then( function ( response ) { - commit( 'toggleConnecting', false ); - if ( response.body.code === 'success' ) { - commit( 'toggleKeyValidity', true ); - commit( 'toggleConnectedToOptml', true ); - commit( 'updateApiKey', data.apiKey ); - commit( 'updateUserData', response.body.data ); - console.log( '%c OptiMole API connection successful.', 'color: #59B278' ); - - } else { - commit( 'toggleKeyValidity', false ); - console.log( '%c Invalid API Key.', 'color: #E7602A' ); + function () { + commit( 'toggleConnecting', false ); + commit( 'restApiNotWorking', true ); } - }, function () { - commit( 'toggleConnecting', false ); - commit( 'restApiNotWorking', true ); - } ); + ); }; const registerOptimole = function ( {commit, state}, data ) { commit( 'restApiNotWorking', false ); commit( 'toggleLoading', true ); - return Vue.http( { - url: optimoleDashboardApp.root + '/register', - method: 'POST', - headers: {'X-WP-Nonce': optimoleDashboardApp.nonce}, - params: {'req': data.req}, - body: { - 'email': data.email, + return Vue.http( + { + url: optimoleDashboardApp.root + '/register', + method: 'POST', + headers: {'X-WP-Nonce': optimoleDashboardApp.nonce}, + params: {'req': data.req}, + body: { + 'email': data.email, + }, + emulateJSON: true, + responseType: 'json' + } + ).then( + function ( response ) { + commit( 'toggleLoading', false ); + return response.data; }, - emulateJSON: true, - responseType: 'json' - } ).then( function ( response ) { - commit( 'toggleLoading', false ); - return response.data; - }, function ( response ) { - commit( 'toggleLoading', false ); - commit( 'restApiNotWorking', true ); - return response.data; - } ); + function ( response ) { + commit( 'toggleLoading', false ); + commit( 'restApiNotWorking', true ); + return response.data; + } + ); }; const disconnectOptimole = function ( {commit, state}, data ) { commit( 'toggleLoading', true, 'loading' ); - Vue.http( { - url: optimoleDashboardApp.root + '/disconnect', - method: 'GET', - headers: {'X-WP-Nonce': optimoleDashboardApp.nonce}, - params: {'req': data.req}, - emulateJSON: true, - responseType: 'json' - } ).then( function ( response ) { - commit( 'updateUserData', null ); - commit( 'toggleLoading', false ); - commit( 'updateApiKey', '' ); - if ( response.ok ) { - commit( 'toggleConnectedToOptml', false ); - console.log( '%c Disconnected from OptiMole API.', 'color: #59B278' ); - } else { - console.error( response ); + Vue.http( + { + url: optimoleDashboardApp.root + '/disconnect', + method: 'GET', + headers: {'X-WP-Nonce': optimoleDashboardApp.nonce}, + params: {'req': data.req}, + emulateJSON: true, + responseType: 'json' } - } ); + ).then( + function ( response ) { + commit( 'updateUserData', null ); + commit( 'toggleLoading', false ); + commit( 'updateApiKey', '' ); + if ( response.ok ) { + commit( 'toggleConnectedToOptml', false ); + console.log( '%c Disconnected from OptiMole API.', 'color: #59B278' ); + } else { + console.error( response ); + } + } + ); }; const saveSettings = function ( {commit, state}, data ) { commit( 'updateSettings', data.settings ); commit( 'toggleLoading', true ); - return Vue.http( { - url: optimoleDashboardApp.root + '/update_option', - method: 'POST', - headers: {'X-WP-Nonce': optimoleDashboardApp.nonce}, - emulateJSON: true, - body: { - 'settings': data.settings - }, - responseType: 'json' - } ).then( function ( response ) { - if ( response.body.code === 'success' ) { - commit( 'updateSettings', response.body.data ); + return Vue.http( + { + url: optimoleDashboardApp.root + '/update_option', + method: 'POST', + headers: {'X-WP-Nonce': optimoleDashboardApp.nonce}, + emulateJSON: true, + body: { + 'settings': data.settings + }, + responseType: 'json' } - commit( 'toggleLoading', false ); - + ).then( + function ( response ) { + if ( response.body.code === 'success' ) { + commit( 'updateSettings', response.body.data ); + } + commit( 'toggleLoading', false ); - } ); + } + ); }; const sampleRate = function ( {commit, state}, data ) { data.component.loading_images = true; - return Vue.http( { - url: optimoleDashboardApp.root + '/images-sample-rate', - method: 'POST', - emulateJSON: true, - headers: {'X-WP-Nonce': optimoleDashboardApp.nonce}, - params: { - 'quality': data.quality, - 'force': data.force - }, - responseType: 'json' - } ).then( function ( response ) { + return Vue.http( + { + url: optimoleDashboardApp.root + '/images-sample-rate', + method: 'POST', + emulateJSON: true, + headers: {'X-WP-Nonce': optimoleDashboardApp.nonce}, + params: { + 'quality': data.quality, + 'force': data.force + }, + responseType: 'json' + } + ).then( + function ( response ) { - data.component.loading_images = false; - if ( response.body.code === 'success' ) { - commit( 'updateSampleRate', response.body.data ); + data.component.loading_images = false; + if ( response.body.code === 'success' ) { + commit( 'updateSampleRate', response.body.data ); + } } - } ); + ); }; const retrieveOptimizedImages = function ( {commit, state}, data ) { let self = this; - setTimeout( function () { - - if ( self.state.optimizedImages.length > 0 ) { - console.log( '%c Images already exsist.', 'color: #59B278' ); - return false; - } - Vue.http( { - url: optimoleDashboardApp.root + '/poll_optimized_images', - method: 'GET', - emulateJSON: true, - headers: {'X-WP-Nonce': optimoleDashboardApp.nonce}, - params: {'req': 'Get Optimized Images'}, - responseType: 'json', - timeout: 10000 - } ).then( function ( response ) { - if ( response.body.code === 'success' ) { - commit( 'updateOptimizedImages', response ); - if ( data.component !== null ) { - data.component.loading = false; - data.component.startTime = data.component.maxTime; - if ( response.body.data.length === 0 ) { - data.component.noImages = true; + setTimeout( + function () { + if ( self.state.optimizedImages.length > 0 ) { + console.log( '%c Images already exsist.', 'color: #59B278' ); + return false; + } + Vue.http( + { + url: optimoleDashboardApp.root + '/poll_optimized_images', + method: 'GET', + emulateJSON: true, + headers: {'X-WP-Nonce': optimoleDashboardApp.nonce}, + params: {'req': 'Get Optimized Images'}, + responseType: 'json', + timeout: 10000 + } + ).then( + function ( response ) { + if ( response.body.code === 'success' ) { + commit( 'updateOptimizedImages', response ); + if ( data.component !== null ) { + data.component.loading = false; + data.component.startTime = data.component.maxTime; + if ( response.body.data.length === 0 ) { + data.component.noImages = true; + } + } + console.log( '%c Images Fetched.', 'color: #59B278' ); + } else { + component.noImages = true; + data.component.loading = false; + console.log( '%c No images available.', 'color: #E7602A' ); } } - console.log( '%c Images Fetched.', 'color: #59B278' ); - } else { - component.noImages = true; - data.component.loading = false; - console.log( '%c No images available.', 'color: #E7602A' ); + ); + }, + data.waitTime + ); +}; + +const retrieveWatermarks = function ( {commit, state}, data ) { + let self = this; + Vue.http( { + url: optimoleDashboardApp.root + '/poll_watermarks', + method: 'GET', + headers: {'X-WP-Nonce': optimoleDashboardApp.nonce}, + params: {'req': 'Get Watermarks'}, + responseType: 'json', + } ).then( function ( response ) { + if( response.status === 200 ) { + data.component.watermarkData = []; + for( let row in response.data.data ) { + let tmp = response.data.data[row]; + let item = { + ID: tmp.ID, + post_title: tmp.post_title, + post_mime_type: tmp.post_mime_type, + guid: tmp.post_content || tmp.guid, + } + data.component.watermarkData.push( item ) + data.component.noImages = false; } - } ); - }, data.waitTime ); + } + } ); +}; + +const removeWatermark = function ( {commit, state}, data ) { + let self = this; + data.component.loading = true + Vue.http( { + url: optimoleDashboardApp.root + '/remove_watermark', + method: 'POST', + headers: {'X-WP-Nonce': optimoleDashboardApp.nonce}, + params: {'req': 'Get Watermarks' , 'postID': data.postID }, + responseType: 'json', + } ).then( function ( response ) { + data.component.loading = false; + retrieveWatermarks( {commit, state}, data ); + } ); }; export default { @@ -171,4 +239,6 @@ export default { saveSettings, sampleRate, retrieveOptimizedImages, -}; \ No newline at end of file + retrieveWatermarks, + removeWatermark +}; diff --git a/assets/vue/store/mutations.js b/assets/vue/store/mutations.js index c9c1f988..e7c52667 100644 --- a/assets/vue/store/mutations.js +++ b/assets/vue/store/mutations.js @@ -28,7 +28,15 @@ const restApiNotWorking = ( state, data ) => { }; const updateSettings = ( state, data ) => { - for ( var setting in data ) { state.site_settings[setting] = data[setting]; } + for ( var setting in data ) { + state.site_settings[setting] = data[setting]; } + +}; + +const updateWatermark = ( state, data ) => { + + for ( var key in data ) { + state.site_settings.watermark[key] = data[key]; } }; @@ -42,5 +50,6 @@ export default { updateSampleRate, restApiNotWorking, updateSettings, + updateWatermark, updateOptimizedImages }; diff --git a/assets/vue/store/store.js b/assets/vue/store/store.js index 6ed244f2..553f4ac4 100644 --- a/assets/vue/store/store.js +++ b/assets/vue/store/store.js @@ -9,23 +9,25 @@ import actions from './actions'; Vue.use( Vuex ); Vue.use( VueResource ); -const store = new Vuex.Store( { - strict: true, - state: { - isConnecting: false, - loading: false, - site_settings: optimoleDashboardApp.site_settings, - connected: optimoleDashboardApp.connection_status === 'yes', - apiKey: optimoleDashboardApp.api_key ? optimoleDashboardApp.api_key : '', - apiKeyValidity: true, - sample_rate: {}, - apiError: false, - userData: optimoleDashboardApp.user_data ? optimoleDashboardApp.user_data : null, - optimizedImages: [], - }, - mutations, - actions -} ); +const store = new Vuex.Store( + { + strict: true, + state: { + isConnecting: false, + loading: false, + site_settings: optimoleDashboardApp.site_settings, + connected: optimoleDashboardApp.connection_status === 'yes', + apiKey: optimoleDashboardApp.api_key ? optimoleDashboardApp.api_key : '', + apiKeyValidity: true, + sample_rate: {}, + apiError: false, + userData: optimoleDashboardApp.user_data ? optimoleDashboardApp.user_data : null, + optimizedImages: [], + watermarks: [], + }, + mutations, + actions + } +); export default store; - diff --git a/composer.json b/composer.json index 5fad1b14..8a5550b3 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,13 @@ "vendor/codeinwp/themeisle-sdk/load.php" ] }, + "require-dev": { + "phpmd/phpmd": "^2.6", + "squizlabs/php_codesniffer": "^3.3", + "wp-coding-standards/wpcs": "^1.2", + "phpunit/phpunit": "5.7.9", + "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0" + }, "require": { "codeinwp/themeisle-sdk": "^3.0" } diff --git a/inc/admin.php b/inc/admin.php index ddb8af94..3f3ba53d 100644 --- a/inc/admin.php +++ b/inc/admin.php @@ -39,13 +39,11 @@ public function __construct() { add_action( 'optml_daily_sync', array( $this, 'daily_sync' ) ); add_action( 'wp_head', array( $this, 'generator' ) ); add_action( 'admin_init', array( $this, 'maybe_redirect' ) ); - if ( ! is_admin() && $this->settings->is_connected() ) { - if ( ! wp_next_scheduled( 'optml_daily_sync' ) ) { - wp_schedule_event( time() + 10, 'daily', 'optml_daily_sync', array() ); - } + if ( ! is_admin() && $this->settings->is_connected() && ! wp_next_scheduled( 'optml_daily_sync' ) ) { + wp_schedule_event( time() + 10, 'daily', 'optml_daily_sync', array() ); } - if ( $this->settings->is_connected() && $this->settings->use_lazyload() ) { + if ( $this->settings->use_lazyload() ) { add_filter( 'body_class', array( $this, 'adds_body_classes' ) ); add_action( 'wp_head', array( $this, 'inline_bootstrap_script' ) ); } @@ -137,9 +135,7 @@ public function add_body_class( $classes ) { if ( ! $this->should_show_notice() ) { return $classes; } - $classes .= ' optimole-optin-show '; - - return $classes; + return $classes . ' optimole-optin-show '; } /** @@ -149,21 +145,15 @@ public function add_body_class( $classes ) { */ public function should_show_notice() { - if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { - return false; - } - - if ( is_network_admin() ) { - return false; - } - - if ( $this->settings->is_connected() ) { - return false; - } $current_screen = get_current_screen(); - if ( empty( $current_screen ) ) { + if ( ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || + is_network_admin() || + $this->settings->is_connected() || + empty( $current_screen ) + ) { return false; } + static $allowed_base = array( 'plugins' => true, 'upload' => true, @@ -171,16 +161,21 @@ public function should_show_notice() { 'themes' => true, 'appearance_page_tgmpa-install-plugins' => true, ); - $screen_slug = isset( $current_screen->parent_base ) ? $current_screen->parent_base : isset( $current_screen->base ) ? $current_screen->base : ''; - if ( empty( $screen_slug ) || - ( ! isset( $allowed_base[ $screen_slug ] ) ) ) { - return false; + $screen_slug = ''; + if ( isset( $current_screen->base ) ) { + $screen_slug = $current_screen->base; } - if ( ! current_user_can( 'manage_options' ) ) { - return false; + + if ( isset( $current_screen->parent_base ) ) { + $screen_slug = $current_screen->parent_base; } - if ( ( get_option( 'optml_notice_optin', 'no' ) === 'yes' ) ) { + + if ( empty( $screen_slug ) || + ( ! isset( $allowed_base[ $screen_slug ] ) ) || + ! current_user_can( 'manage_options' ) || + ( get_option( 'optml_notice_optin', 'no' ) === 'yes' ) + ) { return false; } @@ -189,7 +184,7 @@ public function should_show_notice() { /** - * Adds optin notice. + * Adds opt in notice. */ public function add_notice() { if ( ! $this->should_show_notice() ) { @@ -343,6 +338,8 @@ public function render_dashboard_page() { /** * Enqueue scripts needed for admin functionality. + * + * @codeCoverageIgnore */ public function enqueue() { @@ -361,13 +358,14 @@ public function enqueue() { /** * Localize the dashboard app. * + * @codeCoverageIgnore * @return array */ private function localize_dashboard_app() { $api_key = $this->settings->get( 'api_key' ); $service_data = $this->settings->get( 'service_data' ); $user = get_userdata( get_current_user_id() ); - $args = array( + return array( 'strings' => $this->get_dashboard_strings(), 'assets_url' => OPTML_URL . 'assets/', 'connection_status' => empty( $service_data ) ? 'no' : 'yes', @@ -381,13 +379,12 @@ private function localize_dashboard_app() { 'site_settings' => $this->settings->get_site_settings(), 'home_url' => home_url(), ); - - return $args; } /** * Get all dashboard strings. * + * @codeCoverageIgnore * @return array */ private function get_dashboard_strings() { @@ -446,6 +443,7 @@ private function get_dashboard_strings() { ), 'dashboard_menu_item' => __( 'Dashboard', 'optimole-wp' ), 'settings_menu_item' => __( 'Settings', 'optimole-wp' ), + 'watermarks_menu_item' => __( 'Watermarks', 'optimole-wp' ), 'options_strings' => array( 'toggle_ab_item' => __( 'Admin bar status', 'optimole-wp' ), 'toggle_lazyload' => __( 'Javascript replacement & Lazy load', 'optimole-wp' ), @@ -475,6 +473,39 @@ private function get_dashboard_strings() { 'admin_bar_desc' => __( 'Show in the WordPress admin bar the available quota from Optimole service.', 'optimole-wp' ), 'lazyload_desc' => __( 'We will generate images size based on your visitor\'s screen using javascript and render them without blocking the page execution via lazyload.', 'optimole-wp' ), ), + 'watermarks' => array( + 'image' => __( 'Image', 'optimole-wp' ), + 'id' => __( 'ID', 'optimole-wp' ), + 'name' => __( 'Name', 'optimole-wp' ), + 'type' => __( 'Type', 'optimole-wp' ), + 'action' => __( 'Action', 'optimole-wp' ), + 'upload' => __( 'Upload', 'optimole-wp' ), + 'add_desc' => __( 'Add new watermark', 'optimole-wp' ), + 'wm_title' => __( 'Active watermark', 'optimole-wp' ), + 'wm_desc' => __( 'The active watermark to use from the list of uploaded watermarks.', 'optimole-wp' ), + 'opacity_field' => __( 'Opacity', 'optimole-wp' ), + 'opacity_title' => __( 'Watermark opacity', 'optimole-wp' ), + 'opacity_desc' => __( 'A value between 0 and 100 for the opacity level. If set to 0 it will disable the watermark.', 'optimole-wp' ), + 'position_title' => __( 'Watermark position', 'optimole-wp' ), + 'position_desc' => __( 'The place relative to the image where the watermark should be placed.', 'optimole-wp' ), + 'pos_nowe_title' => __( 'North-West', 'optimole-wp' ), + 'pos_no_title' => __( 'North', 'optimole-wp' ), + 'pos_noea_title' => __( 'North-East', 'optimole-wp' ), + 'pos_we_title' => __( 'West', 'optimole-wp' ), + 'pos_ce_title' => __( 'Center', 'optimole-wp' ), + 'pos_ea_title' => __( 'East', 'optimole-wp' ), + 'pos_sowe_title' => __( 'South-West', 'optimole-wp' ), + 'pos_so_title' => __( 'South', 'optimole-wp' ), + 'pos_soea_title' => __( 'South-East', 'optimole-wp' ), + 'offset_x_field' => __( 'Offset X', 'optimole-wp' ), + 'offset_y_field' => __( 'Offset Y', 'optimole-wp' ), + 'offset_title' => __( 'Watermark offset', 'optimole-wp' ), + 'offset_desc' => __( 'Offset the watermark from set position on X and Y axis. Values can be positive or negative.', 'optimole-wp' ), + 'scale_field' => __( 'Scale', 'optimole-wp' ), + 'scale_title' => __( 'Watermark scale', 'optimole-wp' ), + 'scale_desc' => __( 'A value between 0 and 300 for the scale of the watermark (100 is the original size and 300 is 3x the size). If set to 0 it will default to the original size.', 'optimole-wp' ), + 'save_changes' => __( 'Save changes', 'optimole-wp' ), + ), 'latest_images' => array( 'image' => __( 'Image', 'optimole-wp' ), 'no_images_found' => sprintf( __( 'We might have a delay finding optimized images. Meanwhile you can visit your %1$shomepage%2$s and check how our plugin performs. ', 'optimole-wp' ), '', '' ), @@ -499,11 +530,11 @@ public function add_traffic_node( $wp_admin_bar ) { if ( ! is_user_logged_in() ) { return; } - $settings = new Optml_Settings(); - if ( ! $settings->is_connected() ) { + $this->settings = new Optml_Settings(); + if ( ! $this->settings->is_connected() ) { return; } - $should_load = $settings->get( 'admin_bar_item' ); + $should_load = $this->settings->get( 'admin_bar_item' ); $service_data = $this->settings->get( 'service_data' ); if ( empty( $service_data ) ) { diff --git a/inc/api.php b/inc/api.php index 126504ab..eec69f40 100644 --- a/inc/api.php +++ b/inc/api.php @@ -3,6 +3,7 @@ /** * The class defines way of connecting this user to the Optimole Dashboard. * + * @codeCoverageIgnore * @package \Optimole\Inc * @author Optimole */ @@ -13,8 +14,8 @@ final class Optml_Api { * * @var string Api root. */ - private $api_root = 'https://dashboard.optimole.com/api/optml/v1/'; - // private $api_root = 'http://localhost:8000/api/optml/v1/'; + private $api_root = 'http://127.0.0.1:8000/api/optml/v1/'; + // private $api_root = 'https://dashboard.optimole.com/api/optml/v1/'; /** * Hold the user api key. * @@ -43,6 +44,32 @@ public function get_user_data( $api_key = '' ) { return $this->request( '/image/details', 'POST' ); } + /** + * Builds Request arguments array. + * + * @param string $method Request method (GET | POST | PUT | UPDATE | DELETE). + * @param string $url Request URL. + * @param array $headers Headers Array. + * @param array $params Additional params for the Request. + * + * @return array + */ + private function build_args( $method, $url, $headers, $params ) { + $args = array( + 'url' => $url, + 'method' => $method, + 'timeout' => 45, + 'user-agent' => 'Optimle WP (v' . OPTML_VERSION . ') ', + 'sslverify' => false, + 'headers' => $headers, + ); + if ( $method !== 'GET' ) { + $args['body'] = $params; + } + + return $args; + } + /** * Request constructor. * @@ -69,17 +96,8 @@ private function request( $path, $method = 'GET', $params = array() ) { } } $url = trailingslashit( $this->api_root ) . ltrim( $path, '/' ); - $args = array( - 'url' => $url, - 'method' => $method, - 'timeout' => 45, - 'user-agent' => 'Optimle WP (v' . OPTML_VERSION . ') ', - 'sslverify' => false, - 'headers' => $headers, - ); - if ( $method !== 'GET' ) { - $args['body'] = $params; - } + $args = $this->build_args( $method, $url, $headers, $params ); + $response = wp_remote_request( $url, $args ); if ( is_wp_error( $response ) ) { @@ -137,6 +155,72 @@ public function get_optimized_images( $api_key = '' ) { return $this->request( '/stats/images' ); } + /** + * Get the watermarks from API. + * + * @param string $api_key The API key. + * + * @return array|bool + */ + public function get_watermarks( $api_key = '' ) { + if ( ! empty( $api_key ) ) { + $this->api_key = $api_key; + } + return $this->request( '/settings/watermark' ); + } + + /** + * Remove the watermark from the API. + * + * @param integer $post_id The watermark post ID. + * @param string $api_key The API key. + * + * @return array|bool + */ + public function remove_watermark( $post_id, $api_key = '' ) { + if ( ! empty( $api_key ) ) { + $this->api_key = $api_key; + } + return $this->request( '/settings/watermark', 'DELETE', array( 'watermark' => $post_id ) ); + } + + /** + * Add watermark. + * + * @param array $file The file to be uploaded. + * @param string $api_key The API key. + * + * @return array|bool|mixed|object + */ + public function add_watermark( $file, $api_key = '' ) { + if ( ! empty( $api_key ) ) { + $this->api_key = $api_key; + } + // Grab the url to which we'll be making the request. + $url = str_replace( 'optml/v1/', 'wp/v2/', $this->api_root ) . 'media'; + $headers = array( + 'Optml-Site' => get_site_url(), + ); + if ( ! empty( $this->api_key ) ) { + $headers['Authorization'] = 'Bearer ' . $this->api_key; + $headers['Content-Disposition'] = 'attachment; filename=' . $file['file']['name']; + } + $args = array( + 'url' => $url, + 'method' => 'POST', + 'timeout' => 45, + 'user-agent' => 'Optimle WP (v' . OPTML_VERSION . ') ', + 'sslverify' => false, + 'headers' => $headers, + 'body' => file_get_contents( $file['file']['tmp_name'] ), + ); + $response = wp_remote_request( $url, $args ); + if ( is_wp_error( $response ) ) { + return false; + } + return json_decode( wp_remote_retrieve_body( $response ), true ); + } + /** * Throw error on object clone * diff --git a/inc/app_replacer.php b/inc/app_replacer.php new file mode 100644 index 00000000..11488d18 --- /dev/null +++ b/inc/app_replacer.php @@ -0,0 +1,271 @@ + + */ +abstract class Optml_App_Replacer { + + /** + * Settings handler. + * + * @var Optml_Settings $settings + */ + protected $settings = null; + + /** + * Defines which is the maximum width accepted in the optimization process. + * + * @var int + */ + protected $max_width = 3000; + + /** + * Defines which is the maximum width accepted in the optimization process. + * + * @var int + */ + protected $max_height = 3000; + + /** + * Holds an array of image sizes. + * + * @var array + */ + protected static $image_sizes = array(); + + /** + * A cached version of `wp_upload_dir` + * + * @var null + */ + protected $upload_resource = null; + + /** + * Possible domain sources to optimize. + * + * @var array Domains. + */ + protected $possible_sources = array(); + + /** + * Whitelisted domains sources to optimize from, according to optimole service. + * + * @var array Domains. + */ + protected $allowed_sources = array(); + + /** + * Holds site mapping array, + * if there is already a cdn and we want to fetch the images from there + * and not from he original site. + * + * @var array Site mappings. + */ + protected $site_mappings = array(); + /** + * Whether the site is whitelisted or not. Used when signing the urls.. + * + * @var bool Domains. + */ + protected $is_allowed_site = array(); + + /** + * The initialize method. + */ + public function init() { + $this->settings = new Optml_Settings(); + + if ( ! $this->should_replace() ) { + return false; // @codeCoverageIgnore + } + $this->set_properties(); + + return true; + } + + /** + * Check if we should rewrite the urls. + * + * @return bool If we can replace the image. + */ + public function should_replace() { + + if ( is_admin() || ! $this->settings->is_connected() || ! $this->settings->is_enabled() || is_customize_preview() ) { + return false; // @codeCoverageIgnore + } + + if ( array_key_exists( 'preview', $_GET ) && 'true' == $_GET['preview'] ) { + return false; // @codeCoverageIgnore + } + + if ( array_key_exists( 'optml_off', $_GET ) && 'true' == $_GET['optml_off'] ) { + return false; // @codeCoverageIgnore + } + if ( array_key_exists( 'elementor-preview', $_GET ) && ! empty( $_GET['elementor-preview'] ) ) { + return false; // @codeCoverageIgnore + } + + return true; + } + + /** + * Set the cdn url based on the current connected user. + */ + public function set_properties() { + $upload_data = wp_upload_dir(); + $this->upload_resource = array( + 'url' => str_replace( array( 'https://', 'http://' ), '', $upload_data['baseurl'] ), + 'directory' => $upload_data['basedir'], + ); + $this->upload_resource['url_length'] = strlen( $this->upload_resource['url'] ); + + $service_data = $this->settings->get( 'service_data' ); + + Optml_Config::init( + array( + 'key' => $service_data['cdn_key'], + 'secret' => $service_data['cdn_secret'], + ) + ); + + if ( defined( 'OPTML_SITE_MIRROR' ) && constant( 'OPTML_SITE_MIRROR' ) ) { + $this->site_mappings = array( + rtrim( get_site_url(), '/' ) => rtrim( constant( 'OPTML_SITE_MIRROR' ), '/' ), + ); + } + + $this->possible_sources = $this->extract_domain_from_urls( + array_merge( + array( get_site_url() ), + array_values( $this->site_mappings ) + ) + ); + + $this->allowed_sources = $this->extract_domain_from_urls( $service_data['whitelist'] ); + + $this->is_allowed_site = count( array_diff_key( $this->possible_sources, $this->allowed_sources ) ) > 0; + + $this->max_height = $this->settings->get( 'max_height' ); + $this->max_width = $this->settings->get( 'max_width' ); + } + + /** + * Extract domains and use them as keys for fast processing. + * + * @param array $urls Input urls. + * + * @return array Array of domains as keys. + */ + protected function extract_domain_from_urls( $urls = array() ) { + if ( ! is_array( $urls ) ) { + return $urls; + } + + $urls = array_map( + function ( $value ) { + $parts = parse_url( $value ); + + return isset( $parts['host'] ) ? $parts['host'] : ''; + }, + $urls + ); + $urls = array_filter( $urls ); + $urls = array_unique( $urls ); + return array_fill_keys( $urls, true ); + } + + /** + * Checks if the file is a image size and return the full url. + * + * @param string $url The image URL. + * + * @return string + **/ + protected function strip_image_size_from_url( $url ) { + + if ( preg_match( '#(-\d+x\d+)\.(' . implode( '|', array_keys( Optml_Config::$extensions ) ) . '){1}$#i', $url, $src_parts ) ) { + $stripped_url = str_replace( $src_parts[1], '', $url ); + // Extracts the file path to the image minus the base url + $file_path = substr( $stripped_url, strpos( $stripped_url, $this->upload_resource['url'] ) + $this->upload_resource['url_length'] ); + if ( file_exists( $this->upload_resource['directory'] . $file_path ) ) { + $url = $stripped_url; + } + } + + return $url; + } + + /** + * Returns the array of image sizes since `get_intermediate_image_sizes` and image metadata doesn't include the + * custom image sizes in a reliable way. + * + * Inspired from jetpack/photon. + * + * @global $wp_additional_image_sizes + * + * @return array + */ + protected static function image_sizes() { + + if ( null != self::$image_sizes && is_array( self::$image_sizes ) ) { + return self::$image_sizes; + } + + global $_wp_additional_image_sizes; + + // Populate an array matching the data structure of $_wp_additional_image_sizes so we have a consistent structure for image sizes + $images = array( + 'thumb' => array( + 'width' => intval( get_option( 'thumbnail_size_w' ) ), + 'height' => intval( get_option( 'thumbnail_size_h' ) ), + 'crop' => get_option( 'thumbnail_crop', false ), + ), + 'medium' => array( + 'width' => intval( get_option( 'medium_size_w' ) ), + 'height' => intval( get_option( 'medium_size_h' ) ), + 'crop' => false, + ), + 'large' => array( + 'width' => intval( get_option( 'large_size_w' ) ), + 'height' => intval( get_option( 'large_size_h' ) ), + 'crop' => false, + ), + 'full' => array( + 'width' => null, + 'height' => null, + 'crop' => false, + ), + ); + + // Compatibility mapping as found in wp-includes/media.php + $images['thumbnail'] = $images['thumb']; + + // Update class variable, merging in $_wp_additional_image_sizes if any are set + if ( is_array( $_wp_additional_image_sizes ) && ! empty( $_wp_additional_image_sizes ) ) { + self::$image_sizes = array_merge( $images, $_wp_additional_image_sizes ); + } else { + self::$image_sizes = $images; + } + + return is_array( self::$image_sizes ) ? self::$image_sizes : array(); + } + + /** + * Check if we can replace the url. + * + * @param string $url Url to change. + * + * @return bool Either we can replace this url or not. + */ + public function can_replace_url( $url ) { + if ( ! is_string( $url ) ) { + return false; // @codeCoverageIgnore + } + $url = parse_url( $url ); + + return isset( $this->possible_sources[ $url['host'] ] ); + } +} diff --git a/inc/config.php b/inc/config.php new file mode 100644 index 00000000..ed0b06fd --- /dev/null +++ b/inc/config.php @@ -0,0 +1,81 @@ + + */ +class Optml_Config { + + /** + * A list of allowed extensions. + * + * @var array + */ + public static $extensions = array( + 'jpg|jpeg|jpe' => 'image/jpeg', + 'png' => 'image/png', + 'webp' => 'image/webp', + 'svg' => 'image/svg+xml', + ); + + /** + * Service api key. + * + * @var string Service key. + */ + public static $key = ''; + /** + * Service secret key used for signing the requests. + * + * @var string Secret key. + */ + public static $secret = ''; + /** + * Service url. + * + * @var string Service url. + */ + public static $service_url = ''; + + /** + * Service settings. + * + * @param array $service_settings Service settings. + * + * @throws \InvalidArgumentException In case that key or secret is not provided. + */ + public static function init( $service_settings = array() ) { + + if ( empty( $service_settings['key'] ) && ! defined( 'OPTML_KEY' ) ) { + throw new \InvalidArgumentException( 'Optimole SDK requires service api key.' ); // @codeCoverageIgnore + } + if ( empty( $service_settings['secret'] ) && ! defined( 'OPTML_SECRET' ) ) { + throw new \InvalidArgumentException( 'Optimole SDK requires service secret key.' ); // @codeCoverageIgnore + } + + if ( defined( 'OPTML_KEY' ) && constant( 'OPTML_KEY' ) ) { + self::$key = constant( 'OPTML_KEY' ); + } + + if ( defined( 'OPTML_SECRET' ) && constant( 'OPTML_SECRET' ) ) { + self::$secret = constant( 'OPTML_SECRET' ); + } + + if ( ! empty( $service_settings['key'] ) ) { + self::$key = trim( $service_settings['key'] ); + } + + if ( ! empty( $service_settings['secret'] ) ) { + self::$secret = trim( $service_settings['secret'] ); + } + + self::$service_url = sprintf( 'https://%s.i.optimole.com', self::$key ); + if ( isset( $service_settings['domain'] ) && ! empty( $service_settings['domain'] ) ) { + self::$service_url = $service_settings['domain']; + } elseif ( defined( 'OPTML_CUSTOM_DOMAIN' ) && constant( 'OPTML_CUSTOM_DOMAIN' ) ) { + self::$service_url = constant( 'OPTML_CUSTOM_DOMAIN' ); + } + } +} diff --git a/inc/image.php b/inc/image.php new file mode 100644 index 00000000..d7d5e664 --- /dev/null +++ b/inc/image.php @@ -0,0 +1,212 @@ + + */ +class Optml_Image { + + use Optml_Validator; + use Optml_Normalizer; + + /** + * Signature size. + */ + const SIGNATURE_SIZE = 10; + + /** + * Resize the image while keeping aspect ratio to fit given size. + */ + const RESIZE_FILL = 'fill'; + /** + * Resize the image while keeping aspect ratio + * to fill given size and cropping projecting parts. + */ + const RESIZE_FIT = 'fit'; + /** + * Crops the image to a given size. + */ + const RESIZE_CROP = 'crop'; + + + /** + * Top edge. + */ + const GRAVITY_NORTH = 'no'; + /** + * Bottom Edge. + */ + const GRAVITY_SOUTH = 'so'; + /** + * Right Edge. + */ + const GRAVITY_EAST = 'ea'; + /** + * Left edge. + */ + const GRAVITY_WEST = 'we'; + + /** + * Top right corner. + */ + const GRAVITY_NORTH_WEST = 'noea'; + /** + * Top left corner. + */ + const GRAVITY_NORTH_EAST = 'nowe'; + /** + * Bottom right corner. + */ + const GRAVITY_SOUTH_EAST = 'soea'; + /** + * Bottom left corner. + */ + const GRAVITY_SOUTH_WEST = 'sowe'; + + + /** + * Center + */ + const GRAVITY_CENTER = 'ce'; + /** + * Detects the most "interesting" section of the image and + * considers it as the center of the resulting image + */ + const GRAVITY_SMART = 'sm'; + /** + * Detects the most "interesting" section of the image and + * considers it as the center of the resulting image + */ + const GRAVITY_FOCUS_POINT = 'fp'; + + /** + * Floating point numbers between 0 and 1 that define the coordinates of the resulting image for X axis. + * + * @var int Focus point X. + */ + private $focus_point_x = 0; + /** + * Floating point numbers between 0 and 1 that define the coordinates of the resulting image for X axis. + * + * @var int Focus point Y. + */ + private $focus_point_y = 0; + + /** + * Quality of the resulting image. + * + * @var Optml_Quality Quality; + */ + private $quality = null; + /** + * Width of the resulting image. + * + * @var Optml_Width Width. + */ + private $width = null; + /** + * Height of the resulting image. + * + * @var Optml_Height Height. + */ + private $height = null; + /** + * Watermark for the image. + * + * @var Optml_Watermark Watermark. + */ + private $watermark = null; + /** + * Source image url. + * + * @var string Source image. + */ + private $source_url = ''; + + /** + * Optml_Image constructor. + * + * @param string $url Source image url. + * @param array $args Transformation arguments. + * + * @throws \InvalidArgumentException In case that the url is not provided. + */ + public function __construct( $url = '', $args = array() ) { + if ( empty( $url ) ) { + throw new \InvalidArgumentException( 'Optimole image builder requires the source url to optimize.' ); // @codeCoverageIgnore + } + $this->set_defaults(); + $this->width->set( $args['width'] ); + $this->height->set( $args['height'] ); + $this->quality->set( $args['quality'] ); + if ( isset( $args['watermark_id'] ) && $args['watermark_id'] != 0 ) { + $this->watermark->set( $args['watermark_id'] ); + } + + $this->source_url = $url; + + } + + /** + * Set defaults for image transformations. + */ + private function set_defaults() { + $this->width = new Optml_Width( 'auto' ); + $this->height = new Optml_Height( 'auto' ); + $this->quality = new Optml_Quality( 'auto' ); + $this->watermark = new Optml_Watermark(); + $this->focus_point_x = 0; + $this->focus_point_y = 0; + } + + /** + * Return transformed url. + * + * @param bool $signed Either will be signed or not. + * + * @return string Transformed image url. + */ + public function get_url( $signed = false ) { + $path_parts = array(); + if ( $this->width->get() > 0 ) { + $path_parts[] = $this->width->toString(); + } + if ( $this->height->get() > 0 ) { + $path_parts[] = $this->height->toString(); + } + if ( $this->quality->get() > 0 || $this->quality->get() === 'eco' ) { + $path_parts[] = $this->quality->toString(); + } + if ( ! empty( $this->watermark->get() ) && is_array( $this->watermark->get() ) && isset( $this->watermark->get()['id'] ) && $this->watermark->get()['id'] != 0 ) { + $path_parts[] = $this->watermark->toString(); + } + $path = '/plain/' . $this->source_url; + + if ( ! empty( $path_parts ) ) { + $path = sprintf( '/%s%s', implode( '/', $path_parts ), $path ); + } + if ( $signed ) { + $path = sprintf( '/%s%s', $this->get_signature( $path ), $path ); + } + + return sprintf( '%s%s', Optml_Config::$service_url, $path ); + + } + + /** + * Return the url signature. + * + * @param string $path The path from url. + * + * @return bool|string + */ + public function get_signature( $path = '' ) { + + $full_signature = hash_hmac( 'sha256', Optml_Config::$key . $path, Optml_Config::$secret, true ); + + return substr( $full_signature, 0, self::SIGNATURE_SIZE ); + } + +} diff --git a/inc/image_properties/height.php b/inc/image_properties/height.php new file mode 100644 index 00000000..53eef401 --- /dev/null +++ b/inc/image_properties/height.php @@ -0,0 +1,52 @@ +height = $value; + } + + /** + * Return property value. + * + * @return mixed + */ + public function get() { + return $this->height; + } + + /** + * Set property value. + * + * @param mixed $value Value to set. + */ + public function set( $value ) { + if ( $this->is_valid_numeric( $value ) ) { + $this->height = $this->to_positive_integer( $value ); + } + } + + /** + * Return ImageProxy URL formatted string property. + * + * @return mixed + */ + public function toString() { + return sprintf( 'h:%s', $this->height ); + } +} diff --git a/inc/image_properties/property_type.php b/inc/image_properties/property_type.php new file mode 100644 index 00000000..9d0692ec --- /dev/null +++ b/inc/image_properties/property_type.php @@ -0,0 +1,31 @@ +quality = $value; + } + + /** + * Return property value. + * + * @return mixed + */ + public function get() { + return $this->quality; + } + + /** + * Set property value. + * + * @param mixed $value Value to set. + */ + public function set( $value ) { + if ( $this->is_valid_numeric( $value ) ) { + $this->quality = $this->to_bound_integer( $value, 0, 100 ); + } + if ( $value === 'eco' ) { + $this->quality = 'eco'; + } + } + + /** + * Return ImageProxy URL formatted string property. + * + * @return mixed + */ + public function toString() { + return sprintf( 'q:%s', $this->quality ); + } +} diff --git a/inc/image_properties/watermark.php b/inc/image_properties/watermark.php new file mode 100644 index 00000000..dd2443bc --- /dev/null +++ b/inc/image_properties/watermark.php @@ -0,0 +1,59 @@ +watermark = $settings->get_site_settings()['watermark']; + } + + /** + * Return property value. + * + * @return mixed + */ + public function get() { + return $this->watermark; + } + + /** + * Set property value. + * + * @param mixed $value Value to set. + */ + public function set( $value ) { + if ( $this->is_valid_numeric( $value ) ) { + $this->watermark['id'] = $this->to_positive_integer( $value ); + } + } + + /** + * Return ImageProxy URL formatted string property. + * + * @return mixed + */ + public function toString() { + return sprintf( + 'wm:%s', + $this->watermark['id'] . ':' . + $this->watermark['opacity'] . ':' . + $this->watermark['position'] . ':' . + $this->watermark['x_offset'] . ':' . + $this->watermark['y_offset'] . ':' . + $this->watermark['scale'] + ); + } +} diff --git a/inc/image_properties/width.php b/inc/image_properties/width.php new file mode 100644 index 00000000..4a215721 --- /dev/null +++ b/inc/image_properties/width.php @@ -0,0 +1,52 @@ +width = $value; + } + + /** + * Return property value. + * + * @return mixed + */ + public function get() { + return $this->width; + } + + /** + * Set property value. + * + * @param mixed $value Value to set. + */ + public function set( $value ) { + if ( $this->is_valid_numeric( $value ) ) { + $this->width = $this->to_positive_integer( $value ); + } + } + + /** + * Return ImageProxy URL formatted string property. + * + * @return mixed + */ + public function toString() { + return sprintf( 'w:%s', $this->width ); + } +} diff --git a/inc/lazyload_replacer.php b/inc/lazyload_replacer.php new file mode 100644 index 00000000..481e3db7 --- /dev/null +++ b/inc/lazyload_replacer.php @@ -0,0 +1,159 @@ + + */ +final class Optml_Lazyload_Replacer extends Optml_App_Replacer { + use Optml_Normalizer; + use Optml_Validator; + + /** + * Cached object instance. + * + * @var Optml_Tag_Replacer + */ + protected static $instance = null; + + /** + * The initialize method. + */ + public function init() { + + if ( ! parent::init() ) { + return; // @codeCoverageIgnore + } + + if ( $this->settings->use_lazyload() ) { + add_filter( + 'max_srcset_image_width', + function() { + return 1; + } + ); + add_filter( 'optml_tag_replace', array( $this, 'lazyload_tag_replace' ), 1, 4 ); + } + } + + /** + * Replaces the tags with lazyload tags. + * + * @param string $new_tag The new tag. + * @param string $original_url The original URL. + * @param string $new_url The optimized URL. + * @param array $optml_args Options passed for URL optimization. + * + * @return string + */ + public function lazyload_tag_replace( $new_tag, $original_url, $new_url, $optml_args ) { + + if ( ! $this->can_lazyload_for( $original_url ) ) { + return Optml_Tag_Replacer::instance()->regular_tag_replace( $new_tag, $original_url, $new_url, $optml_args ); + } + + $optml_args['quality'] = 'eco'; + $low_url = apply_filters( 'optml_content_url', $original_url, $optml_args ); + + $no_script_tag = str_replace( + array( + 'src="' . $original_url . '"', + 'src=\"' . $original_url . '"', + ), + array( + 'src="' . $new_url . '"', + wp_slash( 'src="' . $new_url . '"' ), + ), + $new_tag + ); + $new_tag = str_replace( + array( + 'src="' . $original_url . '"', + 'src=\"' . $original_url . '"', + ), + array( + 'src="' . $low_url . '" data-opt-src="' . $new_url . '"', + wp_slash( 'src="' . $low_url . '" data-opt-src="' . $new_url . '"' ), + ), + $new_tag + ); + + return '' . $new_tag; + } + + /** + * Check if the lazyload is allowed for this url. + * + * @param string $url Url. + * + * @return bool We can lazyload? + */ + public function can_lazyload_for( $url ) { + if ( ! defined( 'OPTML_DISABLE_PNG_LAZYLOAD' ) ) { + return true; + } + if ( ! OPTML_DISABLE_PNG_LAZYLOAD ) { + return true; // @codeCoverageIgnore + } + $type = wp_check_filetype( + basename( $url ), + array( + 'png' => 'image/png', + ) + ); + if ( ! isset( $type['ext'] ) || empty( $type['ext'] ) ) { + return true; + } + + return false; + } + + /** + * Class instance method. + * + * @codeCoverageIgnore + * @static + * @since 1.0.0 + * @access public + * @return Optml_Tag_Replacer + */ + public static function instance() { + if ( is_null( self::$instance ) ) { + self::$instance = new self(); + add_action( 'after_setup_theme', array( self::$instance, 'init' ) ); + } + + return self::$instance; + } + + /** + * Throw error on object clone + * + * The whole idea of the singleton design pattern is that there is a single + * object therefore, we don't want the object to be cloned. + * + * @codeCoverageIgnore + * @access public + * @since 1.0.0 + * @return void + */ + public function __clone() { + // Cloning instances of the class is forbidden. + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); + } + + /** + * Disable unserializing of the class + * + * @codeCoverageIgnore + * @access public + * @since 1.0.0 + * @return void + */ + public function __wakeup() { + // Unserializing instances of the class is forbidden. + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); + } + +} diff --git a/inc/main.php b/inc/main.php index 98c2107a..9048bd68 100644 --- a/inc/main.php +++ b/inc/main.php @@ -14,21 +14,50 @@ final class Optml_Main { private static $_instance = null; /** - * Holds the replacer class. + * Holds the url replacer class. * * @access public * @since 1.0.0 - * @var Optml_Replacer Replacer instance. + * @var Optml_Url_Replacer Replacer instance. */ - public $replacer; + public $url_replacer; + + /** + * Holds the tag replacer class. + * + * @access public + * @since 1.0.0 + * @var Optml_Tag_Replacer Replacer instance. + */ + public $tag_replacer; + + /** + * Holds the lazyload replacer class. + * + * @access public + * @since 1.0.0 + * @var Optml_Lazyload_Replacer Replacer instance. + */ + public $lazyload_replacer; + + /** + * Holds the manager class. + * + * @access public + * @since 1.0.0 + * @var Optml_Manager Manager instance. + */ + public $manager; + /** - * Holds the replacer class. + * Holds the rest class. * * @access public * @since 1.0.0 - * @var Optml_Rest Replacer instance. + * @var Optml_Rest REST instance. */ public $rest; + /** * Holds the admin class. * @@ -62,10 +91,13 @@ public static function instance() { add_filter( 'optimole_wp_feedback_review_message', array( __CLASS__, 'change_review_message' ) ); add_filter( 'optimole_wp_logger_heading', array( __CLASS__, 'change_review_message' ) ); add_filter( 'optml_default_settings', array( __CLASS__, 'change_lazyload_default' ) ); - self::$_instance = new self(); - self::$_instance->replacer = Optml_Replacer::instance(); - self::$_instance->rest = new Optml_Rest(); - self::$_instance->admin = new Optml_Admin(); + self::$_instance = new self(); + self::$_instance->url_replacer = Optml_Url_Replacer::instance(); + self::$_instance->tag_replacer = Optml_Tag_Replacer::instance(); + self::$_instance->lazyload_replacer = Optml_Lazyload_Replacer::instance(); + self::$_instance->manager = Optml_Manager::instance(); + self::$_instance->rest = new Optml_Rest(); + self::$_instance->admin = new Optml_Admin(); } $vendor_file = OPTML_PATH . 'vendor/autoload.php'; if ( is_readable( $vendor_file ) ) { diff --git a/inc/manager.php b/inc/manager.php new file mode 100644 index 00000000..5eeb0c1e --- /dev/null +++ b/inc/manager.php @@ -0,0 +1,327 @@ + + */ +final class Optml_Manager { + + /** + * Cached object instance. + * + * @var Optml_Manager + */ + protected static $instance = null; + + /** + * The initialize method. + */ + public function init() { + add_filter( 'init', array( $this, 'filter_options_and_mods' ) ); + add_filter( 'the_content', array( $this, 'process_images_from_content' ), PHP_INT_MAX ); + add_action( 'template_redirect', array( $this, 'process_template_redirect_content' ), PHP_INT_MAX ); + add_action( 'get_post_metadata', array( $this, 'replace_meta' ), PHP_INT_MAX, 4 ); + } + + /** + * Handles the url replacement in options and theme mods. + */ + public function filter_options_and_mods() { + /** + * `optml_imgcdn_options_with_url` is a filter that allows themes or plugins to select which option + * holds an url and needs an optimization. + */ + $options_list = apply_filters( + 'optml_imgcdn_options_with_url', + array( + 'theme_mods_' . get_option( 'stylesheet' ), + 'theme_mods_' . get_option( 'template' ), + ) + ); + + foreach ( $options_list as $option ) { + add_filter( "option_$option", array( $this, 'replace_option_url' ) ); + } + + } + + /** + * A filter which turns a local url into an optimized CDN image url or an array of image urls. + * + * @param string $url The url which should be replaced. + * + * @return string Replaced url. + */ + public function replace_option_url( $url ) { + if ( empty( $url ) ) { + return $url; + } + // $url might be an array or an json encoded array with urls. + if ( is_array( $url ) || filter_var( $url, FILTER_VALIDATE_URL ) === false ) { + $array = $url; + $encoded = false; + + // it might a json encoded array + if ( is_string( $url ) ) { + $array = json_decode( $url, true ); + $encoded = true; + } + + // in case there is an array, apply it recursively. + if ( is_array( $array ) ) { + foreach ( $array as $index => $value ) { + $array[ $index ] = $this->replace_option_url( $value ); + } + + if ( $encoded ) { + return json_encode( $array ); + } + return $array; + } + + if ( filter_var( $url, FILTER_VALIDATE_URL ) === false ) { + return $url; + } + } + + return apply_filters( 'optml_content_url', $url ); + } + + /** + * Replace urls in post meta values. + * + * @param mixed $metadata Metadata. + * @param int $object_id Post id. + * @param string $meta_key Meta key. + * @param bool $single Is single. + * + * @return mixed Altered meta. + */ + public function replace_meta( $metadata, $object_id, $meta_key, $single ) { + + $meta_needed = '_elementor_data'; + + if ( isset( $meta_key ) && $meta_needed == $meta_key ) { + remove_filter( 'get_post_metadata', array( $this, 'replace_meta' ), PHP_INT_MAX ); + + $current_meta = get_post_meta( $object_id, $meta_needed, $single ); + add_filter( 'get_post_metadata', array( $this, 'replace_meta' ), PHP_INT_MAX, 4 ); + + if ( ! is_string( $current_meta ) ) { + return $metadata; + } + + return $this->replace_content( $current_meta, 'elementor' ); + } + + // Return original if the check does not pass + return $metadata; + } + + /** + * Init html replacer handler. + */ + public function process_template_redirect_content() { + // We no longer need this if the handler was started. + remove_filter( 'the_content', array( $this, 'process_images_from_content' ), PHP_INT_MAX ); + + ob_start( + array( &$this, 'replace_content' ) + ); + } + + /** + * Adds a filter with detected images tags and the content. + * + * @param string $content The HTML content. + * + * @return mixed + */ + public function process_images_from_content( $content ) { + if ( $this->should_ignore_image_tags() ) { + return $content; + } + $images = self::parse_images_from_html( $content ); + + if ( empty( $images ) ) { + return $content; + } + + return apply_filters( 'optml_content_images_tags', $content, $images ); + } + + /** + * Method to extract images from content. + * + * @param string $content The HTML content. + * + * @return array + */ + public function extract_image_urls_from_content( $content ) { + $regex = '/(?:http(?:s?):)(?:[\/\\\\|.|\w|\s|-])*\.(?:' . implode( '|', array_keys( Optml_Config::$extensions ) ) . ')/'; + preg_match_all( + $regex, + $content, + $urls + ); + + $urls = array_map( + function ( $value ) { + return rtrim( html_entity_decode( $value ), '\\' ); + }, + $urls[0] + ); + + $urls = array_unique( $urls ); + + return array_values( $urls ); + } + + /** + * Filter raw content for urls. + * + * @param string $html HTML to filter. + * @param string $context Context for $html. + * + * @return mixed Filtered content. + */ + public function replace_content( $html, $context = 'raw' ) { + $extracted_urls = $this->extract_image_urls_from_content( $html ); + $extracted_urls = apply_filters( 'optml_extracted_urls', $extracted_urls ); + $urls = array_combine( $extracted_urls, $extracted_urls ); + if ( $context == 'elementor' ) { + $urls = array_map( 'wp_unslash', $urls ); + } + $urls = array_map( + function ( $url ) { + return apply_filters( 'optml_content_url', $url ); + }, + $urls + ); + + $html = $this->process_images_from_content( $html ); + + foreach ( $urls as $origin => $replace ) { + if ( strpos( $html, '/' . $origin ) === false ) { + $html = str_replace( $origin, $replace, $html ); + } + } + return $html; + } + + /** + * Check if we are on a amp endpoint. + * + * IMPORTANT: This needs to be used after parse_query hook, otherwise will return false positives. + * + * @return bool + */ + protected function should_ignore_image_tags() { + // Ignore image tags replacement in amp context as they are not available. + if ( function_exists( 'is_amp_endpoint' ) ) { + return is_amp_endpoint(); + } + if ( function_exists( 'ampforwp_is_amp_endpoint' ) ) { + return ampforwp_is_amp_endpoint(); + } + + // Ignore image tag replacement in feed context as we don't need it. + if ( is_feed() ) { + return true; + } + + return false; + } + + /** + * Match all images and any relevant tags in a block of HTML. + * + * @param string $content Some HTML. + * + * @return array An array of $images matches, where $images[0] is + * an array of full matches, and the link_url, img_tag, + * and img_url keys are arrays of those matches. + */ + public static function parse_images_from_html( $content ) { + $images = array(); + + $content = self::strip_header_from_content( $content ); + + if ( preg_match_all( '/(?:]+?href=["|\'](?P[^\s]+?)["|\'][^>]*?>\s*)?(?P]*?\s+?src=\\\\?["|\'](?P[^\s]+?)["|\'].*?>){1}(?:\s*<\/a>)?/ism', $content, $images ) ) { + foreach ( $images as $key => $unused ) { + // Simplify the output as much as possible, mostly for confirming test results. + if ( is_numeric( $key ) && $key > 0 ) { + unset( $images[ $key ] ); + } + } + + return $images; + } + + return array(); + } + + /** + * Matches the header tag and removes it. + * + * @param string $content Some HTML. + * + * @return string The HTML without the
tag + */ + public static function strip_header_from_content( $content ) { + if ( preg_match( '//ismU', $content, $matches ) !== 1 ) { + return $content; + } + + return str_replace( $matches[0], '', $content ); + } + + /** + * Class instance method. + * + * @codeCoverageIgnore + * @static + * @since 1.0.0 + * @access public + * @return Optml_Manager + */ + public static function instance() { + if ( is_null( self::$instance ) ) { + self::$instance = new self(); + add_action( 'after_setup_theme', array( self::$instance, 'init' ) ); + } + + return self::$instance; + } + + /** + * Throw error on object clone + * + * The whole idea of the singleton design pattern is that there is a single + * object therefore, we don't want the object to be cloned. + * + * @codeCoverageIgnore + * @access public + * @since 1.0.0 + * @return void + */ + public function __clone() { + // Cloning instances of the class is forbidden. + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); + } + + /** + * Disable unserializing of the class + * + * @codeCoverageIgnore + * @access public + * @since 1.0.0 + * @return void + */ + public function __wakeup() { + // Unserializing instances of the class is forbidden. + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); + } +} diff --git a/inc/replacer.php b/inc/replacer.php deleted file mode 100644 index 7784b39e..00000000 --- a/inc/replacer.php +++ /dev/null @@ -1,1050 +0,0 @@ - - */ -class Optml_Replacer { - /** - * A list of allowed extensions. - * - * @var array - */ - public static $extensions = array( - 'jpg|jpeg|jpe' => 'image/jpeg', - 'png' => 'image/png', - 'webp' => 'image/webp', - 'svg' => 'image/svg+xml', - ); - /** - * Cached object instance. - * - * @var Optml_Replacer - */ - protected static $instance = null; - /** - * Holds an array of image sizes. - * - * @var array - */ - protected static $image_sizes = array(); - /** - * Site url. - * - * @var string $siteurl - */ - protected static $siteurl = null; - /** - * Site mirror, if any, usually CDN url. - * - * @var string $site_mirror - */ - protected static $site_mirror = null; - /** - * Te cdn url, it will be build on the run. - * - * @var null - */ - protected $cdn_url = null; - /** - * A secret key to encode payload. - * - * @var null - */ - protected $cdn_secret = null; - /** - * Domains Whitelist. - * - * @var array - */ - protected $whitelist = array(); - /** - * Lazyload setting. - * - * @var bool - */ - protected $lazyload = false; - /** - * Defines which is the maximum width accepted in the optimization process. - * - * @var int - */ - protected $max_width = 3000; - /** - * Defines the quality parameter. - * - * @var int - */ - protected $quality = 'auto'; - /** - * Defines which is the maximum width accepted in the optimization process. - * - * @var int - */ - protected $max_height = 3000; - /** - * Holds the real images sizes as an array. - * - * @var null - */ - protected $img_real_sizes = null; - /** - * A cached version of `wp_upload_dir` - * - * @var null - */ - protected $upload_resource = null; - /** - * Settings handler. - * - * @var Optml_Settings $settings - */ - protected $settings = null; - - /** - * Class instance method. - * - * @static - * @since 1.0.0 - * @access public - * @return Optml_Replacer - */ - public static function instance() { - if ( is_null( self::$instance ) ) { - self::$instance = new self(); - add_action( 'after_setup_theme', array( self::$instance, 'init' ) ); - } - - return self::$instance; - } - - /** - * The initialize method. - */ - function init() { - - add_filter( 'optml_replace_image', array( $this, 'get_imgcdn_url' ), 10, 2 ); - - $this->settings = new Optml_Settings(); - - $this->set_properties(); - - if ( ! $this->should_replace() ) { - return; - } - if ( empty( $this->cdn_url ) ) { - return; - } - - $this->lazyload = $this->settings->use_lazyload(); - $this->quality = $this->settings->get_quality(); - self::$site_mirror = defined( 'OPTML_SITE_MIRROR' ) ? OPTML_SITE_MIRROR : ''; - self::$siteurl = get_site_url(); - - add_filter( 'image_downsize', array( $this, 'filter_image_downsize' ), PHP_INT_MAX, 3 ); - add_filter( 'the_content', array( $this, 'filter_the_content' ), PHP_INT_MAX ); - add_filter( 'wp_calculate_image_srcset', array( $this, 'filter_srcset_attr' ), PHP_INT_MAX, 5 ); - add_filter( 'init', array( $this, 'filter_options_and_mods' ) ); - add_action( 'template_redirect', array( $this, 'init_html_replacer' ), PHP_INT_MAX ); - add_action( 'get_post_metadata', array( $this, 'replace_meta' ), PHP_INT_MAX, 4 ); - - } - - /** - * Set the cdn url based on the current connected user. - */ - public function set_properties() { - - $service_data = $this->settings->get( 'service_data' ); - if ( ! isset( $service_data['cdn_key'] ) ) { - return; - } - $cdn_key = $service_data ['cdn_key']; - $cdn_secret = $service_data['cdn_secret']; - - if ( empty( $cdn_key ) || empty( $cdn_secret ) ) { - return; - } - $this->cdn_secret = $cdn_secret; - $this->whitelist = isset( $service_data['whitelist'] ) ? $service_data['whitelist'] : array(); - $this->cdn_url = sprintf( - 'https://%s.%s', - strtolower( $cdn_key ), - 'i.optimole.com' - ); - $upload_data = wp_upload_dir(); - $this->upload_resource = array( - 'url' => str_replace( array( 'https://', 'http://' ), '', $upload_data['baseurl'] ), - 'directory' => $upload_data['basedir'], - ); - $this->upload_resource['url_length'] = strlen( $this->upload_resource['url'] ); - - if ( defined( 'OPTML_CUSTOM_DOMAIN' ) && ! empty( OPTML_CUSTOM_DOMAIN ) ) { - $this->cdn_url = OPTML_CUSTOM_DOMAIN; - } - } - - /** - * Check if we should rewrite the urls. - * - * @return bool If we can replace the image. - */ - public function should_replace() { - - if ( is_admin() ) { - - return false; - } - - if ( ! $this->settings->is_connected() ) { - return false; - } - if ( ! $this->settings->is_enabled() ) { - return false; - } - if ( array_key_exists( 'preview', $_GET ) && 'true' == $_GET['preview'] ) { - return false; - } - - if ( array_key_exists( 'optml_off', $_GET ) && 'true' == $_GET['optml_off'] ) { - return false; - } - if ( array_key_exists( 'elementor-preview', $_GET ) && ! empty( $_GET['elementor-preview'] ) ) { - return false; - } - - if ( is_customize_preview() ) { - return false; - } - - return true; - } - - /** - * Init html replacer handler. - */ - public function init_html_replacer() { - - if ( is_admin() ) { - return; - } - // We no longer need this if the handler was started. - remove_filter( 'the_content', array( $this, 'filter_the_content' ), PHP_INT_MAX ); - ob_start( - array( &$this, 'replace_urls' ) - ); - } - - /** - * This filter will replace all the images retrieved via "wp_get_image" type of functions. - * - * @param array $image The filtered value. - * @param int $attachment_id The related attachment id. - * @param array|string $size This could be the name of the thumbnail size or an array of custom dimensions. - * - * @return array - */ - public function filter_image_downsize( $image, $attachment_id, $size ) { - // we don't run optimizations on dashboard side - if ( is_admin() ) { - return $image; - } - if ( $this->lazyload && ! $this->ignore_lazyload() ) { - return $image; - } - $image_url = wp_get_attachment_url( $attachment_id ); - - if ( $image_url ) { - // $image_meta = image_get_intermediate_size( $attachment_id, $size ); - $image_meta = wp_get_attachment_metadata( $attachment_id ); - $image_args = self::image_sizes(); - - // default size - $sizes = array( - 'width' => isset( $image_meta['width'] ) ? intval( $image_meta['width'] ) : 'auto', - 'height' => isset( $image_meta['height'] ) ? intval( $image_meta['height'] ) : 'auto', - ); - // in case there is a custom image size $size will be an array. - if ( is_array( $size ) ) { - $sizes = array( - 'width' => ( $size[0] < $sizes['width'] ? $size[0] : $sizes['width'] ), - 'height' => ( $size[1] < $sizes['height'] ? $size[1] : $sizes['height'] ), - ); - } elseif ( 'full' !== $size && isset( $image_args[ $size ] ) ) { // overwrite if there a size - $sizes = array( - 'width' => $image_args[ $size ]['width'] < $sizes['width'] ? $image_args[ $size ]['width'] : $sizes['width'], - 'height' => $image_args[ $size ]['height'] < $sizes['height'] ? $image_args[ $size ]['height'] : $sizes['height'], - ); - } - - $new_sizes = $this->validate_image_sizes( $sizes['width'], $sizes['height'] ); - - // resized thumbnails will have their own filenames. we should get those instead of the full image one - if ( is_string( $size ) && ! empty( $image_meta['sizes'] ) && ! empty( $image_meta['sizes'][ $size ] ) ) { - $image_url = str_replace( basename( $image_url ), $image_meta['sizes'][ $size ]['file'], $image_url ); - } - - // try to get an optimized image url. - $new_url = $this->get_imgcdn_url( $image_url, $new_sizes ); - if ( $new_url === $image_url ) { - return $image; - } - $return = array( - $new_url, - $sizes['width'], - $sizes['height'], - false, - ); - - return $return; - } - - // in case something wrong comes, well return the default. - return $image; - } - - /** - * Check if we are on a amp endpoint. - * - * IMPORTANT: This needs to be used after parse_query hook, otherwise will return false positives. - * - * @return bool - */ - protected function ignore_lazyload() { - - if ( function_exists( 'is_amp_endpoint' ) ) { - return is_amp_endpoint(); - } - if ( function_exists( 'ampforwp_is_amp_endpoint' ) ) { - return ampforwp_is_amp_endpoint(); - } - if ( is_feed() ) { - return true; - } - return false; - } - - /** - * Returns the array of image sizes since `get_intermediate_image_sizes` and image metadata doesn't include the - * custom image sizes in a reliable way. - * - * Inspired from jetpack/photon. - * - * @global $wp_additional_image_sizes - * - * @return array - */ - protected static function image_sizes() { - if ( null == self::$image_sizes ) { - global $_wp_additional_image_sizes; - - // Populate an array matching the data structure of $_wp_additional_image_sizes so we have a consistent structure for image sizes - $images = array( - 'thumb' => array( - 'width' => intval( get_option( 'thumbnail_size_w' ) ), - 'height' => intval( get_option( 'thumbnail_size_h' ) ), - 'crop' => (bool) get_option( 'thumbnail_crop' ), - ), - 'medium' => array( - 'width' => intval( get_option( 'medium_size_w' ) ), - 'height' => intval( get_option( 'medium_size_h' ) ), - 'crop' => false, - ), - 'large' => array( - 'width' => intval( get_option( 'large_size_w' ) ), - 'height' => intval( get_option( 'large_size_h' ) ), - 'crop' => false, - ), - 'full' => array( - 'width' => null, - 'height' => null, - 'crop' => false, - ), - ); - - // Compatibility mapping as found in wp-includes/media.php - $images['thumbnail'] = $images['thumb']; - - // Update class variable, merging in $_wp_additional_image_sizes if any are set - if ( is_array( $_wp_additional_image_sizes ) && ! empty( $_wp_additional_image_sizes ) ) { - self::$image_sizes = array_merge( $images, $_wp_additional_image_sizes ); - } else { - self::$image_sizes = $images; - } - } - - return is_array( self::$image_sizes ) ? self::$image_sizes : array(); - } - - /** - * Keep the image sizes under a sane limit. - * - * @param string $width The width value which should be sanitized. - * @param string $height The height value which should be sanitized. - * - * @return array - */ - protected function validate_image_sizes( $width, $height ) { - global $content_width; - /** - * While we are inside a content filter we need to keep our max_width under the content_width global - * There is no reason the have a image wider than the content width. - */ - if ( - doing_filter( 'the_content' ) - && isset( $GLOBALS['content_width'] ) - && apply_filters( 'optml_imgcdn_allow_resize_images_from_content_width', false ) - ) { - $content_width = (int) $GLOBALS['content_width']; - - if ( $this->max_width > $content_width ) { - $this->max_width = $content_width; - } - } - - if ( $width > $this->max_width ) { - // we need to remember how much in percentage the width was resized and apply the same treatment to the height. - $percentWidth = ( 1 - $this->max_width / $width ) * 100; - $width = $this->max_width; - $height = round( $height * ( ( 100 - $percentWidth ) / 100 ), 0 ); - } - - // now for the height - if ( $height > $this->max_height ) { - $percentHeight = ( 1 - $this->max_height / $height ) * 100; - // if we reduce the height to max_height by $x percentage than we'll also reduce the width for the same amount. - $height = $this->max_height; - $width = round( $width * ( ( 100 - $percentHeight ) / 100 ), 0 ); - } - - return array( - 'width' => $width, - 'height' => $height, - ); - } - - /** - * Returns a signed image url authorized to be used in our CDN. - * - * @param string $url The url which should be signed. - * @param array $args Dimension params; Supports `width` and `height`. - * - * @return string - */ - public function get_imgcdn_url( - $url, $args = array( - 'width' => 'auto', - 'height' => 'auto', - 'quality' => '', - ) - ) { - if ( apply_filters( 'optml_dont_replace_url', false, $url ) ) { - return $url; - } - if ( strpos( $url, $this->cdn_url ) !== false ) { - return $url; - } - if ( ! $this->check_mimetype( $url ) ) { - - return $url; - } - // not used yet. - $compress_level = apply_filters( 'optml_image_quality', $this->quality ); - if ( isset( $args['quality'] ) && ! empty( $args['quality'] ) ) { - $compress_level = $args['quality']; - } - - $compress_level = $this->normalize_quality( $compress_level ); - // this will authorize the image - if ( ! empty( self::$site_mirror ) ) { - $url = str_replace( self::$siteurl, self::$site_mirror, $url ); - } - - $url_parts = explode( '://', $url ); - if ( count( $url_parts ) != 2 ) { - return $url; - } - - $scheme = trim( $url_parts[0] ); - - $path = $url_parts[1]; - - if ( $args['width'] !== 'auto' ) { - $args['width'] = round( $args['width'], 0 ); - if ( $args['width'] > $this->max_width ) { - $args['width'] = $this->max_width; - } - - if ( $args['width'] == 0 ) { - $args['width'] = 'auto'; - } - } - if ( $args['height'] !== 'auto' ) { - $args['height'] = round( $args['height'], 0 ); - if ( $args['height'] > $this->max_height ) { - $args['height'] = $this->max_height; - } - if ( $args['height'] == 0 ) { - $args['height'] = 'auto'; - } - } - - $hash = ''; - if ( empty( $this->whitelist ) ) { - $payload = array( - 'path' => $this->urlception_encode( $path ), - 'scheme' => $scheme, - 'width' => (string) $args['width'], - 'height' => (string) $args['height'], - 'quality' => (string) $compress_level, - ); - ksort( $payload ); - $values = array_values( $payload ); - $payload = implode( '', $values ); - $hash = sprintf( '/%s', hash_hmac( 'md5', $payload, $this->cdn_secret ) ); - } - $new_url = sprintf( - '%s%s/%s/%s/%s/%s/%s', - $this->cdn_url, - $hash, - (string) $args['width'], - (string) $args['height'], - (string) $compress_level, - $scheme, - $path - ); - - return $new_url; - } - - /** - * Check url mimetype. - * - * @param string $url Url to check. - * - * @return bool Is a valid image url or not. - */ - private function check_mimetype( $url ) { - - $mimes = self::$extensions; - $type = wp_check_filetype( $url, $mimes ); - - if ( ! isset( $type['ext'] ) || empty( $type['ext'] ) ) { - return false; - } - - return true; - } - - /** - * Sanitize quality. - * - * @param string|int $quality Normalize quality. - * - * @return int Numeric quality. - */ - private function normalize_quality( $quality ) { - - if ( is_numeric( $quality ) ) { - return intval( $quality ); - } - $quality = trim( $quality ); - if ( $quality === 'eco' ) { - return 'eco'; - } - if ( $quality === 'auto' ) { - return 'auto'; - } - if ( $quality === 'high_c' ) { - return 55; - } - if ( $quality === 'medium_c' ) { - return 75; - } - if ( $quality === 'low_c' ) { - return 90; - } - - // Legacy values. - return 60; - } - - /** - * Ensures that an url parameter can stand inside an url. - * - * @param string $url The required url. - * - * @return string - */ - protected function urlception_encode( $url ) { - $new_url = rtrim( $url, '/' ); - - return urlencode( $new_url ); - } - - /** - * Replace image URLs in the srcset attributes and in case there is a resize in action, also replace the sizes. - * - * @param array $sources Array of image sources. - * @param array $size_array Array of width and height values in pixels (in that order). - * @param string $image_src The 'src' of the image. - * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'. - * @param int $attachment_id Image attachment ID. - * - * @return array - */ - public function filter_srcset_attr( $sources = array(), $size_array = array(), $image_src = '', $image_meta = array(), $attachment_id = 0 ) { - if ( ! is_array( $sources ) ) { - return $sources; - } - if ( $this->lazyload && ! $this->ignore_lazyload() ) { - return array(); - } - $used = array(); - $new_sources = array(); - foreach ( $sources as $i => $source ) { - list( $width, $height ) = self::parse_dimensions_from_filename( $source['url'] ); - - if ( empty( $width ) ) { - $width = $image_meta['width']; - } - - if ( empty( $height ) ) { - $height = $image_meta['height']; - } - - $source['url'] = $this->strip_image_size_maybe( $source['url'] ); - $new_sizes = $this->validate_image_sizes( $width, $height ); - $new_url = $this->get_imgcdn_url( $source['url'], $new_sizes ); - if ( isset( $used[ md5( $new_url ) ] ) ) { - continue; - } - - $used[ md5( $new_url ) ] = true; - $new_sources[ $i ] = $sources[ $i ]; - $new_sources[ $i ]['url'] = $new_url; - - if ( $new_sources[ $i ]['descriptor'] ) { - $new_sources[ $i ]['value'] = $new_sizes['width']; - } else { - $new_sources[ $i ]['value'] = $new_sizes['height']; - } - } - - return $new_sources; - } - - /** - * Try to determine height and width from strings WP appends to resized image filenames. - * - * @param string $src The image URL. - * - * @return array An array consisting of width and height. - */ - public static function parse_dimensions_from_filename( $src ) { - $width_height_string = array(); - $extensions = array_keys( self::$extensions ); - if ( preg_match( '#-(\d+)x(\d+)\.(?:' . implode( '|', $extensions ) . '){1}$#i', $src, $width_height_string ) ) { - $width = (int) $width_height_string[1]; - $height = (int) $width_height_string[2]; - - if ( $width && $height ) { - return array( $width, $height ); - } - } - - return array( false, false ); - } - - /** - * Checks if the file is a image size and return the full url. - * - * @param string $src The image URL. - * - * @return string - **/ - protected function strip_image_size_maybe( $src ) { - - if ( preg_match( '#(-\d+x\d+)\.(' . implode( '|', array_keys( self::$extensions ) ) . '){1}$#i', $src, $src_parts ) ) { - $stripped_src = str_replace( $src_parts[1], '', $src ); - // Extracts the file path to the image minus the base url - $file_path = substr( $stripped_src, strpos( $stripped_src, $this->upload_resource['url'] ) + $this->upload_resource['url_length'] ); - if ( file_exists( $this->upload_resource['directory'] . $file_path ) ) { - $src = $stripped_src; - } - } - - return $src; - } - - /** - * Handles the url replacement in options and theme mods. - */ - public function filter_options_and_mods() { - /** - * `optml_imgcdn_options_with_url` is a filter that allows themes or plugins to select which option - * holds an url and needs an optimization. - */ - $options_list = apply_filters( - 'optml_imgcdn_options_with_url', - array( - 'theme_mods_' . get_option( 'stylesheet' ), - 'theme_mods_' . get_option( 'template' ), - ) - ); - - foreach ( $options_list as $option ) { - add_filter( "option_$option", array( $this, 'replace_option_url' ) ); - } - - } - - /** - * A filter which turns a local url into an optimized CDN image url or an array of image urls. - * - * @param string $url The url which should be replaced. - * - * @return string Replaced url. - */ - public function replace_option_url( $url ) { - if ( empty( $url ) ) { - return $url; - } - // $url might be an array or an json encoded array with urls. - if ( is_array( $url ) || filter_var( $url, FILTER_VALIDATE_URL ) === false ) { - $array = $url; - $encoded = false; - - // it might a json encoded array - if ( is_string( $url ) ) { - $array = json_decode( $url, true ); - $encoded = true; - } - - // in case there is an array, apply it recursively. - if ( is_array( $array ) ) { - foreach ( $array as $index => $value ) { - $array[ $index ] = $this->replace_option_url( $value ); - } - - if ( $encoded ) { - return json_encode( $array ); - } else { - return $array; - } - } - - if ( filter_var( $url, FILTER_VALIDATE_URL ) === false ) { - return $url; - } - } - - $new_url = $this->get_imgcdn_url( $url ); - - return $new_url; - } - - /** - * Replace urls in post meta values. - * - * @param mixed $metadata Metadata. - * @param int $object_id Post id. - * @param string $meta_key Meta key. - * @param bool $single Is single. - * - * @return mixed Altered meta. - */ - function replace_meta( $metadata, $object_id, $meta_key, $single ) { - - $meta_needed = '_elementor_data'; - - if ( isset( $meta_key ) && $meta_needed == $meta_key ) { - remove_filter( 'get_post_metadata', array( $this, 'replace_meta' ), PHP_INT_MAX ); - - $current_meta = get_post_meta( $object_id, $meta_needed, $single ); - add_filter( 'get_post_metadata', array( $this, 'replace_meta' ), PHP_INT_MAX, 4 ); - - if ( ! is_string( $current_meta ) ) { - return $metadata; - } - - return $this->replace_urls( $current_meta, 'elementor' ); - } - - // Return original if the check does not pass - return $metadata; - } - - /** - * Filter raw content for urls. - * - * @param string $html HTML to filter. - * - * @return mixed Filtered content. - */ - public function replace_urls( $html, $context = 'raw' ) { - $html = $this->filter_the_content( $html ); - $old_urls = $this->extract_non_replaced_urls( $html ); - $urls = array_combine( $old_urls, $old_urls ); - switch ( $context ) { - case 'elementor': - $urls = array_map( 'wp_unslash', $urls ); - // return $html; - break; - } - $urls = array_map( - function ( $url ) { - $tmp_new_url = $this->strip_image_size_maybe( $url ); - $new_url = $this->get_imgcdn_url( $tmp_new_url ); - if ( $tmp_new_url == $new_url ) { - return $url; - } - - return $new_url; - }, - $urls - ); - - return str_replace( array_keys( $urls ), array_values( $urls ), $html ); - } - - /** - * Identify images in post content. - * - * @param string $content The post content which will be filtered. - * - * @return string - */ - public function filter_the_content( $content ) { - if ( $this->ignore_lazyload() ) { - return $content; - } - $images = self::parse_images_from_html( $content ); - - if ( empty( $images ) ) { - return $content; - } - $image_sizes = self::image_sizes(); - - foreach ( $images[0] as $index => $tag ) { - $width = $height = false; - $new_tag = $tag; - $src = $tmp = wp_unslash( $images['img_url'][ $index ] ); - - if ( apply_filters( 'optml_imgcdn_disable_optimization_for_link', false, $src ) ) { - continue; - } - - if ( false !== strpos( $src, 'i.optimole.com' ) ) { - continue; // we already have this - } - if ( false === strpos( $src, self::$siteurl ) && ( empty( self::$site_mirror ) || false === strpos( $src, self::$site_mirror ) ) ) { - continue; - } - - // try to get the declared sizes from the img tag - if ( preg_match( '#width=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $width_string ) ) { - $width = $width_string[1]; - } - - if ( preg_match( '#height=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $height_string ) ) { - $height = $height_string[1]; - } - - // Detect WP registered image size from HTML class - if ( preg_match( '#class=["|\']?[^"\']*size-([^"\'\s]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $size ) ) { - $size = array_pop( $size ); - - if ( false === $width && false === $height && 'full' != $size && array_key_exists( $size, $image_sizes ) ) { - $width = (int) $image_sizes[ $size ]['width']; - $height = (int) $image_sizes[ $size ]['height']; - } - } else { - unset( $size ); - } - $new_sizes = $this->validate_image_sizes( $width, $height ); - $tmp = $this->strip_image_size_maybe( $tmp ); - - $new_url = $this->get_imgcdn_url( $tmp, $new_sizes ); - if ( $new_url === $tmp ) { - continue; - } - // replace the url in hrefs or links - if ( ! empty( $images['link_url'][ $index ] ) ) { - if ( $this->check_mimetype( $images['link_url'][ $index ] ) ) { - $new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . $new_url . '\2', $tag, 1 ); - } - } - $new_tag = str_replace( 'width="' . $width . '"', 'width="' . $new_sizes['width'] . '"', $new_tag ); - $new_tag = str_replace( 'height="' . $height . '"', 'height="' . $new_sizes['height'] . '"', $new_tag ); - - if ( $this->lazyload && $this->can_lazyload( $tmp ) ) { - $new_sizes['quality'] = 'eco'; - $low_url = $this->get_imgcdn_url( $tmp, $new_sizes ); - - $noscript_tag = str_replace( - array( - 'src="' . $images['img_url'][ $index ] . '"', - 'src=\"' . $images['img_url'][ $index ] . '"', - ), - array( - 'src="' . $new_url . '"', - wp_slash( 'src="' . $new_url . '"' ), - ), - $new_tag - ); - $new_tag = str_replace( - array( - 'src="' . $images['img_url'][ $index ] . '"', - 'src=\"' . $images['img_url'][ $index ] . '"', - ), - array( - 'src="' . $low_url . '" data-opt-src="' . $new_url . '"', - wp_slash( 'src="' . $low_url . '" data-opt-src="' . $new_url . '"' ), - ), - $new_tag - ); - - $new_tag = '' . $new_tag; - } else { - $new_tag = str_replace( 'src="' . $images['img_url'][ $index ] . '"', 'src="' . $new_url . '"', $new_tag ); - } - - $content = str_replace( $tag, $new_tag, $content ); - } - - return $content; - } - - /** - * Match all images and any relevant tags in a block of HTML. - * - * @param string $content Some HTML. - * - * @return array An array of $images matches, where $images[0] is - * an array of full matches, and the link_url, img_tag, - * and img_url keys are arrays of those matches. - */ - public static function parse_images_from_html( $content ) { - $images = array(); - - $content = self::strip_header_from_content( $content ); - - if ( preg_match_all( '/(?:]+?href=["|\'](?P[^\s]+?)["|\'][^>]*?>\s*)?(?P]*?\s+?src=\\\\?["|\'](?P[^\s]+?)["|\'].*?>){1}(?:\s*<\/a>)?/ism', $content, $images ) ) { - foreach ( $images as $key => $unused ) { - // Simplify the output as much as possible, mostly for confirming test results. - if ( is_numeric( $key ) && $key > 0 ) { - unset( $images[ $key ] ); - } - } - - return $images; - } - - return array(); - } - - /** - * Matches the header tag and removes it. - * - * @param string $content Some HTML. - * - * @return string The HTML without the
tag - */ - public static function strip_header_from_content( $content ) { - if ( preg_match( '//ismU', $content, $matches ) !== 1 ) { - return $content; - } - - return str_replace( $matches[0], '', $content ); - } - - /** - * Check if the lazyload is allowed for this url. - * - * @param string $url Url. - * - * @return bool We can lazyload? - */ - public function can_lazyload( $url ) { - if ( ! defined( 'OPTML_DISABLE_PNG_LAZYLOAD' ) ) { - return true; - } - if ( ! OPTML_DISABLE_PNG_LAZYLOAD ) { - return true; - } - $type = wp_check_filetype( - basename( $url ), - array( - 'png' => 'image/png', - ) - ); - if ( ! isset( $type['ext'] ) || empty( $type['ext'] ) ) { - return true; - } - - return false; - } - - /** - * Extract slashed urls from content. - * - * @param string $content Content to parse. - * - * @return array Urls found. - */ - private function extract_non_replaced_urls( $content ) { - /** - * Based on the extract_url patter. - * - * @var string Regex rule string. - */ - $regex = '/(?:http(?:s?):)(?:[\/\\\\|.|\w|\s|-](?!i.optimole.com))*\.(?:' . implode( '|', array_keys( self::$extensions ) ) . ')/'; - preg_match_all( - $regex, - $content, - $urls - ); - - $urls = array_map( - function ( $value ) { - return rtrim( html_entity_decode( $value ), '\\' ); - }, - $urls[0] - ); - - $urls = array_unique( $urls ); - - return array_values( $urls ); - } - - /** - * Throw error on object clone - * - * The whole idea of the singleton design pattern is that there is a single - * object therefore, we don't want the object to be cloned. - * - * @access public - * @since 1.0.0 - * @return void - */ - public function __clone() { - // Cloning instances of the class is forbidden. - _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); - } - - /** - * Disable unserializing of the class - * - * @access public - * @since 1.0.0 - * @return void - */ - public function __wakeup() { - // Unserializing instances of the class is forbidden. - _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); - } - -} diff --git a/inc/rest.php b/inc/rest.php index 25d880cb..28e721f0 100644 --- a/inc/rest.php +++ b/inc/rest.php @@ -9,6 +9,8 @@ /** * Class Optml_Rest + * + * @codeCoverageIgnore */ class Optml_Rest { /** @@ -31,6 +33,30 @@ public function __construct() { */ public function register() { + $this->register_service_routes(); + + register_rest_route( + $this->namespace, + '/update_option', + array( + array( + 'methods' => \WP_REST_Server::CREATABLE, + 'permission_callback' => function () { + return current_user_can( 'manage_options' ); + }, + 'callback' => array( $this, 'update_option' ), + ), + ) + ); + + $this->register_image_routes(); + $this->register_watermark_routes(); + } + + /** + * Method to register service specific routes. + */ + public function register_service_routes() { register_rest_route( $this->namespace, '/connect', @@ -82,42 +108,80 @@ public function register() { ), ) ); + } + + /** + * Method to register image specific routes. + */ + public function register_image_routes() { + register_rest_route( + $this->namespace, + '/poll_optimized_images', + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'permission_callback' => function () { + return current_user_can( 'manage_options' ); + }, + 'callback' => array( $this, 'poll_optimized_images' ), + ), + ) + ); register_rest_route( $this->namespace, - '/update_option', + '/images-sample-rate', array( array( 'methods' => \WP_REST_Server::CREATABLE, 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, - 'callback' => array( $this, 'update_option' ), + 'callback' => array( $this, 'get_sample_rate' ), ), ) ); + } + + /** + * Method to register watermark specific routes. + */ + public function register_watermark_routes() { register_rest_route( $this->namespace, - '/poll_optimized_images', + '/poll_watermarks', array( array( 'methods' => \WP_REST_Server::READABLE, 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, - 'callback' => array( $this, 'poll_optimized_images' ), + 'callback' => array( $this, 'poll_watermarks' ), ), ) ); register_rest_route( $this->namespace, - '/images-sample-rate', + '/add_watermark', array( array( 'methods' => \WP_REST_Server::CREATABLE, 'permission_callback' => function () { return current_user_can( 'manage_options' ); }, - 'callback' => array( $this, 'get_sample_rate' ), + 'callback' => array( $this, 'add_watermark' ), + ), + ) + ); + register_rest_route( + $this->namespace, + '/remove_watermark', + array( + array( + 'methods' => \WP_REST_Server::CREATABLE, + 'permission_callback' => function () { + return current_user_can( 'manage_options' ); + }, + 'callback' => array( $this, 'remove_watermark' ), ), ) ); @@ -250,11 +314,11 @@ private function fetch_sample_image() { 'id' => - 1, ); } - $id = $image_result->posts[ array_rand( $image_result->posts, 1 ) ]; + $attachment_id = $image_result->posts[ array_rand( $image_result->posts, 1 ) ]; - $original_image_url = wp_get_attachment_image_url( $id, 'full' ); + $original_image_url = wp_get_attachment_image_url( $attachment_id, 'full' ); - $metadata = wp_get_attachment_metadata( $id ); + $metadata = wp_get_attachment_metadata( $attachment_id ); $width = 'auto'; $height = 'auto'; @@ -266,7 +330,7 @@ private function fetch_sample_image() { return array( 'url' => $original_image_url, - 'id' => $id, + 'id' => $attachment_id, 'width' => $width, 'height' => $height, ); @@ -275,6 +339,7 @@ private function fetch_sample_image() { /** * Disconnect from optimole service. * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @param WP_REST_Request $request disconnect rest request. */ public function disconnect( WP_REST_Request $request ) { @@ -303,10 +368,57 @@ public function poll_optimized_images( WP_REST_Request $request ) { return $this->response( $final_images ); } + /** + * Get watermarks from API. + * + * @param WP_REST_Request $request rest request. + * + * @return WP_REST_Response + */ + public function poll_watermarks( WP_REST_Request $request ) { + $api_key = $request->get_param( 'api_key' ); + $request = new Optml_Api(); + $watermarks = $request->get_watermarks( $api_key ); + if ( ! isset( $watermarks['watermarks'] ) || empty( $watermarks['watermarks'] ) ) { + return $this->response( array() ); + } + $final_images = array_splice( $watermarks['watermarks'], 0, 10 ); + return $this->response( $final_images ); + } + + /** + * Add watermark. + * + * @param WP_REST_Request $request rest request. + * + * @return WP_REST_Response + */ + public function add_watermark( WP_REST_Request $request ) { + $file = $request->get_file_params(); + $api_key = $request->get_param( 'api_key' ); + $request = new Optml_Api(); + return $this->response( $request->add_watermark( $file, $api_key ) ); + } + + /** + * Remove watermark. + * + * @param WP_REST_Request $request rest request. + * + * @return WP_REST_Response + */ + public function remove_watermark( WP_REST_Request $request ) { + $post_id = $request->get_param( 'postID' ); + $api_key = $request->get_param( 'api_key' ); + $request = new Optml_Api(); + return $this->response( $request->remove_watermark( $post_id, $api_key ) ); + } + /** * Update options method. * * @param WP_REST_Request $request option update rest request. + * @return WP_REST_Response */ public function update_option( WP_REST_Request $request ) { $new_settings = $request->get_param( 'settings' ); @@ -316,39 +428,7 @@ public function update_option( WP_REST_Request $request ) { } $settings = new Optml_Settings(); - // TODO Move validation in settings model. - $sanitized = array(); - foreach ( $new_settings as $key => $value ) { - switch ( $key ) { - case 'admin_bar_item': - case 'lazyload': - case 'image_replacer': - $sanitized_value = ( $value === 'enabled' || $value === 'disabled' ) ? $value : 'enabled'; - break; - case 'max_width': - case 'max_height': - $sanitized_value = absint( $value ); - if ( $sanitized_value < 100 ) { - $sanitized_value = 100; - } - if ( $sanitized_value > 5000 ) { - $sanitized_value = 5000; - } - - break; - case 'quality': - $sanitized_value = ( $value === 'low_c' || $value === 'medium_c' || $value === 'auto' || $value === 'high_c' ) ? $value : 'auto'; - break; - default: - $sanitized_value = ''; - break; - } - if ( empty( $sanitized_value ) ) { - continue; - } - $sanitized[ $key ] = $sanitized_value; - $settings->update( $key, $sanitized_value ); - } + $sanitized = $settings->parse_settings( $new_settings ); return $this->response( $sanitized ); } diff --git a/inc/settings.php b/inc/settings.php index cb1f4117..da81e1a1 100644 --- a/inc/settings.php +++ b/inc/settings.php @@ -4,6 +4,7 @@ * Class Optml_Settings. */ class Optml_Settings { + use Optml_Normalizer; /** * Default settings schema. @@ -18,6 +19,12 @@ class Optml_Settings { 'admin_bar_item' => 'enabled', 'lazyload' => 'disabled', 'quality' => 'auto', + 'wm_id' => 0, + 'wm_opacity' => 1, + 'wm_position' => Optml_Image::GRAVITY_CENTER, + 'wm_x' => 0, + 'wm_y' => 0, + 'wm_scale' => 0, 'image_replacer' => 'enabled', ); /** @@ -40,13 +47,60 @@ public function __construct() { $this->namespace = OPTML_NAMESPACE . '_settings'; $this->default_schema = apply_filters( 'optml_default_settings', $this->default_schema ); $this->options = wp_parse_args( get_option( $this->namespace, $this->default_schema ), $this->default_schema ); - if ( defined( 'OPTIML_ENABLED_MU' ) && OPTIML_ENABLED_MU && defined( 'OPTIML_MU_SITE_ID' ) && ! empty( OPTIML_MU_SITE_ID ) ) { - switch_to_blog( OPTIML_MU_SITE_ID ); + if ( defined( 'OPTIML_ENABLED_MU' ) && defined( 'OPTIML_MU_SITE_ID' ) && $this->to_boolean( constant( 'OPTIML_ENABLED_MU' ) ) && constant( 'OPTIML_MU_SITE_ID' ) ) { + switch_to_blog( constant( 'OPTIML_MU_SITE_ID' ) ); $this->options = wp_parse_args( get_option( $this->namespace, $this->default_schema ), $this->default_schema ); restore_current_blog(); } } + /** + * Process settings. + * + * @param array $new_settings List of settings. + * + * @return array + */ + public function parse_settings( $new_settings ) { + $sanitized = array(); + foreach ( $new_settings as $key => $value ) { + switch ( $key ) { + case 'admin_bar_item': + case 'lazyload': + case 'image_replacer': + $sanitized_value = $this->to_map_values( $value, array( 'enabled', 'disabled' ), 'enabled' ); + break; + case 'max_width': + case 'max_height': + case 'max_height': + case 'max_height': + $sanitized_value = $this->to_bound_integer( $value, 100, 5000 ); + break; + case 'quality': + $sanitized_value = $this->to_map_values( $value, array( 'low_c', 'medium_c', 'high_c', 'auto' ), 'auto' ); + break; + case 'wm_id': + $sanitized_value = intval( $value ); + break; + case 'wm_opacity': + case 'wm_scale': + case 'wm_x': + case 'wm_y': + $sanitized_value = floatval( $value ); + break; + default: + $sanitized_value = ''; + break; + } + if ( empty( $sanitized_value ) ) { + continue; + } + $sanitized[ $key ] = $sanitized_value; + $this->update( $key, $sanitized_value ); + } + return $sanitized; + } + /** * Check if the user is connected to Optimole. * @@ -104,6 +158,23 @@ public function get_site_settings() { 'image_replacer' => $this->get( 'image_replacer' ), 'max_width' => $this->get( 'max_width' ), 'max_height' => $this->get( 'max_height' ), + 'watermark' => $this->get_watermark(), + ); + } + + /** + * Return an watermark array. + * + * @return array + */ + public function get_watermark() { + return array( + 'id' => $this->get( 'wm_id' ), + 'opacity' => $this->get( 'wm_opacity' ), + 'position' => $this->get( 'wm_position' ), + 'x_offset' => $this->get( 'wm_x' ), + 'y_offset' => $this->get( 'wm_y' ), + 'scale' => $this->get( 'wm_scale' ), ); } @@ -136,14 +207,7 @@ public function get_quality() { */ public function is_enabled() { $status = $this->get( 'image_replacer' ); - if ( $status === 'disabled' ) { - return false; - } - if ( empty( $status ) ) { - return false; - } - - return true; + return $this->to_boolean( $status ); } /** @@ -153,14 +217,7 @@ public function is_enabled() { */ public function use_lazyload() { $status = $this->get( 'lazyload' ); - if ( $status === 'disabled' ) { - return false; - } - if ( empty( $status ) ) { - return false; - } - - return true; + return $this->to_boolean( $status ); } /** @@ -187,7 +244,6 @@ public function get_cdn_url() { * @return bool Reset action status. */ public function reset() { - $update = update_option( $this->namespace, $this->default_schema ); if ( $update ) { $this->options = $this->default_schema; @@ -209,16 +265,16 @@ public function update( $key, $value ) { return false; } // If we try to update from a website which is not the main OPTML blog, bail. - if ( defined( 'OPTIML_ENABLED_MU' ) && OPTIML_ENABLED_MU && defined( 'OPTIML_MU_SITE_ID' ) && ! empty( OPTIML_MU_SITE_ID ) ) { - if ( intval( OPTIML_MU_SITE_ID ) !== get_current_blog_id() ) { - return false; - } + if ( defined( 'OPTIML_ENABLED_MU' ) && constant( 'OPTIML_ENABLED_MU' ) && defined( 'OPTIML_MU_SITE_ID' ) && constant( 'OPTIML_MU_SITE_ID' ) && + intval( constant( 'OPTIML_MU_SITE_ID' ) ) !== get_current_blog_id() + ) { + return false; } - $options = $this->options; - $options[ $key ] = $value; - $update = update_option( $this->namespace, $options ); + $opt = $this->options; + $opt[ $key ] = $value; + $update = update_option( $this->namespace, $opt, false ); if ( $update ) { - $this->options = $options; + $this->options = $opt; } return $update; diff --git a/inc/tag_replacer.php b/inc/tag_replacer.php new file mode 100644 index 00000000..93892d74 --- /dev/null +++ b/inc/tag_replacer.php @@ -0,0 +1,331 @@ + + */ +final class Optml_Tag_Replacer extends Optml_App_Replacer { + use Optml_Normalizer; + use Optml_Validator; + + /** + * Cached object instance. + * + * @var Optml_Tag_Replacer + */ + protected static $instance = null; + + /** + * The initialize method. + */ + public function init() { + + if ( ! parent::init() ) { + return; + } + + add_filter( 'optml_content_images_tags', array( $this, 'process_image_tags' ), 1, 2 ); + + if ( ! $this->settings->use_lazyload() ) { + add_filter( 'optml_tag_replace', array( $this, 'regular_tag_replace' ), 1, 4 ); + add_filter( 'image_downsize', array( $this, 'filter_image_downsize' ), PHP_INT_MAX, 3 ); + add_filter( 'wp_calculate_image_srcset', array( $this, 'filter_srcset_attr' ), PHP_INT_MAX, 5 ); + add_filter( 'wp_calculate_image_sizes', array( $this, 'filter_sizes_attr' ), 1, 2 ); + } + } + + /** + * Extract image dimensions from img tag. + * + * @param string $tag The HTML img tag. + * @param array $image_sizes WordPress supported image sizes. + * @param array $args Default args to use. + * + * @return array + */ + private function parse_dimensions_from_tag( $tag, $image_sizes, $args = array() ) { + if ( preg_match( '#width=["|\']?([\d%]+)["|\']?#i', $tag, $width_string ) ) { + $args['width'] = $width_string[1]; + } + if ( preg_match( '#height=["|\']?([\d%]+)["|\']?#i', $tag, $height_string ) ) { + $args['height'] = $height_string[1]; + } + if ( preg_match( '#class=["|\']?[^"\']*size-([^"\'\s]+)[^"\']*["|\']?#i', $tag, $size ) ) { + $size = array_pop( $size ); + if ( false === $args['width'] && false === $args['height'] && 'full' != $size && array_key_exists( $size, $image_sizes ) ) { + $args['width'] = (int) $image_sizes[ $size ]['width']; + $args['height'] = (int) $image_sizes[ $size ]['height']; + $args['resize'] = $this->to_optml_crop( $image_sizes[ $size ]['crop'] ); + } + } + + return array( $args['width'], $args['height'], $args['resize'] ); + } + + /** + * Try to determine height and width from strings WP appends to resized image filenames. + * + * @param string $src The image URL. + * + * @return array An array consisting of width and height. + */ + public static function parse_dimensions_from_filename( $src ) { + $width_height_string = array(); + $extensions = array_keys( Optml_Config::$extensions ); + if ( preg_match( '#-(\d+)x(\d+)\.(?:' . implode( '|', $extensions ) . '){1}$#i', $src, $width_height_string ) ) { + $width = (int) $width_height_string[1]; + $height = (int) $width_height_string[2]; + + if ( $width && $height ) { + return array( $width, $height ); + } + } + + return array( false, false ); + } + + /** + * Called by hook to replace image tags in content. + * + * @param string $content The content to process. + * @param array $images List of image tags. + * + * @return mixed + */ + public function process_image_tags( $content, $images = array() ) { + $image_sizes = self::image_sizes(); + foreach ( $images[0] as $index => $tag ) { + $width = $height = false; + $resize = array( 'resize' => Optml_Image::RESIZE_FIT ); + $new_tag = $tag; + $src = $tmp = wp_unslash( $images['img_url'][ $index ] ); + if ( apply_filters( 'optml_ignore_image_link', false, $src ) || + false !== strpos( $src, Optml_Config::$service_url ) || + ! $this->can_replace_url( $src ) + ) { + continue; // @codeCoverageIgnore + } + + list( $width, $height, $resize ) = self::parse_dimensions_from_tag( $images['img_tag'][ $index ], $image_sizes, array( 'width' => $width, 'height' => $height, 'resize' => $resize ) ); + if ( false === $width && false === $height ) { + list( $width, $height ) = self::parse_dimensions_from_filename( $tmp ); + } + $optml_args = $this->to_optml_dimensions_bound( $width, $height, $this->max_width, $this->max_height ); + $tmp = $this->strip_image_size_from_url( $tmp ); + $optml_args = array_merge( $optml_args, $resize ); + + $new_url = apply_filters( 'optml_content_url', $tmp, $optml_args ); + + if ( $new_url === $tmp ) { + continue; // @codeCoverageIgnore + } + // replace the url in hrefs or links + if ( ! empty( $images['link_url'][ $index ] ) && $this->is_valid_mimetype_from_url( $images['link_url'][ $index ] ) ) { + $new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . apply_filters( 'optml_content_url', $tmp, $optml_args ) . '\2', $tag, 1 ); + } + + $new_tag = str_replace( 'width="' . $width . '"', 'width="' . $optml_args['width'] . '"', $new_tag ); + $new_tag = str_replace( 'height="' . $height . '"', 'height="' . $optml_args['height'] . '"', $new_tag ); + $new_tag = apply_filters( 'optml_tag_replace', $new_tag, $images['img_url'][ $index ], $new_url, $optml_args ); + + $content = str_replace( $tag, $new_tag, $content ); + } + return $content; + } + + /** + * Replaces the tags by default. + * + * @param string $new_tag The new tag. + * @param string $original_url The original URL. + * @param string $new_url The optimized URL. + * @param array $optml_args Options passed for URL optimization. + * + * @return string + */ + public function regular_tag_replace( $new_tag, $original_url, $new_url, $optml_args ) { + return str_replace( 'src="' . $original_url . '"', 'src="' . $new_url . '"', $new_tag ); + } + + /** + * Replace image URLs in the srcset attributes and in case there is a resize in action, also replace the sizes. + * + * @param array $sources Array of image sources. + * @param array $size_array Array of width and height values in pixels (in that order). + * @param string $image_src The 'src' of the image. + * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'. + * @param int $attachment_id Image attachment ID. + * + * @return array + */ + public function filter_srcset_attr( $sources = array(), $size_array = array(), $image_src = '', $image_meta = array(), $attachment_id = 0 ) { + if ( ! is_array( $sources ) ) { + return $sources; + } + + foreach ( $sources as $i => $source ) { + $url = $source['url']; + list( $width, $height ) = self::parse_dimensions_from_filename( $url ); + + if ( empty( $width ) ) { + $width = $image_meta['width']; + } + + if ( empty( $height ) ) { + $height = $image_meta['height']; + } + + $url = $this->strip_image_size_from_url( $source['url'] ); + if ( ! empty( $attachment_id ) ) { + $url = wp_get_attachment_url( $attachment_id ); + } + + $args = array(); + if ( 'w' === $source['descriptor'] ) { + if ( $height && ( $source['value'] == $width ) ) { + $args['width'] = $width; + $args['height'] = $height; + } else { + $args['width'] = $source['value']; + } + } + $sources[ $i ]['url'] = apply_filters( 'optml_content_url', $url, $args ); + } + + return $sources; + } + + /** + * Filters sizes attribute of the images. + * + * @param array $sizes An array of media query breakpoints. + * @param array $size Width and height of the image. + * + * @return mixed An array of media query breakpoints. + */ + public function filter_sizes_attr( $sizes, $size ) { + if ( ! doing_filter( 'the_content' ) ) { + return $sizes; + } + + $content_width = false; + if ( isset( $GLOBALS['content_width'] ) ) { + $content_width = $GLOBALS['content_width']; + } + + if ( ! $content_width ) { + $content_width = 1000; + } + + if ( is_array( $size ) && $size[0] < $content_width ) { + return $sizes; + } + + return sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $content_width ); + } + + /** + * This filter will replace all the images retrieved via "wp_get_image" type of functions. + * + * @param array $image The filtered value. + * @param int $attachment_id The related attachment id. + * @param array|string $size This could be the name of the thumbnail size or an array of custom dimensions. + * + * @return array + */ + public function filter_image_downsize( $image, $attachment_id, $size ) { + + $image_url = wp_get_attachment_url( $attachment_id ); + + if ( $image_url === false ) { + return $image; + } + + $image_meta = wp_get_attachment_metadata( $attachment_id ); + $image_args = self::image_sizes(); + + // default size + $sizes = array( + 'width' => isset( $image_meta['width'] ) ? intval( $image_meta['width'] ) : 'auto', + 'height' => isset( $image_meta['height'] ) ? intval( $image_meta['height'] ) : 'auto', + 'resize' => Optml_Image::RESIZE_FIT, + ); + + // in case there is a custom image size $size will be an array. + if ( is_array( $size ) ) { + + $sizes['width'] = ( $size[0] < $sizes['width'] ? $size[0] : $sizes['width'] ); + $sizes['height'] = ( $size[1] < $sizes['height'] ? $size[1] : $sizes['height'] ); + + } elseif ( 'full' !== $size && isset( $image_args[ $size ] ) ) { // overwrite if there a size + $sizes['width'] = $image_args[ $size ]['width'] < $sizes['width'] ? $image_args[ $size ]['width'] : $sizes['width']; + $sizes['height'] = $image_args[ $size ]['height'] < $sizes['height'] ? $image_args[ $size ]['height'] : $sizes['height']; + $sizes = array_merge( $sizes, $this->to_optml_crop( $image_args[ $size ]['crop'] ) ); + } + + $new_sizes = $this->to_optml_dimensions_bound( $sizes['width'], $sizes['height'], $this->max_width, $this->max_height ); + $image_url = $this->strip_image_size_from_url( $image_url ); + $new_url = apply_filters( 'optml_content_url', $image_url, $new_sizes ); + + if ( $new_url === $image_url ) { + return $image; + } + + return array( + $new_url, + $sizes['width'], + $sizes['height'], + false, + ); + } + + /** + * Class instance method. + * + * @codeCoverageIgnore + * @static + * @since 1.0.0 + * @access public + * @return Optml_Tag_Replacer + */ + public static function instance() { + if ( is_null( self::$instance ) ) { + self::$instance = new self(); + add_action( 'after_setup_theme', array( self::$instance, 'init' ) ); + } + + return self::$instance; + } + + /** + * Throw error on object clone + * + * The whole idea of the singleton design pattern is that there is a single + * object therefore, we don't want the object to be cloned. + * + * @codeCoverageIgnore + * @access public + * @since 1.0.0 + * @return void + */ + public function __clone() { + // Cloning instances of the class is forbidden. + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); + } + + /** + * Disable unserializing of the class + * + * @codeCoverageIgnore + * @access public + * @since 1.0.0 + * @return void + */ + public function __wakeup() { + // Unserializing instances of the class is forbidden. + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); + } + +} diff --git a/inc/traits/normalizer.php b/inc/traits/normalizer.php new file mode 100644 index 00000000..4b5f67ce --- /dev/null +++ b/inc/traits/normalizer.php @@ -0,0 +1,227 @@ + + */ +trait Optml_Normalizer { + + /** + * Normalize value to boolean. + * + * @param mixed $value Value to process. + * + * @return bool + */ + public function to_boolean( $value ) { + if ( in_array( $value, array( 'yes', 'enabled', 'true', '1' ) ) ) { + return true; + } + + if ( in_array( $value, array( 'no', 'disabled', 'false', '0' ) ) ) { + return false; + } + + return boolval( $value ); + } + + /** + * Normalize value to an integer within bounds. + * + * @param mixed $value Value to process. + * @param integer $min Lower bound. + * @param integer $max Upper bound. + * + * @return integer + */ + public function to_bound_integer( $value, $min, $max ) { + $integer = absint( $value ); + if ( $integer < $min ) { + $integer = $min; + } + if ( $integer > $max ) { + $integer = $max; + } + return $integer; + } + + /** + * Normalize value to positive integer. + * + * @param mixed $value Value to process. + * + * @return integer + */ + public function to_positive_integer( $value ) { + $integer = intval( $value ); + return ( $integer > 0 ) ? $integer : 0; + } + + /** + * Normalize value to map. + * + * @param mixed $value Value to process. + * @param array $map Associative list from witch to return. + * @param mixed $default Default. + * + * @return mixed + */ + public function to_map_values( $value, $map, $default ) { + if ( in_array( $value, $map ) ) { + return $value; + } + return $default; + } + + /** + * Normalize value to an accepted quality. + * + * @param mixed $value Value to process. + * + * @return mixed + */ + public function to_accepted_quality( $value ) { + if ( is_numeric( $value ) ) { + return intval( $value ); + } + $value = trim( $value ); + + $accepted_qualities = array( + 'eco' => 'eco', + 'auto' => 'auto', + 'high_c' => 55, + 'medium_c' => 75, + 'low_c' => 90, + ); + + if ( array_key_exists( $value, $accepted_qualities ) ) { + return $accepted_qualities[ $value ]; + } + + // Legacy values. + return 60; + } + + /** + * Normalize dimensions to bounds. + * + * @param mixed $width Width. + * @param mixed $height Height. + * @param integer $max_width Max width. + * @param integer $max_height Max height. + * + * @return array + */ + public function to_optml_dimensions_bound( $width, $height, $max_width, $max_height ) { + global $content_width; + + if ( + doing_filter( 'the_content' ) + && isset( $GLOBALS['content_width'] ) + && apply_filters( 'optml_imgcdn_allow_resize_images_from_content_width', false ) + ) { + $content_width = (int) $GLOBALS['content_width']; + + if ( $max_width > $content_width ) { + $max_width = $content_width; + } + } + + if ( $width > $max_width ) { + // we need to remember how much in percentage the width was rescaled and apply the same treatment to the height. + $percentWidth = ( 1 - $max_width / $width ) * 100; + $width = $max_width; + $height = round( $height * ( ( 100 - $percentWidth ) / 100 ), 0 ); + } + + // now for the height + if ( $height > $max_height ) { + $percentHeight = ( 1 - $max_height / $height ) * 100; + // if we reduce the height to max_height by $x percentage than we'll also reduce the width for the same amount. + $height = $max_height; + $width = round( $width * ( ( 100 - $percentHeight ) / 100 ), 0 ); + } + + return array( + 'width' => $width, + 'height' => $height, + ); + } + + /** + * Normalize arguments for crop. + * + * @param array $crop_args Crop arguments. + * + * @return array + */ + public function to_optml_crop( $crop_args = array() ) { + if ( $crop_args === false || ! is_array( $crop_args ) ) { + return array(); + } + + if ( $crop_args === true || empty( $crop_args ) || count( $crop_args ) != 2 ) { + return array( + 'resize' => Optml_Image::RESIZE_FILL, + 'gravity' => Optml_Image::GRAVITY_CENTER, + ); + } + + $allowed_gravities = array( + 'left' => Optml_Image::GRAVITY_WEST, + 'right' => Optml_Image::GRAVITY_EAST, + 'top' => Optml_Image::GRAVITY_NORTH, + 'bottom' => Optml_Image::GRAVITY_SOUTH, + 'left_top' => Optml_Image::GRAVITY_NORTH_WEST, + 'left_bottom' => Optml_Image::GRAVITY_SOUTH_WEST, + 'right_top' => Optml_Image::GRAVITY_NORTH_EAST, + 'right_bottom' => Optml_Image::GRAVITY_SOUTH_EAST, + 'center_top' => array( 0.5, 0 ), + 'center_bottom' => array( 0.5, 1 ), + 'left_center' => array( 0, 0.5 ), + 'right_center' => array( 1, 0.5 ), + ); + + $gravity = Optml_Image::GRAVITY_CENTER; + $key_search = strval( $crop_args[0] ) . strval( $crop_args[1] ); + if ( array_key_exists( $key_search, $allowed_gravities ) ) { + $gravity = $allowed_gravities[ $key_search ]; + } + + return array( + 'resize' => Optml_Image::RESIZE_FILL, + 'gravity' => $gravity, + ); + } + + /** + * Normalize arguments for watermark. + * + * @param array $watermark_args Watermark arguments. + * + * @return array + */ + public function to_optml_watermark( $watermark_args = array() ) { + $allowed_gravities = array( + 'left' => Optml_Image::GRAVITY_WEST, + 'right' => Optml_Image::GRAVITY_EAST, + 'top' => Optml_Image::GRAVITY_NORTH, + 'bottom' => Optml_Image::GRAVITY_SOUTH, + 'left_top' => Optml_Image::GRAVITY_NORTH_WEST, + 'left_bottom' => Optml_Image::GRAVITY_SOUTH_WEST, + 'right_top' => Optml_Image::GRAVITY_NORTH_EAST, + 'right_bottom' => Optml_Image::GRAVITY_SOUTH_EAST, + ); + $gravity = Optml_Image::GRAVITY_CENTER; + if ( isset( $watermark_args['position'] ) && array_key_exists( $watermark_args['position'], $allowed_gravities ) ) { + $gravity = $allowed_gravities[ $watermark_args['position'] ]; + } + + return array( + 'opacity' => 1, + 'position' => $gravity, + ); + } +} diff --git a/inc/traits/validator.php b/inc/traits/validator.php new file mode 100644 index 00000000..8366a7d1 --- /dev/null +++ b/inc/traits/validator.php @@ -0,0 +1,41 @@ + + */ +trait Optml_Validator { + + /** + * Check if the value is a valid numeric. + * + * @param mixed $value The value to check. + * + * @return bool + */ + public function is_valid_numeric( $value ) { + if ( isset( $value ) && ! empty( $value ) && is_numeric( $value ) ) { + return true; + } + return false; + } + + /** + * Check if the url has an accepted mime type extension. + * + * @param mixed $url The url to check. + * + * @return bool + */ + public function is_valid_mimetype_from_url( $url ) { + $type = wp_check_filetype( $url, Optml_Config::$extensions ); + + if ( ! isset( $type['ext'] ) || empty( $type['ext'] ) ) { + return false; + } + + return true; + } +} diff --git a/inc/url_replacer.php b/inc/url_replacer.php new file mode 100644 index 00000000..0d103846 --- /dev/null +++ b/inc/url_replacer.php @@ -0,0 +1,121 @@ + + */ +final class Optml_Url_Replacer extends Optml_App_Replacer { + + use Optml_Validator; + use Optml_Normalizer; + + /** + * Cached object instance. + * + * @var Optml_Url_Replacer + */ + protected static $instance = null; + + /** + * The initialize method. + */ + public function init() { + + add_filter( 'optml_replace_image', array( $this, 'build_image_url' ), 10, 2 ); + + if ( ! parent::init() ) { + return; // @codeCoverageIgnore + } + + add_filter( 'optml_content_url', array( $this, 'build_image_url' ), 1, 2 ); + } + + /** + * Returns a signed image url authorized to be used in our CDN. + * + * @param string $url The url which should be signed. + * @param array $args Dimension params; Supports `width` and `height`. + * + * @return string + */ + public function build_image_url( + $url, $args = array( + 'width' => 'auto', + 'height' => 'auto', + 'quality' => '', + ) + ) { + if ( apply_filters( 'optml_dont_replace_url', false, $url ) ) { + return $url; + } + if ( strpos( $url, Optml_Config::$service_url ) !== false ) { + return $url; + } + if ( ! $this->is_valid_mimetype_from_url( $url ) ) { + return $url; + } + + $compress_level = apply_filters( 'optml_image_quality', $this->settings->get_quality() ); + if ( isset( $args['quality'] ) && ! empty( $args['quality'] ) ) { + $compress_level = $args['quality']; + } + + $args['quality'] = $this->to_accepted_quality( $compress_level ); + + // this will authorize the image + if ( ! empty( $this->site_mappings ) ) { + $url = str_replace( array_keys( $this->site_mappings ), array_values( $this->site_mappings ), $url ); + } + + return ( new Optml_Image( $url, $args ) )->get_url( $this->is_allowed_site ); + } + + /** + * Class instance method. + * + * @codeCoverageIgnore + * @static + * @since 1.0.0 + * @access public + * @return Optml_Url_Replacer + */ + public static function instance() { + if ( is_null( self::$instance ) ) { + self::$instance = new self(); + add_action( 'after_setup_theme', array( self::$instance, 'init' ) ); + } + + return self::$instance; + } + + /** + * Throw error on object clone + * + * The whole idea of the singleton design pattern is that there is a single + * object therefore, we don't want the object to be cloned. + * + * @codeCoverageIgnore + * @access public + * @since 1.0.0 + * @return void + */ + public function __clone() { + // Cloning instances of the class is forbidden. + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); + } + + /** + * Disable unserializing of the class + * + * @codeCoverageIgnore + * @access public + * @since 1.0.0 + * @return void + */ + public function __wakeup() { + // Unserializing instances of the class is forbidden. + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); + } +} diff --git a/optimole-wp.php b/optimole-wp.php index b390fadc..2e0907fc 100644 --- a/optimole-wp.php +++ b/optimole-wp.php @@ -26,14 +26,14 @@ function optml_autoload( $class ) { if ( strpos( $class, $prefix ) !== 0 ) { return; } - $file = str_replace( $prefix . '_', '', $class ); - - $file = strtolower( $file ); - $file = dirname( __FILE__ ) . '/inc/' . $file . '.php'; - if ( file_exists( $file ) ) { - require $file; + foreach ( array( '/inc/', '/inc/traits/', '/inc/image_properties/' ) as $folder ) { + $file = str_replace( $prefix . '_', '', $class ); + $file = strtolower( $file ); + $file = dirname( __FILE__ ) . $folder . $file . '.php'; + if ( file_exists( $file ) ) { + require $file; + } } - } /** diff --git a/package.json b/package.json index 5384acab..aece1f9c 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,25 @@ "url": "https://github.com/Codeinwp/optimole-wp/issues" }, "scripts": { + "commitmsg": "commitlint -e .git/COMMIT_EDITMSG", "dev": "cross-env WEBPACK_ENV=dev webpack --progress --colors --watch", - "build": "cross-env WEBPACK_ENV=production webpack" + "build": "cross-env WEBPACK_ENV=production webpack", + "report": ".\"/vendor/bin/phpmetrics\" --report-html=code_quality . || true", + "phpcbf": ".\"/vendor/bin/phpcbf\" || true", + "phpcs": ".\"/vendor/bin/phpcs\"", + "phpmd": ".\"/vendor/phpmd/phpmd/src/bin/phpmd\" inc text phpmd.xml", + "phpunit": ".\"/vendor/phpunit/phpunit/phpunit\" --configuration phpunit.xml --coverage-text" }, + "pre-commit": [ + "phpcbf", + "phpcs", + "phpunit" + ], "devDependencies": { + "@commitlint/cli": "^7.0.0", + "@commitlint/config-conventional": "^7.0.1", + "pre-commit": "^1.2.2", + "husky": "^0.14.3", "babel-core": "^6.26.0", "babel-eslint": "^8.0.1", "babel-loader": "^7.1.2", @@ -64,6 +79,7 @@ "vue-js-toggle-button": "^1.2.3", "vue-resize": "^0.4.4", "vue-resource": "^1.3.4", + "vue-upload-component": "^2.8.16", "vuex": "^2.4.0", "y": "^0.3.2" } diff --git a/phpcs.xml b/phpcs.xml index 3e699fb6..0777a53c 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -13,6 +13,7 @@ dist dist/* artifact/* + assets/* companion-legacy/* diff --git a/phpmd.xml b/phpmd.xml new file mode 100644 index 00000000..a0c77201 --- /dev/null +++ b/phpmd.xml @@ -0,0 +1,48 @@ + + + PHPMD Ruleset for a WordPress + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index 42de73e4..4254e61e 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -6,6 +6,18 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" > + + + + + ./inc/ + + + + + ./tests/test-generic.php diff --git a/tests/test-lazyload.php b/tests/test-lazyload.php index 81760ac6..dd3cd718 100644 --- a/tests/test-lazyload.php +++ b/tests/test-lazyload.php @@ -25,23 +25,26 @@ public function setUp() { ] ); $settings->update( 'lazyload', 'enabled' ); - Optml_Replacer::instance()->init(); + Optml_Url_Replacer::instance()->init(); + Optml_Tag_Replacer::instance()->init(); + Optml_Lazyload_Replacer::instance()->init(); + Optml_Manager::instance()->init(); } public function test_lazy_load() { - $replaced_content = Optml_Replacer::instance()->filter_the_content( Test_Replacer::IMG_TAGS ); + $replaced_content = Optml_Manager::instance()->process_images_from_content( Test_Replacer::IMG_TAGS ); $this->assertContains( 'i.optimole.com', $replaced_content ); $this->assertContains( 'data-opt-src', $replaced_content ); $this->assertNotContains( 'http://example.org', $replaced_content ); - $replaced_content = Optml_Replacer::instance()->replace_urls( Test_Replacer::IMG_TAGS . Test_Replacer::IMG_URLS ); + $replaced_content = Optml_Manager::instance()->process_images_from_content( Test_Replacer::IMG_TAGS . Test_Replacer::IMG_URLS ); $this->assertContains( 'i.optimole.com', $replaced_content ); $this->assertContains( 'data-opt-src', $replaced_content ); - $this->assertNotContains( 'http://example.org', $replaced_content ); + $this->assertContains( 'http://example.org', $replaced_content ); // Does not touch other URL's - $replaced_content = Optml_Replacer::instance()->replace_urls( Test_Replacer::IMG_TAGS_PNG ); + $replaced_content = Optml_Manager::instance()->process_images_from_content( Test_Replacer::IMG_TAGS_PNG ); $this->assertContains( 'i.optimole.com', $replaced_content ); $this->assertContains( 'data-opt-src', $replaced_content ); @@ -53,8 +56,7 @@ public function test_lazy_load_off() { define( 'OPTML_DISABLE_PNG_LAZYLOAD', true ); - $replaced_content = Optml_Replacer::instance()->filter_the_content( Test_Replacer::IMG_TAGS_PNG . Test_Replacer::IMG_TAGS ); - + $replaced_content = Optml_Manager::instance()->process_images_from_content( Test_Replacer::IMG_TAGS_PNG . Test_Replacer::IMG_TAGS ); $this->assertContains( 'i.optimole.com', $replaced_content ); $this->assertContains( 'data-opt-src', $replaced_content ); $this->assertEquals( 1, substr_count( $replaced_content, 'data-opt-src' ) ); @@ -62,24 +64,24 @@ public function test_lazy_load_off() { } public function test_lazy_dont_lazy_load_headers() { - $replaced_content = Optml_Replacer::instance()->replace_urls( self::HTML_TAGS_HEADER ); + $replaced_content = Optml_Manager::instance()->process_images_from_content( self::HTML_TAGS_HEADER ); $this->assertContains( 'data-opt-src', $replaced_content ); $this->assertContains( 'i.optimole.com', $replaced_content ); - $this->assertNotContains( 'http://example.org', $replaced_content ); + $this->assertContains( 'http://example.org', $replaced_content ); $this->assertEquals( 1, substr_count( $replaced_content, 'data-opt-src' ) ); } public function test_lazy_load_just_first_header() { - $replaced_content = Optml_Replacer::instance()->replace_urls( self::HTML_TAGS_HEADER_MULTIPLE ); + $replaced_content = Optml_Manager::instance()->process_images_from_content( self::HTML_TAGS_HEADER_MULTIPLE ); $this->assertContains( 'data-opt-src', $replaced_content ); $this->assertContains( 'i.optimole.com', $replaced_content ); - $this->assertNotContains( 'http://example.org', $replaced_content ); + $this->assertContains( 'http://example.org', $replaced_content ); $this->assertEquals( 3, substr_count( $replaced_content, 'data-opt-src' ) ); } public function test_check_no_script() { - $replaced_content = Optml_Replacer::instance()->replace_urls( self::HTML_TAGS_HEADER ); + $replaced_content = Optml_Manager::instance()->process_images_from_content( self::HTML_TAGS_HEADER ); $this->assertContains( '