├── index.php ├── includes ├── index.php ├── admin │ ├── index.php │ ├── views │ │ └── publish_now.php │ ├── class-rop-services-factory.php │ ├── class-rop-shortner-factory.php │ ├── shortners │ │ ├── class-rop-owly-shortner.php │ │ ├── class-rop-isgd-shortner.php │ │ ├── class-rop-firebase-shortner.php │ │ ├── class-rop-rebrandly-shortner.php │ │ ├── class-rop-rvivly-shortner.php │ │ └── class-rop-bitly-shortner.php │ ├── models │ │ ├── class-rop-shortners-model.php │ │ └── class-rop-post-format-model.php │ ├── notices │ │ ├── class-rop-admin-notices.php │ │ └── class-rop-admin-notices-helpers.php │ ├── helpers │ │ ├── class-rop-exception-handler.php │ │ └── class-rop-log-handler.php │ └── abstract │ │ └── class-rop-model-abstract.php ├── class-rop-deactivator.php └── class-rop-activator.php ├── .stylelintrc ├── .jshintignore ├── assets ├── img │ ├── to_pro.png │ ├── logo_rop.png │ ├── black-friday.jpg │ ├── to_business.png │ ├── accounts_icon.jpg │ ├── revivenetwork.jpg │ ├── twitter_post_img.jpg │ ├── video_placeholder.jpg │ ├── revive-network-logo.png │ └── x-twitter.svg ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 ├── js │ └── react │ │ └── build │ │ └── index.asset.php └── css │ ├── admin-notices.css │ └── rop.css ├── docs └── images │ ├── no-posts.png │ ├── queue_stack.png │ ├── connect-network.png │ ├── sharing_workflow.png │ └── first_start_workflow.png ├── languages ├── tweet-old-post.mo ├── tweet-old-post-de_DE.mo ├── tweet-old-post-es_ES.mo ├── tweet-old-post-fr_FR.mo ├── tweet-old-post-pt_BR.mo ├── tweet-old-post-pt_PT.mo └── tweet-old-post-sk_SK.mo ├── .wordpress-org ├── icon-128x128.gif ├── icon-256x256.gif ├── screenshot-1.png ├── screenshot-2.png ├── screenshot-3.png ├── screenshot-4.png ├── screenshot-5.png ├── screenshot-6.png ├── screenshot-7.png ├── screenshot-8.png ├── banner-1544x500.jpg └── banner-772x250.jpg ├── .wp-env.json ├── webpack.sharing.config.js ├── cron-system ├── index.php ├── composer.json ├── rest-endpoints │ ├── class-rop-system-base.php │ ├── class-rop-debug-ping.php │ ├── class-rop-registration-check.php │ └── class-rop-ping-system.php └── class-rop-cron-core.php ├── .github ├── labeler.yml ├── dependabot.yml ├── workflows │ ├── issue-labeler.yml │ ├── sync-wporg-assets.yml │ ├── create-tagged-release.yml │ ├── test-js.yml │ ├── sync-branches.yml │ ├── deploy-to-wporg.yml │ ├── pr-checklist.yml │ ├── new-issues.yml │ ├── e2e.yml │ ├── test-php.yml │ └── build-dev-artifacts.yml ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE │ ├── Feature_request.yml │ └── Bug_report.yml ├── .gitignore ├── vue └── src │ ├── rop_publish_now.js │ ├── rop_exclude_posts.js │ ├── rop_main.js │ └── vue-elements │ ├── reusables │ ├── empty-active-accounts.vue │ ├── account-modal.vue │ ├── toast.vue │ ├── button-checkbox.vue │ ├── add-account-tile.vue │ ├── countdown.vue │ ├── webhook-headers.vue │ ├── vue-spinner.vue │ ├── preload_three_dots.vue │ ├── webhook-account-modal.vue │ ├── status-box.vue │ └── counter-input.vue │ ├── upsell-sidebar.vue │ └── queue-tab-panel.vue ├── .jshintrc ├── phpstan.neon ├── .distignore ├── bin └── dist.sh ├── tests ├── e2e │ ├── playwright.config.js │ ├── config │ │ ├── global-setup.js │ │ └── flasky-tests-reporter.js │ ├── specs │ │ └── dashboard │ │ │ ├── accounts.spec.js │ │ │ ├── general-settings.spec.js │ │ │ ├── post-format.spec.js │ │ │ └── publish-now.spec.js │ ├── README.md │ └── utils │ │ └── index.js ├── bootstrap.php ├── test-logger.php ├── php │ └── static-analysis-stubs │ │ └── symbols.php ├── test-selector.php ├── test-scheduler.php ├── test-content.php ├── helpers │ └── class-setup-accounts.php └── test-plugin.php ├── docker-compose.yml ├── Gruntfile.js ├── .releaserc.yml ├── phpunit.xml ├── src ├── variations │ └── Upsell.js ├── manual │ └── index.js ├── instant │ ├── Reshare.js │ ├── ListItem.js │ ├── index.js │ └── PostUpdate.js └── index.js ├── .eslintrc.js ├── webpack.config.js ├── composer.json ├── package.json └── phpcs.xml /index.php: -------------------------------------------------------------------------------- 1 | 4 |
5 | 6 |
7 | -------------------------------------------------------------------------------- /.wp-env.json: -------------------------------------------------------------------------------- 1 | { 2 | "core": null, 3 | "plugins": [ "." ], 4 | "env": { 5 | "tests": { 6 | "config": { 7 | "TI_E2E_TESTING": true 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /webpack.sharing.config.js: -------------------------------------------------------------------------------- 1 | const defaultConfig = require( '@wordpress/scripts/config/webpack.config' ); 2 | 3 | module.exports = [ 4 | { 5 | ...defaultConfig 6 | } 7 | ]; 8 | -------------------------------------------------------------------------------- /cron-system/index.php: -------------------------------------------------------------------------------- 1 | array('lodash', 'react', 'wp-api-fetch', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-date', 'wp-editor', 'wp-element', 'wp-notices', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => '3000d00121986d2a4045'); 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | # Maintain dependencies for Composer 5 | - package-ecosystem: "composer" 6 | directory: "/" 7 | ignore: 8 | - dependency-name: "facebook/graph-sdk*" 9 | target-branch: "development" 10 | ignore: 11 | - dependency-name: "facebook/graph-sdk*" 12 | schedule: 13 | interval: "weekly" 14 | -------------------------------------------------------------------------------- /.github/workflows/issue-labeler.yml: -------------------------------------------------------------------------------- 1 | name: "Issue Labeler" 2 | on: 3 | issues: 4 | types: [opened] 5 | 6 | jobs: 7 | triage: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: github/issue-labeler@master 11 | with: 12 | repo-token: "${{ secrets.BOT_TOKEN }}" 13 | enable-versioned-regex: 0 14 | configuration-path: .github/labeler.yml -------------------------------------------------------------------------------- /assets/img/x-twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue/src/rop_publish_now.js: -------------------------------------------------------------------------------- 1 | // jshint ignore: start 2 | /* eslint no-unused-vars: 0 */ 3 | 4 | import Vue from 'vue' 5 | 6 | import store from './models/rop_store.js' 7 | import PublishNow from './vue-elements/pro/publish-now.vue' 8 | 9 | window.addEventListener( 'load', function () { 10 | var RopPublishNow = new Vue( { 11 | el: '#rop_publish_now', 12 | store, 13 | components: { 14 | PublishNow 15 | }, 16 | created() { 17 | }, 18 | } ); 19 | } ); -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "boss": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "eqnull": true, 6 | "expr": true, 7 | "immed": true, 8 | "noarg": true, 9 | "nonbsp": true, 10 | "onevar": true, 11 | "quotmark": "single", 12 | "trailing": true, 13 | "undef": true, 14 | "unused": true, 15 | "browser": true, 16 | "globals": { 17 | "_": false, 18 | "Backbone": false, 19 | "jQuery": false, 20 | "JSON": false, 21 | "wp": false 22 | } 23 | } -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 6 3 | paths: 4 | - %currentWorkingDirectory%/tweet-old-post.php 5 | - %currentWorkingDirectory%/class-rop-autoloader.php 6 | - %currentWorkingDirectory%/includes 7 | - %currentWorkingDirectory%/cron-system 8 | bootstrapFiles: 9 | - %currentWorkingDirectory%/tests/php/static-analysis-stubs/symbols.php 10 | includes: 11 | - %currentWorkingDirectory%/vendor/szepeviktor/phpstan-wordpress/extension.neon 12 | - %currentWorkingDirectory%/phpstan-baseline.neon -------------------------------------------------------------------------------- /vue/src/rop_exclude_posts.js: -------------------------------------------------------------------------------- 1 | // jshint ignore: start 2 | /* eslint no-unused-vars: 0 */ 3 | /* exported RopExcludePosts */ 4 | 5 | import Vue from 'vue' 6 | 7 | import store from './models/rop_store.js' 8 | import ExcludePostsPage from './vue-elements/exclude-posts-page.vue' 9 | 10 | 11 | window.addEventListener( 'load', function () { 12 | var RopExcludePosts = new Vue( { 13 | el: '#rop_content_filters', 14 | store, 15 | components: { 16 | ExcludePostsPage 17 | }, 18 | created() { 19 | }, 20 | } ); 21 | } ); 22 | -------------------------------------------------------------------------------- /.distignore: -------------------------------------------------------------------------------- 1 | .git 2 | .distignore 3 | .gitignore 4 | .travis.yml 5 | .jshintrc 6 | .jshintignore 7 | .eslintrc.js 8 | Gruntfile.js 9 | grunt 10 | phpcs.xml 11 | node_modules 12 | logs 13 | package.json 14 | bin 15 | tests 16 | phpunit.xml 17 | npm-debug.log 18 | dist 19 | docs 20 | artifact 21 | composer.json 22 | composer.lock 23 | key.enc 24 | package-lock.json 25 | vue 26 | /src 27 | .github 28 | .vscode 29 | .wordpress-org 30 | .releaserc.yml 31 | .stylelintrc 32 | webpack.config.js 33 | docker-compose.yml 34 | phpstan.neon 35 | phpstan-baseline.neon -------------------------------------------------------------------------------- /bin/dist.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | BUILD_VERSION=$(node -pe "require('./package.json').version") 4 | export BUILD_VERSION 5 | BUILD_NAME=$(node -pe "require('./package.json').name") 6 | export BUILD_NAME 7 | 8 | echo "BUILD_NAME=$BUILD_NAME" >> $GITHUB_ENV 9 | echo "BUILD_VERSION=$BUILD_VERSION" >> $GITHUB_ENV 10 | 11 | if [ ! -d "dist" ]; then 12 | mkdir "dist" 13 | fi 14 | 15 | if [ ! -d "artifact" ]; then 16 | mkdir "artifact" 17 | fi 18 | 19 | rsync -rc --exclude-from ".distignore" "./" "dist/$BUILD_NAME" 20 | 21 | cd dist 22 | zip -r "../artifact/$BUILD_NAME" "./$BUILD_NAME/" 23 | cd - -------------------------------------------------------------------------------- /.github/workflows/sync-wporg-assets.yml: -------------------------------------------------------------------------------- 1 | name: Sync Plugin Assets With WordPress.org 2 | 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | paths: 9 | - 'readme.txt' 10 | - '.wordpress-org/**' 11 | jobs: 12 | run: 13 | runs-on: ubuntu-latest 14 | if: "! contains(github.event.head_commit.message, 'chore(release)')" 15 | name: Push Assets to WP.org 16 | steps: 17 | - uses: actions/checkout@master 18 | - name: WordPress.org plugin asset/readme update 19 | uses: selul/action-wordpress-plugin-asset-update@develop 20 | env: 21 | SVN_PASSWORD: ${{ secrets.SVN_THEMEISLE_PASSWORD }} 22 | SVN_USERNAME: ${{ secrets.SVN_THEMEISLE_USERNAME }} -------------------------------------------------------------------------------- /tests/e2e/playwright.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import os from 'os'; 5 | import { fileURLToPath } from 'url'; 6 | import { defineConfig, devices } from '@playwright/test'; 7 | 8 | /** 9 | * WordPress dependencies 10 | */ 11 | const baseConfig = require( '@wordpress/scripts/config/playwright.config' ); 12 | 13 | const config = defineConfig( { 14 | ...baseConfig, 15 | reporter: 'list', 16 | workers: 1, 17 | globalSetup: fileURLToPath( 18 | new URL( './config/global-setup.js', 'file:' + __filename ).href 19 | ), 20 | projects: [ 21 | { 22 | name: 'chromium', 23 | use: { ...devices[ 'Desktop Chrome' ] }, 24 | grepInvert: /-chromium/, 25 | }, 26 | ], 27 | } ); 28 | 29 | export default config; -------------------------------------------------------------------------------- /.github/workflows/create-tagged-release.yml: -------------------------------------------------------------------------------- 1 | name: Create Tag & Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | create_tag: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | with: 13 | persist-credentials: false 14 | - name: Release New Version 15 | id: release 16 | run: | 17 | npm ci 18 | npm run release 19 | env: 20 | CI: true 21 | GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} 22 | GIT_AUTHOR_NAME: themeisle[bot] 23 | GIT_AUTHOR_EMAIL: ${{ secrets.BOT_EMAIL }} 24 | GIT_COMMITTER_NAME: themeisle[bot] 25 | GIT_COMMITTER_EMAIL: ${{ secrets.BOT_EMAIL }} -------------------------------------------------------------------------------- /vue/src/rop_main.js: -------------------------------------------------------------------------------- 1 | // jshint ignore: start 2 | /* eslint no-unused-vars: 0 */ 3 | /* exported RopApp */ 4 | 5 | import Vue from 'vue' 6 | 7 | import store from './models/rop_store.js' 8 | import MainPagePanel from './vue-elements/main-page-panel.vue' 9 | 10 | window.addEventListener( 'load', function () { 11 | var RopApp = new Vue( { 12 | el: '#rop_core', 13 | store, 14 | components: { 15 | MainPagePanel 16 | }, 17 | created() { 18 | store.dispatch( 'fetchAJAX', {req: 'manage_cron', data: {action: 'status'}} ) 19 | store.dispatch( 'fetchAJAXPromise', {req: 'get_available_services'} ) 20 | store.dispatch( 'fetchAJAXPromise', {req: 'get_authenticated_services'} ) 21 | store.dispatch( 'fetchAJAXPromise', {req: 'get_active_accounts'} ) 22 | }, 23 | } ); 24 | } ); -------------------------------------------------------------------------------- /cron-system/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rop-stand-alone-cron-system/rop-cron", 3 | "description": "Replaces the user cron system with the server true cron.", 4 | "minimum-stability": "dev", 5 | "license": "GPL-2.0+", 6 | "authors": [ 7 | { 8 | "name": "ThemeIsle Team", 9 | "email": "friends@themeisle.com.com", 10 | "homepage": "https://themeisle.com" 11 | } 12 | ], 13 | "autoload": { 14 | "classmap": [ 15 | "includes/class-rop-curl-methods.php", 16 | "includes/class-rop-helpers.php", 17 | "includes/class-debug-page.php", 18 | "rest-endpoints/class-rop-ping-system.php", 19 | "rest-endpoints/class-rop-system-base.php", 20 | "rest-endpoints/class-rop-debug-ping.php", 21 | "rest-endpoints/class-rop-registration-check.php", 22 | "class-rop-cron-core.php" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/test-js.yml: -------------------------------------------------------------------------------- 1 | name: Test JS 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, ready_for_review] 6 | 7 | jobs: 8 | run: 9 | runs-on: ubuntu-latest 10 | if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name 11 | strategy: 12 | matrix: 13 | node-version: [18.x] 14 | name: JS Lint 15 | steps: 16 | - uses: actions/checkout@master 17 | with: 18 | persist-credentials: false 19 | - name: Build files using ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: Lint js files 24 | run: | 25 | npm ci 26 | npm run lint 27 | env: 28 | CI: true 29 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | mysql: 5 | image: mysql:5.7 6 | volumes: 7 | - ~/rop/db_data:/var/lib/mysql 8 | restart: always 9 | environment: 10 | MYSQL_ROOT_PASSWORD: wordpress 11 | MYSQL_DATABASE: wordpress 12 | MYSQL_USER: wordpress 13 | MYSQL_PASSWORD: wordpress 14 | wordpress: 15 | depends_on: 16 | - mysql 17 | image: hardeepasrani/pirate-brewery 18 | ports: 19 | - 8888:80 20 | networks: 21 | - default 22 | volumes: 23 | - ~/wpcore:/var/www/html/ 24 | - .:/var/www/html/wp-content/plugins/tweet-old-post/ 25 | restart: always 26 | environment: 27 | WORDPRESS_DB_NAME: wordpress 28 | WORDPRESS_DB_USER: wordpress 29 | WORDPRESS_DB_PASSWORD: wordpress 30 | WORDPRESS_DB_ROOT_PASSWORD: wordpress 31 | WORDPRESS_DEBUG: 1 32 | -------------------------------------------------------------------------------- /.github/workflows/sync-branches.yml: -------------------------------------------------------------------------------- 1 | name: Sync branches 2 | on: 3 | push: 4 | branches: 5 | - 'master' 6 | - 'development' 7 | jobs: 8 | sync-branch: 9 | runs-on: ubuntu-latest 10 | if: ${{ github.repository_owner == 'Codeinwp' }} #Disable on forks 11 | steps: 12 | - uses: actions/checkout@master 13 | - name: Retrieve branch name 14 | id: retrieve-branch-name 15 | run: echo "::set-output name=branch_name::$(REF=${GITHUB_HEAD_REF:-$GITHUB_REF} && echo ${REF#refs/heads/} | sed 's/\//-/g')" 16 | - name: Merge master -> development 17 | if: ${{ steps.retrieve-branch-name.outputs.branch_name == 'master' }} 18 | uses: Codeinwp/merge-branch@master 19 | with: 20 | type: now 21 | from_branch: master 22 | target_branch: development 23 | github_token: ${{ secrets.BOT_TOKEN }} -------------------------------------------------------------------------------- /includes/class-rop-deactivator.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class Rop_Deactivator { 24 | 25 | /** 26 | * Short Description. (use period) 27 | * 28 | * Long Description. 29 | * 30 | * @since 8.0.0 31 | */ 32 | public static function deactivate() { 33 | /** 34 | * Stop posting action. 35 | */ 36 | $cron_helper = new Rop_Cron_Helper(); 37 | $cron_helper->remove_cron(); 38 | 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /vue/src/vue-elements/reusables/empty-active-accounts.vue: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /.github/workflows/deploy-to-wporg.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | jobs: 7 | tag: 8 | name: New version 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Get The Version 13 | id: get_version 14 | run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/} 15 | - name: Setup PHP 16 | uses: shivammathur/setup-php@v2 17 | with: 18 | php-version: '7.4' 19 | - name: Run Composer Install 20 | run: | 21 | npm ci 22 | npm run build 23 | npm run sharing 24 | composer install --no-dev --prefer-dist --no-progress --no-suggest 25 | - name: WordPress Plugin Deploy 26 | uses: 10up/action-wordpress-plugin-deploy@master 27 | env: 28 | SVN_PASSWORD: ${{ secrets.SVN_THEMEISLE_PASSWORD }} 29 | SVN_USERNAME: ${{ secrets.SVN_THEMEISLE_USERNAME }} 30 | -------------------------------------------------------------------------------- /.github/workflows/pr-checklist.yml: -------------------------------------------------------------------------------- 1 | name: Add labels to pull request based on checklist 2 | 3 | on: 4 | pull_request: 5 | types: [ opened, edited, synchronize, labeled, unlabeled ] 6 | branches-ignore: 7 | - 'master' 8 | - 'update_dependencies' 9 | 10 | permissions: write-all 11 | 12 | jobs: 13 | add-labels: 14 | runs-on: ubuntu-latest 15 | name: Label PR based on checklist 16 | if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name && github.actor != 'dependabot[bot]' 17 | steps: 18 | - name: Check if checklist items are completed 19 | uses: Codeinwp/gha-pr-check-helper@master 20 | with: 21 | token: ${{ secrets.BOT_TOKEN }} 22 | requireChecklist: true 23 | onlyCheckBody: true 24 | completedLabel: "pr-checklist-complete" 25 | incompleteLabel: "pr-checklist-incomplete" 26 | skipLabel: "pr-checklist-skip" 27 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Grunt File 3 | * 4 | * @package tweet-old-post 5 | */ 6 | module.exports = function (grunt) { 7 | grunt.initConfig( 8 | { 9 | wp_readme_to_markdown: { 10 | files: { 11 | 'readme.md': 'readme.txt' 12 | }, 13 | }, 14 | version: { 15 | project: { 16 | src: [ 17 | 'package.json' 18 | ] 19 | }, 20 | style: { 21 | options: { 22 | prefix: 'Version\\:\.*\\s' 23 | }, 24 | src: [ 25 | 'tweet-old-post.php', 26 | ] 27 | }, 28 | class: { 29 | options: { 30 | prefix: '\\$this->version\.*\\s=\.*\\s\'' 31 | }, 32 | src: [ 33 | 'includes/class-rop.php', 34 | ] 35 | }, 36 | constants: { 37 | options: { 38 | prefix: 'ROP_LITE_VERSION\'\,\\s+\'' 39 | }, 40 | src: [ 41 | 'tweet-old-post.php', 42 | ] 43 | } 44 | } 45 | } 46 | ); 47 | grunt.loadNpmTasks( 'grunt-version' ); 48 | grunt.loadNpmTasks( 'grunt-wp-readme-to-markdown' ); 49 | }; 50 | -------------------------------------------------------------------------------- /.releaserc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | branches: master 3 | plugins: 4 | - - "@semantic-release/commit-analyzer" 5 | - preset: simple-preset 6 | releaseRules: conventional-changelog-simple-preset/release-rules 7 | - - "@semantic-release/changelog" 8 | - changelogFile: CHANGELOG.md 9 | - - "@semantic-release/release-notes-generator" 10 | - preset: simple-preset 11 | - - "@semantic-release/exec" 12 | - prepareCmd: "replace-in-file \"== Changelog ==\" \"== Changelog ==\n\n${nextRelease.notes}\" readme.txt" 13 | - - "@semantic-release/exec" 14 | - prepareCmd: grunt version::${nextRelease.version} && grunt wp_readme_to_markdown 15 | - - "@semantic-release/git" 16 | - assets: 17 | - CHANGELOG.md 18 | - readme.md 19 | - readme.txt 20 | - tweet-old-post.php 21 | - includes/class-rop.php 22 | - package-lock.json 23 | - package.json 24 | message: "chore(release): ${nextRelease.version} \n\n${nextRelease.notes}" 25 | - - "@semantic-release/github" -------------------------------------------------------------------------------- /tests/e2e/config/global-setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { request } from '@playwright/test'; 5 | 6 | /** 7 | * WordPress dependencies 8 | */ 9 | import { RequestUtils } from '@wordpress/e2e-test-utils-playwright'; 10 | 11 | async function globalSetup( config ) { 12 | const { storageState, baseURL } = config.projects[ 0 ].use; 13 | const storageStatePath = 14 | typeof storageState === 'string' ? storageState : undefined; 15 | 16 | const requestContext = await request.newContext( { 17 | baseURL, 18 | } ); 19 | 20 | const requestUtils = new RequestUtils( requestContext, { 21 | storageStatePath, 22 | } ); 23 | 24 | console.log(`----------> BaseURL: ${baseURL}`); 25 | 26 | // Authenticate and save the storageState to disk. 27 | await requestUtils.setupRest(); 28 | 29 | // Reset the test environment before running the tests. 30 | await Promise.all( [ 31 | requestUtils.deleteAllPosts(), 32 | requestUtils.deleteAllBlocks(), 33 | requestUtils.resetPreferences(), 34 | ] ); 35 | 36 | await requestContext.dispose(); 37 | } 38 | 39 | export default globalSetup; -------------------------------------------------------------------------------- /includes/admin/class-rop-services-factory.php: -------------------------------------------------------------------------------- 1 | { 7 | 8 | test.beforeEach( async ( { page, admin } ) => { 9 | await admin.visitAdminPage( '/admin.php?page=TweetOldPost' ); 10 | 11 | // Wait for the accounts tab to load. 12 | await page.waitForSelector( '.tab-view[type="accounts"]' ); 13 | } ); 14 | 15 | test( 'Social Accounts', async ( { admin, page }) => { 16 | await expect( page.getByRole('button', { name: 'Facebook' }) ).toBeVisible(); 17 | await expect( page.getByRole('button', { name: 'Twitter' }) ).toBeVisible(); 18 | await expect( page.getByRole('button', { name: 'LinkedIn' }) ).toBeVisible(); 19 | await expect( page.getByRole('button', { name: 'Tumblr' }) ).toBeVisible(); 20 | await expect( page.getByRole('button', { name: 'GMB' }) ).toBeVisible(); 21 | await expect( page.getByRole('button', { name: 'Vk' }) ).toBeVisible(); 22 | await expect( page.getByRole('button', { name: 'Webhook' }) ).toBeVisible(); 23 | } ); 24 | } ); -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | 4 | ### Will affect the visual aspect of the product 5 | 6 | YES/NO 7 | 8 | ### Screenshots 9 | 10 | ### Test instructions 11 | 12 | 13 | - 14 | - 15 | 16 | ## Check before Pull Request is ready: 17 | 18 | * [ ] I have [written a test](CONTRIBUTING.md#writing-an-acceptance-test) and included it in this PR 19 | * [ ] I have [run all tests](CONTRIBUTING.md#run-tests) and they pass 20 | * [ ] The code passes when [running the PHP CodeSniffer](CONTRIBUTING.md#run-php-codesniffer) 21 | * [ ] Code meets [WordPress Coding Standards](CONTRIBUTING.md#coding-standards) for PHP, HTML, CSS and JS 22 | * [ ] [Security and Sanitization](CONTRIBUTING.md#security-and-sanitization) requirements have been followed 23 | * [ ] I have assigned a reviewer or two to review this PR (if you're not sure who to assign, we can do this step for you) 24 | 25 | 26 | Closes #. 27 | 28 | -------------------------------------------------------------------------------- /includes/admin/class-rop-shortner-factory.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | ./tests/test-plugin.php 12 | 13 | 14 | ./tests/test-logger.php 15 | 16 | 17 | ./tests/test-accounts.php 18 | 19 | 20 | ./tests/test-post-format.php 21 | 22 | 23 | ./tests/test-scheduler.php 24 | 25 | 26 | ./tests/test-content.php 27 | 28 | 29 | ./tests/test-queue.php 30 | 31 | 32 | ./tests/test-selector.php 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | set_role( 'administrator' ); 33 | wp_update_user( array( 'ID' => 1, 'first_name' => 'Admin', 'last_name' => 'User' ) ); -------------------------------------------------------------------------------- /tests/e2e/specs/dashboard/general-settings.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { test, expect } from '@wordpress/e2e-test-utils-playwright'; 5 | 6 | test.describe( 'Accounts', () => { 7 | 8 | test.beforeEach( async ( { page, admin } ) => { 9 | await admin.visitAdminPage( '/admin.php?page=TweetOldPost' ); 10 | 11 | // Wait for the accounts tab to load. 12 | await page.waitForSelector( '.tab-view[type="accounts"]' ); 13 | 14 | await page.getByText('General Settings').click(); 15 | } ); 16 | 17 | test( 'Can change inputs', async ( { page }) => { 18 | 19 | /** 20 | * Check Minimum Interval Between Shares input field. 21 | */ 22 | await page.waitForSelector( '#default_interval' ); 23 | await page.fill( '#default_interval', '5' ); 24 | 25 | await expect( page.getByText('Minimum Interval Between') ).toBeVisible(); 26 | await expect( page.$eval('#default_interval', el => el.value) ).resolves.toBe('5'); 27 | 28 | /** 29 | * Check Share More Than Once toggle. 30 | */ 31 | await page.locator('#share_more_than_once').first().uncheck(); 32 | 33 | await expect( page.getByText('Share More Than Once?') ).toBeVisible(); 34 | await expect( page.locator('#share_more_than_once').first().isChecked() ).resolves.toBe(false); 35 | } ); 36 | } ); -------------------------------------------------------------------------------- /vue/src/vue-elements/reusables/account-modal.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 54 | 55 | -------------------------------------------------------------------------------- /.github/workflows/new-issues.yml: -------------------------------------------------------------------------------- 1 | name: Add to project 2 | 3 | on: 4 | issues: 5 | types: [opened,transferred] 6 | 7 | jobs: 8 | add-to-project: 9 | name: Add issue to project 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/add-to-project@main 13 | id: add_project 14 | with: 15 | project-url: ${{ secrets.PROJECT_PLANNING }} 16 | github-token: ${{ secrets.BOT_TOKEN_PROJECT }} 17 | - name: Set Team 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.BOT_TOKEN_PROJECT }} 20 | run: | 21 | gh api graphql -f query=' 22 | mutation( 23 | $project: ID! 24 | $item: ID! 25 | $status_field: ID! 26 | $status_value: String! 27 | ) { 28 | updateProjectV2ItemFieldValue( 29 | input: { 30 | projectId: $project 31 | itemId: $item 32 | fieldId: $status_field 33 | value: { 34 | singleSelectOptionId: $status_value 35 | } 36 | } 37 | ) { 38 | projectV2Item { 39 | id 40 | } 41 | } 42 | }' -f project=${{ secrets.PROJECT_PLANNING_ID }} -f item=${{ steps.add_project.outputs.itemId }} -f status_field=${{ secrets.PLANNING_TEAM_FIELD_ID }} -f status_value=ed46988b --silent -------------------------------------------------------------------------------- /tests/test-logger.php: -------------------------------------------------------------------------------- 1 | assertEmpty( $log->get_logs(), 'By default logs should be empty' ); 28 | 29 | $log->alert_error( 'test alert' ); 30 | $this->assertNotEmpty( $log->get_logs(), 'Alert error is not saved' ); 31 | $this->assertNonEmptyMultidimensionalArray( $log->get_logs() ); 32 | 33 | $log->clear_user_logs(); 34 | $this->assertEmpty( $log->get_logs(), 'Clear should clear the logs' ); 35 | 36 | $log->alert_success( 'test success' ); 37 | $logs = $log->get_logs(); 38 | $log_record = reset( $logs ); 39 | $this->assertEquals( 1, count( $logs ), 'Alert should add a single log entry.' ); 40 | $this->assertArrayHasKey( 'type', $log_record, 'Type key is not present in the logs' ); 41 | $this->assertEquals( 'success', $log_record['type'], 'Type key is not present in the logs' ); 42 | 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/variations/Upsell.js: -------------------------------------------------------------------------------- 1 | import { Button } from '@wordpress/components'; 2 | 3 | import { getUtmLink } from '../utils'; 4 | 5 | const Upsell = () => ( 6 |
11 |

20 | 21 | 🔓 22 | 23 | {ropApiSettings.labels.post_editor.upsell.title} 24 |

25 |

31 | {ropApiSettings.labels.post_editor.upsell.subtitle} 32 |

33 | 45 | 57 |
58 | ); 59 | 60 | export default Upsell; 61 | -------------------------------------------------------------------------------- /tests/e2e/specs/dashboard/post-format.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { test, expect } from '@wordpress/e2e-test-utils-playwright'; 5 | import { addFakeTwitterAccount } from '../../utils'; 6 | 7 | test.describe( 'Accounts', () => { 8 | 9 | test.beforeEach( async ( { page, admin } ) => { 10 | await admin.visitAdminPage( '/admin.php?page=TweetOldPost' ); 11 | 12 | // Wait for the accounts tab to load. 13 | await page.waitForSelector( '.tab-view[type="accounts"]' ); 14 | 15 | const accountAdded = await addFakeTwitterAccount( page ); 16 | 17 | expect( accountAdded ).toBe( true ); 18 | } ); 19 | 20 | test( 'check custom content post message', async ( { page, admin } ) => { 21 | await admin.visitAdminPage( '/admin.php?page=TweetOldPost' ); 22 | 23 | // Go to Post Format tab. 24 | await page.getByText('Post Format').click(); 25 | 26 | // Activate Custom Content for the post message. 27 | await page.getByRole('combobox').first().selectOption( 'Custom Content (Pro)' ); 28 | 29 | // Check UI elements. 30 | await expect( page.getByText('Message Content') ).toBeVisible(); 31 | await expect( page.getByPlaceholder('{title} with {content}') ).toBeVisible(); 32 | await expect( page.getByText('Override Share Content') ).toBeVisible(); 33 | await expect( page.getByText('Choose where you want the') ).toBeHidden(); 34 | } ); 35 | 36 | } ); -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest a feature that we can implement. 3 | labels: new feature 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: What problem does this address? 8 | description: | 9 | Can you give us a little more insight into this feature request? We'd love to know if it's related to any problems or pain points you've been facing. 10 | If so, can you please let us know what the issue is in a clear and simple way? 11 | Tip: If this is related to a customer request, please add the Help Scout thread URL. 12 | placeholder: | 13 | For example, something like "I find it tough when..." or "I get frustrated because..." would be great. 14 | validations: 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: What is your proposed solution? 19 | description: Can you please specify the desired feature or improvement and how it resolves the problem mentioned? 20 | validations: 21 | required: false 22 | - type: dropdown 23 | id: doc-needed 24 | attributes: 25 | label: Will this feature require documentation? (Optional) 26 | description: | 27 | Does this feature require the creation or update of documentation? If you're unsure, feel free to skip this. 28 | multiple: false 29 | options: 30 | - 'I dont know.' 31 | - 'No.' 32 | - 'Yes, it requires documentation.' 33 | validations: 34 | required: false -------------------------------------------------------------------------------- /includes/admin/shortners/class-rop-owly-shortner.php: -------------------------------------------------------------------------------- 1 | service_name = 'ow.ly'; 29 | $this->credentials = array( 30 | 'key' => '', 31 | ); 32 | } 33 | 34 | /** 35 | * Method to retrieve the shorten url from the API call. 36 | * 37 | * @since 8.0.0 38 | * @access public 39 | * @param string $url The url to shorten. 40 | * @return string 41 | */ 42 | public function shorten_url( $url ) { 43 | 44 | $response = $this->callAPI( 45 | 'http://ow.ly/api/1.1/url/shorten', 46 | array( 'method' => 'get', 'json' => true ), 47 | array( 'longUrl' => $url, 'apiKey' => $this->credentials['key'] ), 48 | null 49 | ); 50 | $shortURL = $url; 51 | if ( intval( $response['error'] ) == 200 && ! isset( $response['response']['error'] ) ) { 52 | $shortURL = $response['response']['results']['shortUrl']; 53 | } 54 | return $shortURL; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/php/static-analysis-stubs/symbols.php: -------------------------------------------------------------------------------- 1 | 2 |
6 |
16 | 17 | 18 | 56 | 57 | -------------------------------------------------------------------------------- /includes/admin/shortners/class-rop-isgd-shortner.php: -------------------------------------------------------------------------------- 1 | service_name = 'is.gd'; 29 | $this->credentials = array(); 30 | } 31 | 32 | /** 33 | * Method to retrieve the shorten url from the API call. 34 | * 35 | * @since 8.0.0 36 | * @access public 37 | * @param string $url The url to shorten. 38 | * @return string 39 | */ 40 | public function shorten_url( $url ) { 41 | 42 | $response = $this->callAPI( 43 | 'https://is.gd/create.php', 44 | array( 'method' => 'get' ), 45 | array( 46 | 'format' => 'simple', 47 | 'url' => $url, 48 | ), 49 | null 50 | ); 51 | 52 | $shortURL = $url; 53 | if ( intval( $response['error'] ) == 200 && wp_http_validate_url( $response['response'] ) ) { 54 | $shortURL = $response['response']; 55 | } else { 56 | $this->error->throw_exception( 'Error', 'is.gd error: ' . print_r( $response, true ) ); 57 | } 58 | 59 | return $shortURL; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/test-selector.php: -------------------------------------------------------------------------------- 1 | assertGreaterThan( 0, sizeof( $posts_selector->select( Rop_InitAccounts::get_account_id() ) ), 'Empty results from post selector.' ); 39 | 40 | $settings = new Rop_Settings_Model(); 41 | $new_settings = $settings->get_settings(); 42 | $new_settings['minimum_post_age'] = 1; 43 | $new_settings['maximum_post_age'] = 10; 44 | $settings->save_settings( $new_settings ); 45 | $posts_selector2 = new Rop_Posts_Selector_Model(); 46 | $this->assertEquals( 0, sizeof( $posts_selector2->select( Rop_InitAccounts::get_account_id() ) ), 'Date selection is not working.' ); 47 | 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /vue/src/vue-elements/reusables/button-checkbox.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 67 | -------------------------------------------------------------------------------- /includes/admin/shortners/class-rop-firebase-shortner.php: -------------------------------------------------------------------------------- 1 | service_name = 'firebase'; 29 | $this->credentials = array( 30 | 'key' => '', 31 | 'domain' => '', 32 | ); 33 | } 34 | 35 | /** 36 | * Method to retrieve the shorten url from the API call. 37 | * 38 | * @since 8.0.0 39 | * @access public 40 | * @param string $url The url to shorten. 41 | * @return string 42 | */ 43 | public function shorten_url( $url ) { 44 | 45 | $response = $this->callAPI( 46 | 'https://firebasedynamiclinks.googleapis.com/v1/shortLinks?key=' . $this->credentials['key'], 47 | array( 'method' => 'json', 'json' => true ), 48 | array( 'longDynamicLink' => $this->credentials['domain'] . '?link=' . urlencode( $url ) ), 49 | array( 'Content-Type' => 'application/json' ) 50 | ); 51 | 52 | $shortURL = $url; 53 | if ( intval( $response['error'] ) == 200 && ! isset( $response['response']['error'] ) ) { 54 | $shortURL = $response['response']['shortLink']; 55 | } 56 | return $shortURL; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/e2e/utils/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Add a fake Twitter account to the plugin. Must be used in the plugin dashboard page. 4 | * 5 | * @param {import("@playwright/test").Page} page The page object. 6 | */ 7 | export async function addFakeTwitterAccount( page ) { 8 | return await page.evaluate( async () => { 9 | const response = await fetch( `${window.ropApiSettings.root}&req=add_account_tw`, { 10 | method: 'POST', 11 | body: JSON.stringify({ 12 | id: 'aToxMzEwNTQ2NjkzMDU4OTczNj==', 13 | pages: { 14 | id: '13105466930589737', 15 | name: 'Test Account', 16 | screen_name: 'testaccount', 17 | profile_image_url_https: 'https://pbs.twimg.com/profile_images/1331227579035119618/UnO-tehU_normal.jpg', 18 | credentials: { 19 | rop_auth_token: '13105466930' 20 | }, 21 | activate_account: true, 22 | } 23 | }), 24 | headers: { 25 | 'Content-Type': 'application/json', 26 | 'X-WP-Nonce': window.ropApiSettings.nonce, 27 | }, 28 | } ); 29 | 30 | return response.ok; 31 | } ); 32 | } 33 | 34 | /** 35 | * Close the tour modal if it is visible. 36 | * 37 | * @param {import('playwright').Page} page The page object. 38 | */ 39 | export async function tryCloseTourModal( page ) { 40 | if (await page.getByRole('button', { name: 'Skip' }).isVisible()) { 41 | await page.getByRole('button', { name: 'Skip' }).click(); 42 | await page.waitForTimeout(500); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /vue/src/vue-elements/reusables/add-account-tile.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 47 | 48 | 71 | -------------------------------------------------------------------------------- /vue/src/vue-elements/upsell-sidebar.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 56 | 57 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | overrides: [ 8 | // Vue files 9 | { 10 | files: ['vue/**/*.js', 'vue/**/*.vue'], 11 | extends: ['plugin:vue/recommended'], 12 | plugins: ['vue'], 13 | parserOptions: { 14 | parser: 'babel-eslint', 15 | }, 16 | rules: { 17 | 'vue/no-dupe-keys': 'warn', 18 | 'vue/no-unused-components': 'warn', 19 | 'vue/no-side-effects-in-computed-properties': 'warn', 20 | 'vue/return-in-computed-property': 'warn', 21 | 'vue/no-unused-vars': 'warn', 22 | 'vue/no-textarea-mustache': 'warn', 23 | 'vue/require-valid-default-prop': 'warn', 24 | 'vue/multi-word-component-names': 'warn', 25 | 'vue/no-mutating-props': 'warn', 26 | }, 27 | }, 28 | // React files 29 | { 30 | files: [ 'src/**/*.js' ], 31 | extends: ['plugin:@wordpress/eslint-plugin/recommended'], 32 | parserOptions: { 33 | ecmaFeatures: { 34 | jsx: true, 35 | }, 36 | ecmaVersion: 'latest', 37 | sourceType: 'module', 38 | }, 39 | rules: { 40 | 'linebreak-style': ['error', 'unix'], 41 | 'array-bracket-spacing': [ 42 | 'warn', 43 | 'always', 44 | { 45 | arraysInArrays: false, 46 | objectsInArrays: false, 47 | }, 48 | ], 49 | 'key-spacing': [ 50 | 'warn', 51 | { 52 | beforeColon: false, 53 | afterColon: true, 54 | }, 55 | ], 56 | 'object-curly-spacing': [ 57 | 'warn', 58 | 'always', 59 | { 60 | arraysInObjects: true, 61 | objectsInObjects: false, 62 | }, 63 | ], 64 | '@wordpress/i18n-text-domain': [ 65 | 'error', 66 | { 67 | allowedTextDomain: 'tweet-old-post', 68 | }, 69 | ], 70 | '@wordpress/no-unsafe-wp-apis': 0, 71 | }, 72 | }, 73 | ], 74 | }; 75 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: E2E 2 | 3 | on: 4 | pull_request: 5 | types: [synchronize, ready_for_review] 6 | branches-ignore: 7 | - "update_dependencies" 8 | 9 | jobs: 10 | e2e: 11 | runs-on: ubuntu-latest 12 | if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name 13 | 14 | steps: 15 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 16 | - uses: actions/checkout@v4 17 | 18 | - name: Setup PHP 19 | uses: shivammathur/setup-php@v2 20 | with: 21 | php-version: "7.4" 22 | 23 | # run composer install 24 | - name: Install composer 25 | run: composer install --prefer-dist --no-progress --quiet 26 | 27 | # setup the node cache (node_modules) with github actions cache 28 | - name: Cache Node - npm 29 | uses: actions/cache@v4 30 | with: 31 | path: ~/.npm 32 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 33 | restore-keys: | 34 | ${{ runner.os }}-node-cache- 35 | 36 | - name: npm ci 37 | run: | 38 | npm ci 39 | 40 | - name: Make build 41 | run: | 42 | npm run build 43 | npm run sharing 44 | 45 | - name: Setup WP Env 46 | run: | 47 | npm run wp-env start 48 | 49 | - name: Install Playwright 50 | run: | 51 | npm install -g playwright-cli 52 | npx playwright install 53 | 54 | - name: Playwright Blocks 55 | run: | 56 | npm run test:e2e:playwright 57 | 58 | - name: Upload tests artifacts 59 | if: failure() 60 | uses: actions/upload-artifact@v4 61 | with: 62 | name: artifact 63 | path: ./artifacts 64 | retention-days: 1 65 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require( 'webpack' ) 2 | const TerserPlugin = require( "terser-webpack-plugin" ); 3 | const path = require( 'path' ) 4 | const VueLoaderPlugin = require( 'vue-loader' ).VueLoaderPlugin; 5 | 6 | // Main Settings config 7 | module.exports = (env, argv) => ( 8 | { 9 | mode: argv.mode, 10 | devtool: argv.mode === 'production' ? false : 'inline-source-map', 11 | entry: { 12 | dashboard: './vue/src/rop_main.js' , 13 | exclude: './vue/src/rop_exclude_posts.js' , 14 | publish_now: './vue/src/rop_publish_now.js' , 15 | }, 16 | output: { 17 | path: path.resolve( __dirname, './assets/js/build' ), 18 | filename: '[name].js', 19 | publicPath: '/' 20 | }, 21 | module: { 22 | rules: [ 23 | { test: /\.(jpe?g|png|gif|svg)$/i, use: [ 'file-loader?name=assets/img/[name].[ext]' ] }, 24 | { 25 | test: /\.js$/, 26 | exclude: /(node_modules|bower_components)/, 27 | use: { 28 | loader: 'babel-loader', 29 | options: { 30 | presets: [ 31 | [ 32 | "@babel/preset-env", 33 | { 34 | useBuiltIns: "usage", 35 | corejs: "3.22", 36 | targets: [ 37 | "last 2 versions", 38 | "> 1%", 39 | "not ie <= 8" 40 | ] 41 | }, 42 | ], 43 | ] 44 | } 45 | } 46 | }, 47 | { 48 | test: /\.vue$/, 49 | use: 'vue-loader' 50 | }, 51 | { 52 | test: /\.s?css$/, 53 | use: [ 54 | "style-loader", 55 | { 56 | loader: 'css-loader', 57 | options: { 58 | esModule: false 59 | } 60 | }, 61 | "sass-loader" 62 | ], 63 | }, 64 | ] 65 | }, 66 | resolve: { 67 | alias: { 68 | 'vue$': 'vue/dist/vue.esm.js' 69 | } 70 | }, 71 | plugins: [ 72 | new VueLoaderPlugin(), 73 | ], 74 | optimization: { 75 | minimizer: [new TerserPlugin()], 76 | }, 77 | } 78 | ) 79 | -------------------------------------------------------------------------------- /vue/src/vue-elements/reusables/countdown.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 71 | -------------------------------------------------------------------------------- /includes/admin/shortners/class-rop-rebrandly-shortner.php: -------------------------------------------------------------------------------- 1 | service_name = 'rebrand.ly'; 29 | $this->credentials = array( 30 | 'key' => '', 31 | 'domain' => '', 32 | ); 33 | } 34 | 35 | 36 | /** 37 | * Method to retrieve the shorten url from the API call. 38 | * 39 | * @since 8.0.0 40 | * @access public 41 | * @param string $url The url to shorten. 42 | * @return string 43 | */ 44 | public function shorten_url( $url ) { 45 | $post_id = url_to_postid( $url ); 46 | $title = ''; 47 | if ( $post_id ) { 48 | $title = get_the_title( $post_id ); 49 | } 50 | 51 | $response = $this->callAPI( 52 | 'https://api.rebrandly.com/v1/links', 53 | array( 'method' => 'json', 'json' => true ), 54 | array( 'destination' => $url, 'title' => $title, 'domain' => array( 'id' => $this->credentials['domain'] ) ), 55 | array( 'apikey' => $this->credentials['key'], 'Content-Type' => 'application/json' ) 56 | ); 57 | 58 | $shortURL = $url; 59 | if ( intval( $response['error'] ) === 200 ) { 60 | if ( ! array_key_exists( 'httpCode', $response['response'] ) ) { 61 | $shortURL = $response['response']['shortUrl']; 62 | } else { 63 | $this->error->throw_exception( 'Error', "Error from {$this->service_name} " . $response['response']['message'] ); 64 | } 65 | } else { 66 | $this->error->throw_exception( 'Error', "Error from {$this->service_name} " . $response['error'] ); 67 | } 68 | return $shortURL; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /includes/admin/models/class-rop-shortners-model.php: -------------------------------------------------------------------------------- 1 | shortner_name = str_replace( '.', '', $shortner ); 45 | parent::__construct( 'rop_shortners_' . $this->shortner_name ); 46 | $this->credentials = ( $this->get( $this->shortner_name . '_credentials' ) != null ) ? $this->get( $this->shortner_name . '_credentials' ) : $default_credentials; 47 | 48 | } 49 | 50 | /** 51 | * Method to save credentials for shortner. 52 | * 53 | * @since 8.0.0 54 | * @access public 55 | * @param array $credentials The credentials to store. 56 | * @return mixed 57 | */ 58 | public function save( $credentials ) { 59 | return $this->set( $this->shortner_name . '_credentials', $credentials ); 60 | } 61 | 62 | /** 63 | * Method to retrieve credentials of service. 64 | * 65 | * @since 8.0.0 66 | * @access public 67 | * @return array|mixed 68 | */ 69 | public function credentials() { 70 | $this->credentials = ( $this->get( $this->shortner_name . '_credentials' ) != null ) ? $this->get( $this->shortner_name . '_credentials' ) : $this->credentials; 71 | return $this->credentials; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /includes/admin/shortners/class-rop-rvivly-shortner.php: -------------------------------------------------------------------------------- 1 | service_name = 'rviv.ly'; 38 | $this->credentials = array(); 39 | $this->set_website(); 40 | } 41 | 42 | /** 43 | * Utility method to change default website. 44 | * 45 | * @since 8.0.0 46 | * @access public 47 | * @param bool|string $website Optional. Another value for website if required. 48 | */ 49 | public function set_website( $website = false ) { 50 | $this->website = get_bloginfo( 'url' ); 51 | if ( $website ) { 52 | $this->website = $website; 53 | } 54 | } 55 | 56 | /** 57 | * Method to retrieve the shorten url from the API call. 58 | * 59 | * @since 8.0.0 60 | * @access public 61 | * @param string $url The url to shorten. 62 | * @return string 63 | */ 64 | public function shorten_url( $url ) { 65 | $shortURL = $url; 66 | 67 | $response = $this->callAPI( 68 | 'https://rviv.ly/yourls-api.php', 69 | array( 'method' => 'post' ), 70 | array( 'action' => 'shorturl', 'format' => 'simple', 'signature' => substr( md5( $this->website . md5( 'themeisle' ) ), 0, 10 ), 'url' => $url, 'website' => base64_encode( $this->website ) ), 71 | null 72 | ); 73 | 74 | $shortURL = $url; 75 | if ( intval( $response['error'] ) == 200 ) { 76 | $shortURL = $response['response']; 77 | } 78 | if ( $shortURL == null || $shortURL === '' ) { 79 | $shortURL = $url; 80 | } 81 | 82 | return $shortURL; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /includes/class-rop-activator.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class Rop_Activator { 24 | 25 | /** 26 | * Short Description. (use period) 27 | * 28 | * Long Description. 29 | * 30 | * @since 8.0.0 31 | * @access public 32 | */ 33 | public static function activate() { 34 | 35 | // Add version when ROP was first installed on site to DB. 36 | $rop_first_install_version = get_option( 'rop_first_install_version' ); 37 | 38 | if ( class_exists( 'Rop' ) ) { 39 | $rop = new Rop(); 40 | $version = $rop->get_version(); 41 | } 42 | 43 | if ( empty( $rop_first_install_version ) && ! empty( $version ) ) { 44 | add_option( 'rop_first_install_version', $version ); 45 | } 46 | 47 | self::rop_create_install_token(); 48 | self::rop_set_first_install_date(); 49 | 50 | } 51 | 52 | /** 53 | * ROP installation tasks 54 | * 55 | * Creates unique ID for website used during authentication requests 56 | * 57 | * @since 8.3.0 58 | * @package Rop 59 | * @subpackage Rop/includes 60 | * @author Revive Social 61 | */ 62 | private static function rop_create_install_token() { 63 | 64 | $url = get_site_url(); 65 | 66 | $token = hash( 'ripemd160', $url . date( 'Y-m-d H:i:s' ) ); 67 | 68 | update_option( ROP_INSTALL_TOKEN_OPTION, $token, false ); 69 | 70 | } 71 | 72 | /** 73 | * Set Install Date. 74 | * 75 | * @since 1.0.0 76 | */ 77 | private static function rop_set_first_install_date() { 78 | 79 | // Create timestamp for when plugin was activated. 80 | $install_date = time(); 81 | 82 | // If our option doesn't exist already, we'll create it with today's timestamp. 83 | if ( empty( get_option( 'rop_first_install_date' ) ) ) { 84 | add_option( 'rop_first_install_date', $install_date, '', 'yes' ); 85 | } 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /tests/e2e/config/flasky-tests-reporter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A **flaky** test is defined as a test which passed after auto-retrying. 3 | * - By default, all tests run once if they pass. 4 | * - If a test fails, it will automatically re-run at most 2 times. 5 | * - If it pass after retrying (below 2 times), then it's marked as **flaky** 6 | * but displayed as **passed** in the original test suite. 7 | * - If it fail all 3 times, then it's a **failed** test. 8 | */ 9 | /** 10 | * External dependencies 11 | */ 12 | import fs from 'fs'; 13 | import filenamify from 'filenamify'; 14 | 15 | // Remove "steps" to prevent stringify circular structure. 16 | function formatTestResult( testResult ) { 17 | const result = { ...testResult, steps: undefined }; 18 | delete result.steps; 19 | return result; 20 | } 21 | 22 | class FlakyTestsReporter { 23 | failingTestCaseResults = new Map(); 24 | 25 | onBegin() { 26 | try { 27 | fs.mkdirSync( 'flaky-tests' ); 28 | } catch ( err ) { 29 | if ( 30 | err instanceof Error && 31 | ( err ).code === 'EEXIST' 32 | ) { 33 | // Ignore the error if the directory already exists. 34 | } else { 35 | throw err; 36 | } 37 | } 38 | } 39 | 40 | onTestEnd( test, testCaseResult ) { 41 | const testPath = test.location.file; 42 | const testTitle = test.title; 43 | 44 | switch ( test.outcome() ) { 45 | case 'unexpected': { 46 | if (!this.failingTestCaseResults.has(testTitle)) { 47 | this.failingTestCaseResults.set(testTitle, []); 48 | } 49 | const failingResults = this.failingTestCaseResults.get(testTitle); 50 | if (failingResults) { 51 | failingResults.push(formatTestResult(testCaseResult)); 52 | } 53 | break; 54 | } 55 | case 'flaky': { 56 | fs.writeFileSync( 57 | `flaky-tests/${ filenamify( testTitle ) }.json`, 58 | JSON.stringify( { 59 | version: 1, 60 | runner: '@playwright/test', 61 | title: testTitle, 62 | path: testPath, 63 | results: this.failingTestCaseResults.get( testTitle ), 64 | } ), 65 | 'utf-8' 66 | ); 67 | break; 68 | } 69 | default: 70 | break; 71 | } 72 | } 73 | 74 | onEnd() { 75 | this.failingTestCaseResults.clear(); 76 | } 77 | 78 | printsToStdio() { 79 | return false; 80 | } 81 | } 82 | 83 | module.exports = FlakyTestsReporter; -------------------------------------------------------------------------------- /.github/workflows/test-php.yml: -------------------------------------------------------------------------------- 1 | name: Test PHP 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.ref }} 5 | cancel-in-progress: true 6 | on: 7 | push: 8 | branches-ignore: 9 | - "master" 10 | 11 | jobs: 12 | code-sniff: 13 | name: PHP Lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout source code 17 | uses: actions/checkout@v4 18 | - name: Setup PHP 19 | uses: shivammathur/setup-php@v2 20 | with: 21 | php-version: "7.4" 22 | tools: phpcs, phpcbf 23 | - name: Run Composer Install 24 | run: composer install --prefer-dist --no-progress --quiet 25 | continue-on-error: true 26 | - name: Run PHPCS 27 | run: composer run lint 28 | continue-on-error: false 29 | 30 | php-unit: 31 | name: PHPUnit 32 | needs: code-sniff 33 | runs-on: ubuntu-22.04 34 | services: 35 | mysql: 36 | image: mysql:5.7 37 | env: 38 | MYSQL_ROOT_PASSWORD: root 39 | ports: 40 | - 3306/tcp 41 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 42 | steps: 43 | - name: Checkout source code 44 | uses: actions/checkout@v4 45 | - name: Setup PHP 46 | uses: shivammathur/setup-php@v2 47 | with: 48 | php-version: "7.4" 49 | tools: phpunit-polyfills 50 | - name: Install WordPress Test Suite 51 | run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:${{ job.services.mysql.ports['3306'] }} 52 | - name: Install composer 53 | run: composer install --prefer-dist --no-progress 54 | - name: Run PHP Unit 55 | run: composer run test 56 | 57 | phpstan: 58 | name: PHPStan 59 | runs-on: ubuntu-latest 60 | steps: 61 | - name: Setup PHP version 62 | uses: shivammathur/setup-php@v2 63 | with: 64 | php-version: "7.4" 65 | extensions: simplexml 66 | - name: Checkout source code 67 | uses: actions/checkout@v4 68 | - name: Install composer 69 | run: | 70 | composer install --prefer-dist --no-progress 71 | - name: Run phpstan 72 | run: composer run phpstan 73 | -------------------------------------------------------------------------------- /includes/admin/notices/class-rop-admin-notices.php: -------------------------------------------------------------------------------- 1 | ID; 25 | 26 | $days_since_installed = Rop_Admin_Notices_Helpers::rop_get_days_since_installed(); 27 | $should_show_notice = Rop_Admin_Notices_Helpers::rop_should_show_notice( $user_id, $notice_id ); 28 | $revive_network_active = class_exists( 'Revive_Network_Admin' ); 29 | 30 | if ( $days_since_installed >= 3 && $should_show_notice && $revive_network_active === false ) { 31 | 32 | $plugin_image_path = ROP_LITE_URL . 'assets/img/revive-network-logo.png'; 33 | $upsell_title = Rop_I18n::get_labels( 'notices.revive_network_upsell_notice_title' ); 34 | $upsell_body = Rop_I18n::get_labels( 'notices.revive_network_upsell_notice_body' ); 35 | $learn_more = Rop_I18n::get_labels( 'misc.learn_more' ); 36 | $dismiss = Rop_I18n::get_labels( 'notices.dismiss_permanently' ); 37 | $nonce = wp_create_nonce( 'rop_notice_nonce_value' ); 38 | 39 | $dismiss_url = admin_url( 'admin-ajax.php?action=rop_notice_dismissed&rop_notice_id=' . $notice_id . '&rop_notice_nonce=' . $nonce ); 40 | $upsell_link = tsdk_utmify( Rop_Admin::RN_LINK, 'rn', 'admin_notice' ); 41 | $markup = << 44 | 45 |

$upsell_title

46 |

$upsell_body

47 |
48 | 52 | 53 | 54 | UPSELLHTML; 55 | echo $markup; 56 | } 57 | 58 | } 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codeinwp/tweet-old-post", 3 | "description": "Tweet Old Post for WordPress. ", 4 | "keywords": [ 5 | "wordpress", 6 | "share" 7 | ], 8 | "homepage": "https://themeisle.com/plugins/tweet-old-post/", 9 | "license": "GPL-2.0+", 10 | "authors": [ 11 | { 12 | "name": "ThemeIsle Team", 13 | "email": "friends@themeisle.com.com", 14 | "homepage": "https://themeisle.com" 15 | } 16 | ], 17 | "type": "wordpress-plugin", 18 | "support": { 19 | "issues": "https://github.com/Codeinwp/tweet-old-post/issues", 20 | "source": "https://github.com/Codeinwp/tweet-old-post" 21 | }, 22 | "scripts": { 23 | "build": "sh bin/dist.sh", 24 | "test": "vendor/bin/phpunit", 25 | "lint": "vendor/bin/phpcs --standard=phpcs.xml --extensions=php -s", 26 | "format": "vendor/bin/phpcbf --standard=phpcs.xml --report-summary --report-source", 27 | "phpstan": "phpstan", 28 | "phpstan:generate:baseline": "phpstan --generate-baseline" 29 | }, 30 | "autoload": { 31 | "files": [ 32 | "vendor/codeinwp/themeisle-sdk/load.php" 33 | ], 34 | "classmap": [ 35 | "cron-system/includes/class-rop-curl-methods.php", 36 | "cron-system/includes/class-rop-helpers.php", 37 | "cron-system/includes/class-debug-page.php", 38 | "cron-system/rest-endpoints/class-rop-ping-system.php", 39 | "cron-system/rest-endpoints/class-rop-system-base.php", 40 | "cron-system/rest-endpoints/class-rop-debug-ping.php", 41 | "cron-system/rest-endpoints/class-rop-registration-check.php", 42 | "cron-system/class-rop-cron-core.php" 43 | ] 44 | }, 45 | "require": { 46 | "facebook/graph-sdk": "^5.7", 47 | "codeinwp/twitteroauth": "dev-main", 48 | "codeinwp/themeisle-sdk": "^3.3", 49 | "vkcom/vk-php-sdk": "5.101.0" 50 | }, 51 | "extra": { 52 | "installer-disable": true 53 | }, 54 | "require-dev": { 55 | "wp-coding-standards/wpcs": "^2.3", 56 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7.2", 57 | "phpunit/phpunit": "9.*", 58 | "yoast/phpunit-polyfills": "^2.0", 59 | "codeinwp/phpcs-ruleset": "dev-main", 60 | "phpstan/phpstan": "^2.1", 61 | "szepeviktor/phpstan-wordpress": "^2.0" 62 | }, 63 | "config": { 64 | "allow-plugins": { 65 | "dealerdirect/phpcodesniffer-composer-installer": true 66 | } 67 | }, 68 | "repositories": [ 69 | { 70 | "type": "vcs", 71 | "url": "https://github.com/Codeinwp/twitteroauth" 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /tests/e2e/specs/dashboard/publish-now.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { test, expect } from '@wordpress/e2e-test-utils-playwright'; 5 | import { 6 | tryCloseTourModal, 7 | addFakeTwitterAccount 8 | } from '../../utils'; 9 | 10 | test.describe( 'Publish Now', () => { 11 | 12 | test.beforeEach( async ( { page, admin } ) => { 13 | await admin.visitAdminPage( '/admin.php?page=TweetOldPost' ); 14 | 15 | // Wait for the accounts tab to load. 16 | await page.waitForSelector( '.tab-view[type="accounts"]' ); 17 | 18 | const accountAdded = await addFakeTwitterAccount( page ); 19 | 20 | expect( accountAdded ).toBe( true ); 21 | 22 | await admin.createNewPost({ 23 | title: 'Test Post' 24 | }); 25 | await tryCloseTourModal( page ); 26 | } ); 27 | 28 | test( 'Instant Share', async ( { admin, page }) => { 29 | await page.getByRole( 'button', { name: 'Revive Social' } ).click(); 30 | 31 | // Make sure Instant Share button is visible and checked by default. 32 | const shareImmediatelyCheckbox = page.getByRole( 'checkbox', { name: 'Share Immediately' } ); 33 | await expect( shareImmediatelyCheckbox ).toBeVisible(); 34 | await expect( shareImmediatelyCheckbox ).toBeChecked(); 35 | 36 | // Make sure Twitter account is selected by default. 37 | const twitterCheckbox = page.getByRole( 'checkbox', { name: ' @testaccount' } ); 38 | await expect( twitterCheckbox ).toBeVisible(); 39 | await expect( twitterCheckbox ).toBeChecked(); 40 | 41 | const publishButton = page.getByRole( 'button', { name: 'Publish', exact: true } ); 42 | await expect( publishButton ).toBeVisible(); 43 | await publishButton.click(); 44 | 45 | // Make sure Pre Publish modal is visible. 46 | await expect( page.getByLabel( 'Editor publish' ).getByRole( 'button', { name: 'Instant Sharing' } ) ).toBeVisible(); 47 | 48 | await page.getByLabel( 'Editor publish' ).getByRole( 'button', { name: 'Publish', exact: true }).click(); 49 | 50 | await page.waitForTimeout( 5000 ); 51 | 52 | // We make sure post-publish status is visible and manual sharing is visible. 53 | await expect( page.locator('div').filter({ hasText: /^Posting to social media…$/ }).first() ).toBeVisible(); 54 | await expect( page.getByLabel( 'Editor publish' ).getByRole( 'button', { name: 'Manual Sharing' } ) ).toBeVisible(); 55 | } ); 56 | } ); -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report a bug so we can get to squashing it. 3 | labels: bug 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this bug report! 9 | - type: textarea 10 | attributes: 11 | label: Description 12 | description: Please write a brief description of the bug, including what you expect to happen and what is currently happening. 13 | placeholder: | 14 | Feature '...' is not working properly. I expect '...' to happen, but '...' happens instead 15 | validations: 16 | required: true 17 | 18 | - type: textarea 19 | attributes: 20 | label: Step-by-step reproduction instructions 21 | description: Please write the steps needed to reproduce the bug. 22 | placeholder: | 23 | 1. Go to '...' 24 | 2. Click on '...' 25 | 3. Scroll down to '...' 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | attributes: 31 | label: Screenshots, screen recording, code snippet or Help Scout ticket 32 | description: | 33 | If possible, please upload a screenshot or screen recording which demonstrates the bug. 34 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 35 | Tip: You can include links to customer Help Scout support thread. 36 | validations: 37 | required: false 38 | 39 | - type: input 40 | attributes: 41 | label: Environment info 42 | description: | 43 | Please share a https://pastebin.com/ link of your system details by going to site Admin -> Tools -> Site Health -> Info and Copy to Clipboard 44 | placeholder: pastebin.com/ ... 45 | validations: 46 | required: false 47 | 48 | - type: dropdown 49 | id: regression 50 | attributes: 51 | label: Is the issue you are reporting a regression 52 | description: | 53 | Choose "Yes" if the bug appeared after updating the product, meaning it worked before but not now. Choose "No" if the bug isn't caused by an update. Marking a bug as regression helps us fix issues from new changes faster. 54 | multiple: false 55 | options: 56 | - 'No' 57 | - 'Yes, this is a regression.' 58 | validations: 59 | required: true 60 | -------------------------------------------------------------------------------- /includes/admin/helpers/class-rop-exception-handler.php: -------------------------------------------------------------------------------- 1 | logger = new Rop_Logger(); 44 | } 45 | 46 | /** 47 | * Utility method to parse Facebook exceptions. 48 | * 49 | * @since 8.0.0 50 | * @access public 51 | * 52 | * @param object $helper The Facebook helper object. 53 | * 54 | * @return string 55 | */ 56 | public function get_fb_exeption_message( $helper ) { 57 | $message = 'Error: ' . $helper->getError() . PHP_EOL; 58 | $message .= 'Error Code: ' . $helper->getErrorCode() . PHP_EOL; 59 | $message .= 'Error Reason: ' . $helper->getErrorReason() . PHP_EOL; 60 | $message .= 'Error Description: ' . $helper->getErrorDescription() . PHP_EOL; 61 | 62 | return $message; 63 | } 64 | 65 | /** 66 | * Utility method to add to the exceptions array. 67 | * 68 | * @since 8.0.0 69 | * @access public 70 | * 71 | * @param string $message An exception message. 72 | */ 73 | public function register_exception( $message ) { 74 | $this->exception[] = $message; 75 | } 76 | 77 | /** 78 | * Sets the headers and message as a REST response to an exception. 79 | * 80 | * @since 8.0.0 81 | * @access public 82 | * 83 | * @param string $header The header as rfc2616 HTTP compliant code. 84 | * @param string $message The message. 85 | */ 86 | public function throw_exception( $header, $message ) { 87 | 88 | $this->logger->alert_error( $message ); 89 | if ( ! ROP_DEBUG ) { 90 | return; 91 | } 92 | header( 'HTTP/1.0 ' . $header ); 93 | $backtrace = wp_debug_backtrace_summary(); 94 | echo $message . ' BackTrace: ' . $backtrace; 95 | exit; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /tests/test-scheduler.php: -------------------------------------------------------------------------------- 1 | get_default_schedule(); 38 | 39 | $this->assertEquals( $scheduler->get_schedule( $account_id ), $schedule_defaults ); 40 | $this->assertArrayHasKey( 'type', $schedule_defaults ); 41 | $this->assertEquals( 'recurring', $schedule_defaults['type'] ); 42 | 43 | $new_data = $schedule_defaults; 44 | $new_data['type'] = 'fixed'; 45 | $scheduler->add_update_schedule( $account_id, $new_data ); 46 | $this->assertNotEquals( $scheduler->get_schedule( $account_id ), $new_data ); 47 | $schedule_fixed = $scheduler->get_schedule( $account_id ); 48 | $this->assertNotEmpty( $schedule_fixed['interval_f']['week_days'] ); 49 | $this->assertEquals( 7, count( $schedule_fixed['interval_f']['week_days'] ) ); 50 | $this->assertNotEmpty( $schedule_fixed['interval_f']['time'] ); 51 | 52 | /** 53 | * Check if the events limit per account is used. 54 | */ 55 | $events = $scheduler->get_upcoming_events( $account_id ); 56 | $this->assertEquals( Rop_Scheduler_Model::EVENTS_PER_ACCOUNT, count( $events ) ); 57 | $events_clear = reset( $events ); 58 | /** 59 | * Check after we process an event, we still have the timeline full. 60 | */ 61 | $scheduler->remove_timestamp( $events_clear, $account_id ); 62 | $events = $scheduler->get_upcoming_events( $account_id ); 63 | $this->assertEquals( Rop_Scheduler_Model::EVENTS_PER_ACCOUNT, count( $events ), 'We should always have the required events limit for every account.' ); 64 | $scheduler->remove_schedule( $account_id ); 65 | $this->assertEquals( $scheduler->get_schedule( $account_id ), $schedule_defaults ); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /includes/admin/shortners/class-rop-bitly-shortner.php: -------------------------------------------------------------------------------- 1 | service_name = 'bit.ly'; 29 | $this->credentials = array( 30 | 'generic_access_token' => '', 31 | ); 32 | } 33 | 34 | /** 35 | * Handles upgrade from old authentication to new oauth2 keys authentication. 36 | * 37 | * @since ? 38 | * @access public 39 | * @return mixed 40 | */ 41 | public function filter_credentials( $credentials ) { 42 | // if the keys are the same, no sweat. 43 | // if they are anything but identical, we should assume these need to be refreshed as this could be an upgrade to oauth2 keys. 44 | $prev = array_keys( $credentials ); 45 | $now = array_keys( $this->credentials ); 46 | $diff = array_diff( $prev, $now ); 47 | 48 | if ( ! empty( $diff ) ) { 49 | return $this->credentials; 50 | } 51 | return $credentials; 52 | } 53 | 54 | /** 55 | * Method to retrieve the shorten url from the API call. 56 | * 57 | * @since 8.0.0 58 | * @access public 59 | * 60 | * @param string $url The url to shorten. 61 | * 62 | * @return string 63 | */ 64 | public function shorten_url( $url ) { 65 | $saved = $this->get_credentials(); 66 | 67 | if ( ! array_key_exists( 'generic_access_token', $saved ) ) { 68 | $logger = new Rop_Logger(); 69 | $logger->alert_error( 'Generic Access Token not found. Please see the following link for instructions: https://is.gd/rop_bitly' ); 70 | 71 | return $url; 72 | } 73 | 74 | $response = $this->callAPI( 75 | 'https://api-ssl.bit.ly/v4/shorten', 76 | array( 'method' => 'json' ), 77 | array( 'long_url' => $url ), 78 | array( 79 | 'Authorization' => 'Bearer ' . $saved['generic_access_token'], 80 | 'Content-Type' => 'application/json', 81 | ) 82 | ); 83 | 84 | $shortURL = $url; 85 | 86 | if ( intval( $response['error'] ) === 200 || intval( $response['error'] ) === 201 ) { 87 | $response = json_decode( $response['response'] ); 88 | $shortURL = $response->link; 89 | } 90 | 91 | return trim( $shortURL ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /cron-system/rest-endpoints/class-rop-system-base.php: -------------------------------------------------------------------------------- 1 | error = new Rop_Exception_Handler(); 46 | $this->logger = new Rop_Logger(); 47 | } 48 | 49 | /** 50 | * Retrieves the client token from headers sent with the request with REST API. 51 | * 52 | * @param string $token_data Encoded string containing the auth information. 53 | * 54 | * @return bool|string 55 | * @since 8.5.5 56 | * @access protected 57 | */ 58 | protected function fetch_token_from_headers( $token_data = '' ) { 59 | // We need data to exist. 60 | if ( empty( $token_data ) ) { 61 | return false; 62 | } 63 | 64 | // Decode base64 client data. 65 | $decode_data = base64_decode( $token_data ); 66 | 67 | // Create the array which contains user data. 68 | parse_str( $decode_data, $token_holder ); 69 | 70 | if ( ! is_array( $token_holder ) || ! isset( $token_holder['token'] ) ) { 71 | return false; 72 | } 73 | 74 | return sanitize_text_field( trim( $token_holder['token'] ) ); 75 | } 76 | 77 | /** 78 | * Checks if the given auth token is registered. 79 | * 80 | * @param string $token_data Client auth token. 81 | * 82 | * @return bool 83 | * @since 8.5.5 84 | * @access protected 85 | */ 86 | protected function is_valid_token( $token_data = '' ) { 87 | 88 | // Fetch user token from REST API headers request. 89 | $token = $this->fetch_token_from_headers( $token_data ); 90 | 91 | // If the token is not found. 92 | if ( false === $token ) { 93 | return false; 94 | } 95 | 96 | $saved_token = get_option( 'rop_access_token' ); 97 | 98 | return trim( $token ) === trim( $saved_token ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /vue/src/vue-elements/reusables/webhook-headers.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 84 | 85 | -------------------------------------------------------------------------------- /src/manual/index.js: -------------------------------------------------------------------------------- 1 | import { Button, __experimentalHStack as HStack } from '@wordpress/components'; 2 | 3 | import { useCopyToClipboard } from '@wordpress/compose'; 4 | 5 | import { useDispatch, useSelect } from '@wordpress/data'; 6 | 7 | import { store as editorStore } from '@wordpress/editor'; 8 | 9 | import { store as noticesStore } from '@wordpress/notices'; 10 | 11 | const allowedPlatforms = [ 12 | 'facebook', 13 | 'twitter', 14 | 'linkedin', 15 | 'bluesky', 16 | 'tumblr', 17 | 'telegram', 18 | 'whatsapp', 19 | 'link', 20 | ]; 21 | 22 | import { getIcon } from '../utils'; 23 | 24 | const getSocialShareLinks = (title, url) => { 25 | const encodedTitle = encodeURIComponent(title); 26 | const encodedURL = encodeURIComponent(url); 27 | 28 | return { 29 | facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodedURL}`, 30 | twitter: `https://x.com/intent/post?text=${encodedTitle}%20-%20${encodedURL}`, 31 | linkedin: `https://www.linkedin.com/shareArticle?mini=true&url=${encodedURL}&title=${encodedTitle}`, 32 | tumblr: `https://www.tumblr.com/widgets/share/tool?canonicalUrl=${encodedURL}&title=${encodedTitle}`, 33 | telegram: `https://t.me/share/url?url=${encodedURL}&text=${encodedTitle}`, 34 | whatsapp: `https://api.whatsapp.com/send?text=${encodedTitle}%20-%20${encodedURL}`, 35 | bluesky: `https://bsky.app/intent/compose?text=${encodedTitle}%20-%20${encodedURL}`, 36 | link: `${encodedTitle} - ${encodedURL}`, 37 | }; 38 | }; 39 | 40 | const ManualSharing = () => { 41 | const { title, permalink } = useSelect((select) => { 42 | const getAttr = select(editorStore).getCurrentPostAttribute; 43 | return { 44 | title: getAttr('title'), 45 | permalink: getAttr('link'), 46 | }; 47 | }, []); 48 | 49 | const { createNotice } = useDispatch(noticesStore); 50 | 51 | const links = getSocialShareLinks(title, permalink); 52 | 53 | const ref = useCopyToClipboard(links.link, () => 54 | createNotice( 55 | 'info', 56 | ropApiSettings.labels.publish_now.copied_to_clipboard, 57 | { 58 | isDismissible: true, 59 | type: 'snackbar', 60 | } 61 | ) 62 | ); 63 | 64 | return ( 65 | <> 66 |

{ropApiSettings.labels.publish_now.manual_sharing_desc}

67 | 68 | 73 | {allowedPlatforms.map((service) => ( 74 | 88 | ))} 89 | 90 | 91 | ); 92 | }; 93 | 94 | export default ManualSharing; 95 | -------------------------------------------------------------------------------- /vue/src/vue-elements/reusables/vue-spinner.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 75 | 76 | 104 | -------------------------------------------------------------------------------- /assets/css/rop.css: -------------------------------------------------------------------------------- 1 | /** 2 | * All of the CSS for your admin-specific functionality should be 3 | * included in this file. 4 | */ 5 | 6 | *, 7 | *::before, 8 | *::after { 9 | box-sizing: inherit; 10 | } 11 | 12 | .toplevel_page_TweetOldPost #wpbody-content > *:not(.themeisle-sdk-notice), 13 | .revive-old-posts_page_rop_content_filters #wpbody-content > *:not(.themeisle-sdk-notice) { 14 | display: none !important; 15 | } 16 | 17 | .revive-old-posts_page_rop_content_filters #wpbody-content > #rop_content_filters, 18 | .toplevel_page_TweetOldPost #wpbody-content > #rop_core { 19 | display: block !important; 20 | } 21 | 22 | .revive-social-sidebar .revive-social__toggle > .components-base-control__field > .components-flex { 23 | flex-direction: row-reverse; 24 | } 25 | 26 | .revive-social-platform-icon { 27 | width: 28px; 28 | height: 28px; 29 | border-radius: 50%; 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | color: white; 34 | font-weight: bold; 35 | font-size: 14px; 36 | flex-shrink: 0; 37 | } 38 | 39 | .revive-social__checkbox { 40 | --checkbox-input-size: 20px; 41 | } 42 | 43 | .revive-social__modal table { 44 | width: 100%; 45 | } 46 | 47 | .revive-social__modal th { 48 | text-align: left; 49 | padding: 0 0 1rem 0; 50 | } 51 | 52 | .revive-social__modal tbody tr { 53 | height: 40px; 54 | } 55 | 56 | .revive-social__spinner svg { 57 | margin: 0; 58 | } 59 | 60 | .revive-social__spinner p { 61 | margin: 0; 62 | } 63 | 64 | .revive-social__sharing-buttons .components-button { 65 | padding: 0; 66 | height: auto; 67 | } 68 | 69 | .revive-social__sharing-buttons .revive-social-platform-icon { 70 | border-radius: 0; 71 | font-size: 16px; 72 | width: 32px; 73 | height: 32px; 74 | } 75 | 76 | .revive-social__sharing-buttons .revive-social-platform-icon:hover { 77 | opacity: 0.8; 78 | } 79 | 80 | .facebook-icon { 81 | background-color: #1877f2; 82 | } 83 | .twitter-icon { 84 | background-color: #000000; 85 | } 86 | .linkedin-icon { 87 | background-color: #0077b5; 88 | } 89 | .instagram-icon { 90 | background: linear-gradient(135deg, #fbbf24, #ef4444, #8b5cf6); 91 | } 92 | .telegram-icon { 93 | background-color: #0088cc; 94 | } 95 | .tumblr-icon { 96 | background-color: #001935; 97 | } 98 | .vk-icon { 99 | background-color: #0074ab; 100 | } 101 | .mastodon-icon { 102 | background-color: #6364ff; 103 | } 104 | .gmb-icon { 105 | background-color: #323b43; 106 | } 107 | .webhook-icon { 108 | background-color: #ffbc42; 109 | } 110 | .bluesky-icon { 111 | background-color: #0ea5e9; 112 | } 113 | .whatsapp-icon { 114 | background-color: #25d366; 115 | } 116 | 117 | .link-icon { 118 | background-color: #0073aa; 119 | } 120 | -------------------------------------------------------------------------------- /vue/src/vue-elements/reusables/preload_three_dots.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 62 | 63 | 111 | -------------------------------------------------------------------------------- /includes/admin/notices/class-rop-admin-notices-helpers.php: -------------------------------------------------------------------------------- 1 | diff( $today ); 39 | $days_since_installed = $date_difference->format( '%a' ); 40 | return (int) $days_since_installed; 41 | } 42 | 43 | /** 44 | * Check whether the user has dismissed an admin notice and add the option to the database if they did. 45 | * 46 | * @since 8.7.0 47 | */ 48 | public static function rop_notice_dismissed() { 49 | 50 | if ( ! wp_verify_nonce( $_REQUEST['rop_notice_nonce'], 'rop_notice_nonce_value' ) ) { 51 | exit( 'Failed to verify nonce. Please try going back and refreshing the page to try again.' ); 52 | } 53 | 54 | $notice_id = ! empty( $_REQUEST['rop_notice_id'] ) ? $_REQUEST['rop_notice_id'] : ''; 55 | 56 | if ( ! empty( $notice_id ) ) { 57 | 58 | $user_id = wp_get_current_user()->ID; 59 | 60 | add_user_meta( $user_id, $notice_id, 'true', true ); 61 | 62 | wp_redirect( $_SERVER['HTTP_REFERER'] ); 63 | exit; 64 | 65 | } else { 66 | 67 | return; 68 | 69 | } 70 | 71 | } 72 | 73 | /** 74 | * Check whether the user has dismissed an admin notice and add the option to the database if they did. 75 | * 76 | * @param int $user_id The user ID. 77 | * @param string $notice_id unique notice ID. 78 | * @since 8.7.0 79 | */ 80 | public static function rop_should_show_notice( $user_id, $notice_id ) { 81 | 82 | $rop_user_dismissed_notice = get_user_meta( $user_id, $notice_id ); 83 | 84 | // We shouldn't show the notice if user has dismissed it. 85 | if ( ! empty( $rop_user_dismissed_notice ) ) { 86 | return false; 87 | } 88 | 89 | $shownotice = false; 90 | 91 | // These notices should only show to admin users 92 | if ( is_multisite() && current_user_can( 'create_sites' ) ) { 93 | $shownotice = true; 94 | } elseif ( is_multisite() == false && current_user_can( 'install_plugins' ) ) { 95 | $shownotice = true; 96 | } else { 97 | $shownotice = false; 98 | } 99 | 100 | return $shownotice; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/instant/Reshare.js: -------------------------------------------------------------------------------- 1 | import apiFetch from '@wordpress/api-fetch'; 2 | 3 | import { 4 | Button, 5 | __experimentalSpacer as Spacer, 6 | __experimentalVStack as VStack, 7 | } from '@wordpress/components'; 8 | 9 | import { useDispatch, useSelect } from '@wordpress/data'; 10 | 11 | import { store as editorStore } from '@wordpress/editor'; 12 | 13 | import { useState } from '@wordpress/element'; 14 | 15 | import { store as noticesStore } from '@wordpress/notices'; 16 | 17 | import ListItem from './ListItem'; 18 | 19 | const Reshare = ({ accounts, isPro, setHistory }) => { 20 | const [ meta, setMeta ] = useState({ 21 | rop_publish_now_accounts: {}, 22 | }); 23 | 24 | const [ isLoading, setIsLoading ] = useState(false); 25 | 26 | const postId = useSelect( 27 | (select) => select(editorStore).getCurrentPostId(), 28 | [] 29 | ); 30 | 31 | const { createNotice } = useDispatch(noticesStore); 32 | 33 | const shareRequest = async (id, data) => { 34 | setIsLoading(true); 35 | 36 | try { 37 | const request = await apiFetch({ 38 | path: `tweet-old-post/v8/share/${id}`, 39 | method: 'POST', 40 | data: { 41 | ...data, 42 | }, 43 | }); 44 | 45 | if (true !== request.success) { 46 | createNotice('error', request.message, { 47 | isDismissible: true, 48 | type: 'snackbar', 49 | }); 50 | 51 | return request; 52 | } 53 | 54 | if (request?.history) { 55 | setHistory(request.history); 56 | } 57 | 58 | createNotice('info', request.message, { 59 | isDismissible: true, 60 | type: 'snackbar', 61 | }); 62 | 63 | updateMetaValue('rop_publish_now_accounts', {}); 64 | 65 | return request; 66 | } catch (error) { 67 | createNotice('error', error?.message, { 68 | isDismissible: true, 69 | type: 'snackbar', 70 | }); 71 | 72 | throw error; 73 | } finally { 74 | setIsLoading(false); 75 | } 76 | }; 77 | 78 | // We basically create a dummy meta object to hold the accounts 79 | // and their respective messages. 80 | const updateMetaValue = (key, value) => { 81 | setMeta({ 82 | ...meta, 83 | [key]: value, 84 | }); 85 | }; 86 | 87 | return ( 88 | <> 89 |

{ropApiSettings.labels.publish_now.reshare_description}

90 | 91 | 92 | {accounts?.map((key) => ( 93 | 101 | ))} 102 | 103 | 104 | 105 | 121 | 122 | 123 | ); 124 | }; 125 | 126 | export default Reshare; 127 | -------------------------------------------------------------------------------- /src/instant/ListItem.js: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | CheckboxControl, 4 | TextareaControl, 5 | __experimentalHStack as HStack, 6 | __experimentalVStack as VStack, 7 | } from '@wordpress/components'; 8 | 9 | import { useState } from '@wordpress/element'; 10 | 11 | import { commentEditLink, check } from '@wordpress/icons'; 12 | 13 | import { getIcon } from '../utils'; 14 | 15 | const ListItem = ({ id, platform, meta, updateMetaValue, isPro }) => { 16 | const [ isEditing, setIsEditing ] = useState(false); 17 | const [ socialMessage, setSocialMessage ] = useState(''); 18 | 19 | const toggleAccount = (value) => { 20 | const currentAccounts = meta.rop_publish_now_accounts || {}; 21 | const updatedAccounts = { ...currentAccounts }; 22 | 23 | if (value) { 24 | updatedAccounts[id] = socialMessage; 25 | } else { 26 | delete updatedAccounts[id]; 27 | } 28 | 29 | updateMetaValue('rop_publish_now_accounts', updatedAccounts); 30 | }; 31 | 32 | const handleMessageChange = (value) => { 33 | setSocialMessage(value); 34 | const currentAccounts = meta.rop_publish_now_accounts || {}; 35 | const updatedAccounts = { ...currentAccounts, [id]: value }; 36 | updateMetaValue('rop_publish_now_accounts', updatedAccounts); 37 | }; 38 | 39 | return ( 40 | 41 | 42 | toggleAccount(value)} 49 | id={`revive-social-checkbox__${id}`} 50 | className="revive-social__checkbox" 51 | /> 52 | 53 | 54 | 61 | {getIcon(platform?.service)} 62 | 63 |
{platform?.user}
64 |
65 | 66 | 82 | 83 | 84 | ); 85 | } 86 | 87 | if (isPostPublished) { 88 | return ( 89 | <> 90 | 95 | 96 | {history.length > 0 && ( 97 | 104 | )} 105 | 106 | ); 107 | } 108 | 109 | return ( 110 | <> 111 |

{ropApiSettings.labels.publish_now.instant_sharing_desc}

112 | 113 | 118 | updateMetaValue('rop_publish_now', value ? 'yes' : 'no') 119 | } 120 | /> 121 | 122 | {'yes' === meta.rop_publish_now && ( 123 | <> 124 | 125 | {accounts?.map((key) => ( 126 | 136 | ))} 137 | 138 | 139 | 140 | 152 | 153 | 154 | )} 155 | 156 | ); 157 | }; 158 | 159 | export default InstantSharing; 160 | -------------------------------------------------------------------------------- /includes/admin/models/class-rop-post-format-model.php: -------------------------------------------------------------------------------- 1 | defaults = $global_settings->get_default_post_format( $service_name ); 56 | $this->service_name = $service_name; 57 | $this->post_format = ( $this->get( 'post_format' ) != null ) ? $this->get( 'post_format' ) : array(); 58 | } 59 | 60 | /** 61 | * Utility method to retrieve post_format from DB 62 | * and merge them with the global defaults, 63 | * 64 | * @since 8.0.0 65 | * @access public 66 | * 67 | * @param string $account_id The account ID for which to retrieve options. 68 | * 69 | * @return array 70 | */ 71 | public function get_post_format( $account_id = false ) { 72 | $post_format_from_db = $this->get_post_formats(); 73 | if ( empty( $account_id ) ) { 74 | return $post_format_from_db; 75 | } 76 | $selected_post_format = array(); 77 | if ( isset( $post_format_from_db[ $account_id ] ) ) { 78 | $selected_post_format = $post_format_from_db[ $account_id ]; 79 | } 80 | 81 | return $selected_post_format; 82 | } 83 | 84 | /** 85 | * Return post formats array. 86 | * 87 | * @return array Array of formats. 88 | */ 89 | public function get_post_formats() { 90 | $services = new Rop_Services_Model(); 91 | $active_accounts = $services->get_active_accounts(); 92 | 93 | $saved_formats = $this->get( 'post_format' ); 94 | $saved_formats = ( is_array( $saved_formats ) ) ? $saved_formats : array(); 95 | $valid_formats = array(); 96 | foreach ( $active_accounts as $account_id => $data ) { 97 | $valid_formats[ $account_id ] = isset( $saved_formats[ $account_id ] ) ? $saved_formats[ $account_id ] : ( empty( $this->service_name ) ? $this->defaults[ $data['service'] ] : $this->defaults ); 98 | } 99 | 100 | return $valid_formats; 101 | } 102 | 103 | /** 104 | * Method to add a post format per. account. 105 | * 106 | * @since 8.0.0 107 | * @access public 108 | * 109 | * @param string $account_id The account ID for which to add post format. 110 | * @param array $data The post format data. 111 | * 112 | * @return void 113 | */ 114 | public function add_update_post_format( $account_id, $data ) { 115 | $data = wp_parse_args( $data, $this->defaults ); 116 | $this->post_format[ $account_id ] = $data; 117 | $this->set( 'post_format', $this->post_format ); 118 | 119 | } 120 | 121 | /** 122 | * Method to remove a post format per. account. 123 | * 124 | * @since 8.0.0 125 | * @access public 126 | * 127 | * @param string $account_id The account ID for which to update post format. 128 | * 129 | * @return mixed 130 | */ 131 | public function remove_post_format( $account_id ) { 132 | unset( $this->post_format[ $account_id ] ); 133 | 134 | $this->set( 'post_format', $this->post_format ); 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /tests/helpers/class-setup-accounts.php: -------------------------------------------------------------------------------- 1 | array( 33 | 'class' => 'Facebook\Facebook', 34 | 'credentials' => array( 35 | '470293890022208', 36 | 'bf3ee9335692fee071c1a41fbe52fdf5' 37 | ), 38 | 'credentials_name' => array( 39 | 'app_id', 40 | 'secret' 41 | ) 42 | ), 43 | 'Twitter' => array( 44 | 'class' => 'Abraham\TwitterOAuth\TwitterOAuth', 45 | 'credentials' => array( 46 | '', 47 | '' 48 | ), 49 | 'credentials_name' => array( 50 | 'oauth_token', 51 | 'oauth_token_secret' 52 | ) 53 | ), 54 | ); 55 | private static $factory; 56 | 57 | /** 58 | * Testing services avilability. 59 | * 60 | * @since 8.0.0 61 | * @access public 62 | */ 63 | public static function init() { 64 | 65 | $services_model = new Rop_Services_Model(); 66 | $factory = new Rop_Services_Factory(); 67 | try { 68 | $test_twitter_service = $factory->build( 'twitter' ); 69 | } catch ( Exception $exception ) { 70 | 71 | } 72 | $user_test = wp_parse_args( array( 73 | 'account' => 'test account', 74 | 'user' => '@roptest', 75 | 'id' => self::ROP_TEST_ACCOUNT_ID, 76 | 'service' => 'twitter' 77 | ), $test_twitter_service->user_default ); 78 | $service_test = array( 79 | 'id' => self::ROP_TEST_SERVICE_ID, 80 | 'service' => 'twitter', 81 | 'credentials' => array(), 82 | 'public_credentials' => false, 83 | 'available_accounts' => array( $user_test ), 84 | ); 85 | $new_service[ $service_test['service'] . '_' . $service_test['id'] ] = $service_test; 86 | $services_model->add_authenticated_service( $new_service ); 87 | self::$factory = new WP_UnitTest_Factory(); 88 | } 89 | 90 | public static function generatePosts( $count = 1, $type = 'post', $time_shift = '- 1 day' ) { 91 | $post_ids = array(); 92 | $date = date( 'Y-m-d H:i:s' ); 93 | if ( $time_shift ) { 94 | $date = date( 'Y-m-d H:i:s', strtotime( $time_shift ) ); 95 | } 96 | $tags = array(); 97 | $cats = array(); 98 | // Setup terms. 99 | for ( $i = 0; $i < 5; $i ++ ) { 100 | $tag_name = sprintf( 'Tag %s', $i ); 101 | //$tag = wp_insert_term( $tag_name, 'post_tag' ); 102 | $tags[] = $tag_name; 103 | } 104 | for ( $i = 0; $i < 5; $i ++ ) { 105 | $cat_name = sprintf( 'Category %s', $i ); 106 | $cat = wp_insert_term( $cat_name, 'category' ); 107 | $cats[] = $cat; 108 | } 109 | //var_dump( $date ); 110 | for ( $i = 0; $i < $count; $i ++ ) { 111 | $content = file_get_contents( 'https://loripsum.net/api/5/medium/plaintext' ); 112 | $id = self::$factory->post->create( array( 113 | 'post_title' => 'Test Post ' . str_pad( $i + 1, 2, "0", STR_PAD_LEFT ), 114 | 'post_content' => $content, 115 | 'post_type' => $type, 116 | 'post_date' => $date, 117 | 'post_status' => 'publish' 118 | ) ); 119 | $tag_value = $tags[ rand( 0, count( $tags ) - 1 ) ]; 120 | $cat_value = $cats[ rand( 0, count( $cats ) -1 ) ]; 121 | 122 | wp_set_post_terms( $id, $tag_value, 'post_tag' ); 123 | wp_set_post_terms( $id, $cat_value['term_id'], 'category' ); 124 | array_push( $post_ids, $id ); 125 | } 126 | 127 | return $post_ids; 128 | } 129 | 130 | public static function get_account_id() { 131 | return self::ROP_TEST_SERVICE_NAME . '_' . self::ROP_TEST_SERVICE_ID . '_' . self::ROP_TEST_ACCOUNT_ID; 132 | } 133 | } 134 | 135 | -------------------------------------------------------------------------------- /cron-system/class-rop-cron-core.php: -------------------------------------------------------------------------------- 1 | init_rest_api_route(); 70 | 71 | $debug = new Rop_Debug_Ping(); 72 | $debug->init_rest_api_route(); 73 | 74 | $registration_check = new Rop_Registration_Check(); 75 | $registration_check->init_rest_api_route(); 76 | 77 | new Debug_Page(); 78 | } 79 | 80 | /** 81 | * Register to ROP Cron the share start. 82 | * 83 | * @access public 84 | * @since 8.5.5 85 | */ 86 | public function server_start_share() { 87 | 88 | $time_to_share = current_time( 'timestamp' ) + 30; // phpcs:ignore 89 | 90 | $request_call = new Rop_Curl_Methods(); 91 | 92 | $arguments = array( 93 | 'type' => 'POST', 94 | 'request_path' => ':activate_account:', 95 | 'time_to_share' => date( 'Y-m-d H:i:s', $time_to_share ),// phpcs:ignore 96 | ); 97 | 98 | $call_response = $request_call->create_call_process( $arguments ); 99 | // TODO add to log. 100 | } 101 | 102 | /** 103 | * Register to ROP Cron the share stop. 104 | * 105 | * @access public 106 | * @since 8.5.5 107 | */ 108 | public function server_stop_share() { 109 | $request_call = new Rop_Curl_Methods(); 110 | 111 | $arguments = array( 112 | 'type' => 'POST', 113 | 'request_path' => ':disable_account:', 114 | ); 115 | 116 | $call_response = $request_call->create_call_process( $arguments ); 117 | // TODO add to log. 118 | } 119 | 120 | /** 121 | * Register to ROP Cron the next valid time to share from queue. 122 | * 123 | * @access public 124 | * @since 8.5.5 125 | */ 126 | public function server_update_time_to_share() { 127 | $time_to_share = Rop_Helpers::extract_time_to_share();// This will be in UNIX time from the database queue. 128 | 129 | $request_call = new Rop_Curl_Methods(); 130 | 131 | $arguments = array( 132 | 'type' => 'POST', 133 | 'request_path' => ':share_time:', 134 | 'time_to_share' => date( 'Y-m-d H:i:s', $time_to_share ),// phpcs:ignore 135 | ); 136 | 137 | $call_response = $request_call->create_call_process( $arguments ); 138 | 139 | // TODO add to log. 140 | } 141 | 142 | /** 143 | * Register to ROP Cron a new account. 144 | * 145 | * @access public 146 | * @since 8.5.5 147 | */ 148 | public function server_register_client() { 149 | $request_call = new Rop_Curl_Methods(); 150 | 151 | $arguments = array( 152 | 'type' => 'POST', 153 | 'request_path' => ':register_account:', 154 | ); 155 | $call_response = $request_call->create_call_process( $arguments ); 156 | 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /tests/test-plugin.php: -------------------------------------------------------------------------------- 1 | assertTrue( class_exists( 'ThemeisleSDK\Loader' ), 'ThemeIsle SDK Is NOT present.' ); 35 | 36 | $all_plugins = apply_filters( 'themeisle_sdk_products', array() ); 37 | $this->assertContains( ROP_LITE_BASE_FILE, $all_plugins, 'ThemeIsle is NOT loaded' ); 38 | 39 | $this->assertFalse( ROP_DEBUG ); 40 | } 41 | 42 | /** 43 | * Testing General Settings. 44 | * 45 | * @access public 46 | */ 47 | public function test_global_settings() { 48 | $global = new Rop_Global_Settings(); 49 | $this->assertFalse( $global->get_start_time(), 'By default the start should be off' ); 50 | $global->update_start_time(); 51 | $this->assertLessThan( 5, ( time() - $global->get_start_time() ), ' Starttime is not relative to unix timestamp.' ); 52 | $global->reset_start_time(); 53 | $this->assertFalse( $global->get_start_time(), 'After reset start time should be false' ); 54 | $this->assertEquals( - 1, $global->license_type(), 'License type should be -1 by default' ); 55 | $defaults = $global->get_default_settings(); 56 | $this->assertNotEmpty( $defaults, 'Default general settings should not be empty.' ); 57 | } 58 | 59 | /** 60 | * Testing Settings actions. 61 | * 62 | * @access public 63 | */ 64 | public function test_settings_model() { 65 | $settings = new Rop_Settings_Model(); 66 | $this->assertFalse( $settings->get_start_time(), 'Start time should be false by default.' ); 67 | $this->assertFalse( $settings->get_custom_messages(), 'Custom messages should be off by default.' ); 68 | $this->assertEmpty( $settings->get_selected_posts(), 'Exclude posts should be empty by default.' ); 69 | $this->assertTrue( $settings->get_ga_tracking(), 'GA tags should be on by default.' ); 70 | $this->assertEquals( 12.00, $settings->get_interval(), 'Default interval should be 4 hours.' ); 71 | $this->assertEquals( 365, $settings->get_maximum_post_age(), 'Default maximum age should be 0' ); 72 | $this->assertEquals( 30, $settings->get_minimum_post_age(), 'Default minimum age should be 0' ); 73 | $this->assertTrue( $settings->get_more_than_once(), 'More than once setting should be on' ); 74 | $this->assertNotEmpty( $settings->get_selected_post_types(), 'Default we should have the post type selected' ); 75 | $this->assertEquals( 1, $settings->get_number_of_posts(), 'By default we should have 1 post selected.' ); 76 | 77 | $settings_data = $settings->get_settings(); 78 | $this->assertNotEmpty( $settings_data, 'Settings data should not be empty' ); 79 | self::$settings = $settings; 80 | } 81 | 82 | public function testing_settings_validation() { 83 | 84 | $settings_data = self::$settings->get_settings(); 85 | $settings = self::$settings; 86 | $settings->save_settings( $settings_data ); 87 | $settings_data_after = $settings->get_settings(); 88 | $this->assertEquals( $settings_data, $settings_data_after, 'Settings data missmatch after no change.' ); 89 | 90 | $settings_data['custom_messages'] = true; 91 | $settings->save_settings( $settings_data ); 92 | $this->assertTrue( $settings->get_custom_messages(), 'Custom share messages not changed.' ); 93 | 94 | $settings_data['number_of_posts'] = 10; 95 | $settings->save_settings( $settings_data ); 96 | $this->assertNotEquals( 10, $settings->get_number_of_posts(), 'It should not allow to save more than 5 posts per cycle.' ); 97 | 98 | $settings_data['number_of_posts'] = - 1; 99 | $settings->save_settings( $settings_data ); 100 | $this->assertNotEquals( - 1, $settings->get_number_of_posts(), 'It should not allow to save negative number of posts ' ); 101 | 102 | $settings_data['default_interval'] = 0.0001; 103 | $settings->save_settings( $settings_data ); 104 | $this->assertNotEquals( 0.0001, $settings->get_interval(), 'Time interval should not be less than 6 min.' ); 105 | 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /vue/src/vue-elements/reusables/counter-input.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 144 | 145 | -------------------------------------------------------------------------------- /vue/src/vue-elements/queue-tab-panel.vue: -------------------------------------------------------------------------------- 1 | 96 | 97 | 155 | -------------------------------------------------------------------------------- /src/instant/PostUpdate.js: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Modal, 4 | Spinner, 5 | __experimentalHStack as HStack, 6 | } from '@wordpress/components'; 7 | 8 | import { dateI18n } from '@wordpress/date'; 9 | 10 | import { useEffect, useState } from '@wordpress/element'; 11 | 12 | import { capitalize } from 'lodash'; 13 | 14 | import { getIcon, getPostMeta } from '../utils'; 15 | 16 | let interval; 17 | 18 | const getLabels = (history) => { 19 | const length = history.reduce((acc, item) => { 20 | if ('error' === item.status) { 21 | return 'failed'; 22 | } 23 | if ('success' === item.status) { 24 | return 'success'; 25 | } 26 | 27 | return 'partially_shared'; 28 | }, ''); 29 | 30 | switch (length) { 31 | case 'failed': 32 | return { 33 | title: ropApiSettings.labels.publish_now.share_failed_title, 34 | description: 35 | ropApiSettings.labels.publish_now.share_failed_desc, 36 | }; 37 | case 'partially_shared': 38 | return { 39 | title: ropApiSettings.labels.publish_now 40 | .share_partially_shared_title, 41 | description: 42 | ropApiSettings.labels.publish_now 43 | .share_partially_shared_desc, 44 | }; 45 | default: 46 | return { 47 | title: ropApiSettings.labels.publish_now.shared_title, 48 | description: ropApiSettings.labels.publish_now.shared_desc, 49 | }; 50 | } 51 | }; 52 | 53 | const formatTimestamp = (timestamp) => { 54 | return dateI18n('j F, Y g:i A', timestamp); 55 | }; 56 | 57 | const TableRow = ({ service, account, timestamp, status }) => { 58 | return ( 59 | 60 | 61 | 62 | <>{getIcon(service)} 63 | <>{ropApiSettings.publish_now.accounts[account]?.user} 64 | 65 | 66 | {formatTimestamp(Number(timestamp + '000'))} 67 | {capitalize(status)} 68 | 69 | ); 70 | }; 71 | 72 | const HistoryTable = ({ data }) => { 73 | return ( 74 | 75 | 76 | 77 | 80 | 83 | 86 | 87 | 88 | 89 | {data.map((item, index) => { 90 | return ( 91 | 98 | ); 99 | })} 100 | 101 |
78 | {ropApiSettings.labels.publish_now.account} 79 | 81 | {ropApiSettings.labels.publish_now.time} 82 | 84 | {ropApiSettings.labels.publish_now.status} 85 |
102 | ); 103 | }; 104 | 105 | const HistoryModal = ({ history, isOpen, setOpen }) => { 106 | const onClose = () => { 107 | setOpen(!isOpen); 108 | }; 109 | 110 | if (!isOpen) { 111 | return null; 112 | } 113 | 114 | return ( 115 | 121 | 122 | 123 | ); 124 | }; 125 | 126 | const PostUpdate = ({ 127 | status, 128 | history, 129 | isPostPublish, 130 | setStatus, 131 | setHistory, 132 | }) => { 133 | const [ isOpen, setOpen ] = useState(false); 134 | const isQueued = history.some((item) => 'queued' === item.status); 135 | 136 | useEffect(() => { 137 | if (status === 'queued' || isQueued) { 138 | interval = setInterval(() => { 139 | const currentStatus = getPostMeta(); 140 | setStatus(currentStatus?.rop_publish_now_status); 141 | setHistory(currentStatus?.rop_publish_now_history || []); 142 | }, 5000); 143 | } else { 144 | clearInterval(interval); 145 | } 146 | return () => clearInterval(interval); 147 | }, [ status, isQueued ]); 148 | 149 | useEffect(() => { 150 | if ('done' === status && !isQueued) { 151 | clearInterval(interval); 152 | } 153 | }, [ status ]); 154 | 155 | if ('queued' === status || isQueued) { 156 | return ( 157 | 158 | 159 |

{ropApiSettings.labels.publish_now.queued}

160 |
161 | ); 162 | } 163 | 164 | if ('done' === status && history.length === 0) { 165 | return null; 166 | } 167 | 168 | const labels = getLabels(history); 169 | 170 | return ( 171 | <> 172 | {isPostPublish && ( 173 | <> 174 |

{labels.title}

175 |

{labels.description}

176 | 177 | )} 178 | 179 | 180 | 181 | 191 | 192 | ); 193 | }; 194 | 195 | export default PostUpdate; 196 | -------------------------------------------------------------------------------- /cron-system/rest-endpoints/class-rop-ping-system.php: -------------------------------------------------------------------------------- 1 | WP_REST_Server::CREATABLE, 75 | 'callback' => array( &$this, 'catch_authorization_data' ), 76 | 'permission_callback' => array( &$this, 'catch_authorization_data_permissions' ), // Callback method for this endpoint 77 | ) 78 | ); 79 | } 80 | 81 | /** 82 | * Retrieves end-point data and responds with json element. 83 | * 84 | * @param WP_REST_Request $request End-point data sent to the endpoint. 85 | * 86 | * @return bool 87 | * @since 8.5.5 88 | * @access public 89 | */ 90 | public function catch_authorization_data( WP_REST_Request $request ) { 91 | // Get the headers the client is sending. 92 | $headers = Rop_Helpers::apache_request_headers(); 93 | 94 | if ( empty( $headers ) || ! isset( $headers['rop-authorization'] ) ) { 95 | 96 | return false; 97 | } 98 | 99 | // Fetch the client identity from headers. 100 | $fetch_token = $this->fetch_token_from_headers( $headers['rop-authorization'] ); 101 | 102 | if ( false === $fetch_token ) { 103 | 104 | return false; 105 | } 106 | 107 | $fetch_related_data = $this->fetch_next_time_to_share(); 108 | 109 | if ( false !== $fetch_related_data ) { 110 | // This info goes to the ROP server. 111 | $return_data = array( 112 | 'success' => true, 113 | 'next-time-to-share' => $fetch_related_data, // test line 114 | 'timezone' => Rop_Helpers::local_timezone(), 115 | ); 116 | 117 | $admin = new Rop_Admin(); 118 | $admin->rop_cron_job(); 119 | } else { 120 | 121 | // Could not fetch the next time to share. 122 | $return_data = array( 123 | 'success' => false, 124 | 'next-time-to-share' => false, // test line 125 | ); 126 | } 127 | 128 | wp_send_json( $return_data ); 129 | } 130 | 131 | /** 132 | * Handles the endpoint restriction. 133 | * Here we will validate the client token. 134 | * 135 | * @return bool 136 | * @since 8.5.5 137 | * @access public 138 | */ 139 | public function catch_authorization_data_permissions() { 140 | // Get the headers the client is sending. 141 | $headers = Rop_Helpers::apache_request_headers(); 142 | 143 | if ( empty( $headers ) || ! isset( $headers['rop-authorization'] ) ) { 144 | return false; 145 | } 146 | 147 | return $this->is_valid_token( $headers['rop-authorization'] ); 148 | } 149 | 150 | 151 | /** 152 | * Register the endpoint using WP Hook. 153 | * 154 | * @since 8.5.5 155 | * @access public 156 | */ 157 | public function init_rest_api_route() { 158 | add_action( 'rest_api_init', array( &$this, 'register_routes' ) ); 159 | } 160 | 161 | 162 | /** 163 | * Reads from the database the next time to share. 164 | * 165 | * @return int Unix Timestamp with next time to share. 166 | * @since 8.5.5 167 | * @access private 168 | */ 169 | private function fetch_next_time_to_share() { 170 | $time_to_share = Rop_Helpers::extract_time_to_share();// This will be in UNIX time from the database queue. 171 | 172 | // TODO This should only happen if Sharing is active 173 | 174 | if ( empty( $time_to_share ) ) { 175 | $this->logger->alert_error( 'Could not fetch future share timer.' ); 176 | 177 | return false; 178 | } 179 | 180 | return $time_to_share; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /.github/workflows/build-dev-artifacts.yml: -------------------------------------------------------------------------------- 1 | name: Build Artifact 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, ready_for_review] 6 | branches-ignore: 7 | - "update_dependencies" 8 | 9 | jobs: 10 | dev-zip: 11 | name: Build ZIP and Upload To S3 12 | if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name 13 | runs-on: ubuntu-latest 14 | outputs: 15 | branch-name: ${{ steps.retrieve-branch-name.outputs.branch_name }} 16 | git-sha-8: ${{ steps.retrieve-git-sha-8.outputs.sha8 }} 17 | steps: 18 | - name: Checkout Source Files 19 | uses: actions/checkout@v2 20 | - name: Setup PHP 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: "7.4" 24 | - name: Get Composer Cache Directory 25 | id: composer-cache 26 | run: | 27 | echo "::set-output name=dir::$(composer config cache-files-dir)" 28 | - name: Configure Composer Cache 29 | uses: actions/cache@v4 30 | with: 31 | path: ${{ steps.composer-cache.outputs.dir }} 32 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 33 | restore-keys: | 34 | ${{ runner.os }}-composer- 35 | - name: Install composer deps 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} 38 | run: | 39 | composer install --no-dev --prefer-dist --no-progress 40 | - name: Create dev build file 41 | run: | 42 | npm ci 43 | npm run build-dev 44 | npm run sharing 45 | - name: Create Zip file 46 | run: npm run dist 47 | - name: Retrieve branch name 48 | id: retrieve-branch-name 49 | run: | 50 | echo "::set-output name=branch_name::$(REF=${GITHUB_HEAD_REF:-$GITHUB_REF} && echo ${REF#refs/heads/} | sed 's/\//-/g')" 51 | echo "branch name: $(REF=${GITHUB_HEAD_REF:-$GITHUB_REF} && echo ${REF#refs/heads/} | sed 's/\//-/g')" 52 | - name: Retrieve git SHA-8 string 53 | id: retrieve-git-sha-8 54 | run: echo "::set-output name=sha8::$(echo ${GITHUB_SHA} | cut -c1-8)" 55 | - name: Upload Latest Version to S3 56 | uses: jakejarvis/s3-sync-action@master 57 | with: 58 | args: --acl public-read --follow-symlinks --delete 59 | env: 60 | AWS_S3_BUCKET: ${{ secrets.AWS_DEV_BUCKET }} 61 | AWS_ACCESS_KEY_ID: ${{ secrets.S3_AWS_KEY_ARTIFACTS }} 62 | AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_AWS_SECRET_ARTIFACTS }} 63 | SOURCE_DIR: artifact/ 64 | DEST_DIR: ${{ github.event.pull_request.base.repo.name }}-${{ steps.retrieve-branch-name.outputs.branch_name }}-${{ steps.retrieve-git-sha-8.outputs.sha8 }}/ 65 | 66 | comment-on-pr: 67 | name: Comment on PR With Links to Plugin Zip 68 | if: ${{ github.head_ref && github.head_ref != null }} 69 | runs-on: ubuntu-latest 70 | needs: dev-zip 71 | env: 72 | CI: true 73 | GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} 74 | outputs: 75 | pr_number: ${{ steps.get-pr-number.outputs.num }} 76 | comment_body: ${{ steps.get-comment-body.outputs.body }} 77 | steps: 78 | - name: Get PR number 79 | id: get-pr-number 80 | run: echo "::set-output name=num::$(echo $GITHUB_REF | awk 'BEGIN { FS = "/" } ; { print $3 }')" 81 | - name: Check if a comment was already made 82 | id: find-comment 83 | uses: peter-evans/find-comment@v1 84 | with: 85 | issue-number: ${{ steps.get-pr-number.outputs.num }} 86 | comment-author: pirate-bot 87 | body-includes: Download [build] 88 | - name: Get comment body 89 | id: get-comment-body 90 | run: | 91 | body="Plugin build for ${{ github.event.pull_request.head.sha }} is ready :bellhop_bell:! 92 | - Download [build](https://verti-artifacts.s3.amazonaws.com/${{ github.event.pull_request.base.repo.name }}-${{ needs.dev-zip.outputs.branch-name }}-${{ needs.dev-zip.outputs.git-sha-8 }}/tweet-old-post.zip)" 93 | body="${body//$'\n'/'%0A'}" 94 | echo "::set-output name=body::$body" 95 | - name: Create comment on PR with links to plugin builds 96 | if: ${{ steps.find-comment.outputs.comment-id == '' }} 97 | uses: peter-evans/create-or-update-comment@v1 98 | with: 99 | issue-number: ${{ steps.get-pr-number.outputs.num }} 100 | token: ${{ secrets.BOT_TOKEN }} 101 | body: ${{ steps.get-comment-body.outputs.body }} 102 | - name: Update comment on PR with links to plugin builds 103 | if: ${{ steps.find-comment.outputs.comment-id != '' }} 104 | uses: peter-evans/create-or-update-comment@v1 105 | with: 106 | comment-id: ${{ steps.find-comment.outputs.comment-id }} 107 | token: ${{ secrets.BOT_TOKEN }} 108 | edit-mode: replace 109 | body: ${{ steps.get-comment-body.outputs.body }} 110 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { Icon } from '@wordpress/components'; 2 | 3 | import { PanelBody } from '@wordpress/components'; 4 | 5 | import { useEntityProp } from '@wordpress/core-data'; 6 | 7 | import { useSelect } from '@wordpress/data'; 8 | 9 | import { 10 | PluginPrePublishPanel, 11 | PluginPostPublishPanel, 12 | PluginSidebar, 13 | PluginSidebarMoreMenuItem, 14 | store as editorStore, 15 | } from '@wordpress/editor'; 16 | 17 | import { useEffect } from '@wordpress/element'; 18 | 19 | import { registerPlugin } from '@wordpress/plugins'; 20 | 21 | import InstantSharing from './instant'; 22 | import ManualSharing from './manual'; 23 | import Variations from './variations'; 24 | import Upsell from './variations/Upsell'; 25 | 26 | const icon = ( 27 | 30 | 34 | 35 | } 36 | /> 37 | ); 38 | 39 | const isPro = Number(ropApiSettings.license_type) > 0; 40 | 41 | const render = () => { 42 | const postType = useSelect( 43 | (select) => select(editorStore).getCurrentPostType(), 44 | [] 45 | ); 46 | const postStatus = useSelect( 47 | (select) => select(editorStore).getCurrentPostAttribute('status'), 48 | [] 49 | ); 50 | 51 | const [ meta, setMeta ] = useEntityProp('postType', postType, 'meta'); 52 | 53 | const updateMetaValue = (keyOrObject, newValue) => { 54 | if (typeof keyOrObject === 'object' && keyOrObject !== null) { 55 | setMeta({ ...meta, ...keyOrObject }); 56 | } else { 57 | setMeta({ ...meta, [keyOrObject]: newValue }); 58 | } 59 | }; 60 | 61 | useEffect(() => { 62 | if ( 63 | 'initial' === meta.rop_publish_now && 64 | postStatus !== 'publish' && 65 | ropApiSettings.publish_now.instant_share_by_default 66 | ) { 67 | updateMetaValue('rop_publish_now', 'yes'); 68 | } 69 | }, []); 70 | 71 | return ( 72 | <> 73 | 74 | {ropApiSettings.labels.general.plugin_name} 75 | 76 | 77 | 83 | {Boolean(ropApiSettings.publish_now.instant_share_enabled) && ( 84 | <> 85 | 91 | 98 | 99 | 100 | {postStatus === 'publish' && ( 101 | 107 | 108 | 109 | )} 110 | 111 | )} 112 | 113 | {isPro && Boolean(ropApiSettings.custom_messages) && ( 114 | 115 | )} 116 | 117 | {!isPro && } 118 | 119 | 120 | {Boolean(ropApiSettings.publish_now.instant_share_enabled) && ( 121 | <> 122 | 129 | 136 | 137 | 138 | {postStatus === 'publish' && ( 139 | <> 140 | 144 | 151 | 152 | 153 | 161 | 162 | 163 | 164 | )} 165 | 166 | )} 167 | 168 | ); 169 | }; 170 | 171 | if ( 172 | Boolean(ropApiSettings.publish_now.instant_share_enabled) || 173 | (isPro && Boolean(ropApiSettings.custom_messages)) 174 | ) { 175 | registerPlugin('revive-social', { render }); 176 | } 177 | --------------------------------------------------------------------------------