├── .editorconfig ├── .github └── workflows │ ├── behat-test.yml │ ├── cs-lint.yml │ └── integrations.yml ├── .gitignore ├── .phpcs.xml.dist ├── README.md ├── bin └── install-wp-tests.sh ├── class.mexp.php ├── class.oauth.php ├── class.plugin.php ├── class.response.php ├── class.service.php ├── class.template.php ├── composer.json ├── css └── mexp.css ├── js └── mexp.js ├── media-explorer.php ├── mexp-creds.php ├── mexp-keyring-user-creds.php ├── phpunit.xml.dist ├── readme.txt ├── services ├── instagram │ ├── js.js │ ├── service.php │ ├── style.css │ └── template.php ├── twitter │ ├── class.wp-twitter-oauth.php │ ├── js.js │ ├── service.php │ └── template.php └── youtube │ ├── class.wp-youtube-client.php │ ├── js.js │ ├── service.php │ └── template.php └── tests ├── bootstrap.php └── media-explorer-testcase.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # It is based on https://core.trac.wordpress.org/browser/trunk/.editorconfig 3 | # See https://editorconfig.org for more information about the standard. 4 | 5 | # WordPress Coding Standards 6 | # https://make.wordpress.org/core/handbook/coding-standards/ 7 | 8 | root = true 9 | 10 | [*] 11 | charset = utf-8 12 | end_of_line = lf 13 | insert_final_newline = true 14 | trim_trailing_whitespace = true 15 | indent_style = tab 16 | 17 | [{*.yml,*.feature}] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | 24 | [*.txt] 25 | end_of_line = crlf 26 | -------------------------------------------------------------------------------- /.github/workflows/behat-test.yml: -------------------------------------------------------------------------------- 1 | name: Behat Testing 2 | 3 | on: 4 | push: 5 | paths: 6 | - '.github/workflows/behat-test.yml' 7 | - '**.php' 8 | - '**.feature' 9 | - 'behat.yml' 10 | - 'composer.json' 11 | pull_request: 12 | paths: 13 | - '.github/workflows/behat-test.yml' 14 | - '**.php' 15 | - '**.feature' 16 | - 'behat.yml' 17 | - 'composer.json' 18 | types: 19 | - opened 20 | - reopened 21 | - synchronize 22 | 23 | workflow_dispatch: 24 | 25 | jobs: 26 | behat: 27 | uses: automattic/wpvip-plugins-.github/.github/workflows/reusable-behat-test.yml@trunk 28 | -------------------------------------------------------------------------------- /.github/workflows/cs-lint.yml: -------------------------------------------------------------------------------- 1 | name: CS & Lint 2 | 3 | on: 4 | # Run on all pushes and on all pull requests. 5 | # Prevent the "push" build from running when there are only irrelevant changes. 6 | push: 7 | paths-ignore: 8 | - "**.md" 9 | pull_request: 10 | # Allow manually triggering the workflow. 11 | workflow_dispatch: 12 | 13 | jobs: 14 | checkcs: 15 | name: "Basic CS and QA checks" 16 | runs-on: ubuntu-latest 17 | 18 | env: 19 | XMLLINT_INDENT: " " 20 | 21 | steps: 22 | - name: Setup PHP 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: "7.4" 26 | coverage: none 27 | tools: cs2pr 28 | 29 | # Show PHP lint violations inline in the file diff. 30 | # @link https://github.com/marketplace/actions/xmllint-problem-matcher 31 | - name: Register PHP lint violations to appear as file diff comments 32 | uses: korelstar/phplint-problem-matcher@v1 33 | 34 | # Show XML violations inline in the file diff. 35 | # @link https://github.com/marketplace/actions/xmllint-problem-matcher 36 | - name: Register XML violations to appear as file diff comments 37 | uses: korelstar/xmllint-problem-matcher@v1 38 | 39 | - name: Checkout code 40 | uses: actions/checkout@v2 41 | 42 | # Validate the composer.json file. 43 | # @link https://getcomposer.org/doc/03-cli.md#validate 44 | - name: Validate Composer installation 45 | run: composer validate --no-check-all 46 | 47 | # Install dependencies and handle caching in one go. 48 | # @link https://github.com/marketplace/actions/install-composer-dependencies 49 | - name: Install Composer dependencies 50 | uses: ramsey/composer-install@v1 51 | 52 | # Lint PHP. 53 | - name: Lint PHP against parse errors 54 | run: composer lint-ci | cs2pr 55 | 56 | # Needed as runs-on: system doesn't have xml-lint by default. 57 | # @link https://github.com/marketplace/actions/xml-lint 58 | - name: Lint phpunit.xml.dist 59 | uses: ChristophWurst/xmllint-action@v1 60 | with: 61 | xml-file: ./phpunit.xml.dist 62 | xml-schema-file: ./vendor/phpunit/phpunit/phpunit.xsd 63 | 64 | # Check the code-style consistency of the PHP files. 65 | # - name: Check PHP code style 66 | # continue-on-error: true 67 | # run: vendor/bin/phpcs --report-full --report-checkstyle=./phpcs-report.xml 68 | 69 | # - name: Show PHPCS results in PR 70 | # run: cs2pr ./phpcs-report.xml 71 | -------------------------------------------------------------------------------- /.github/workflows/integrations.yml: -------------------------------------------------------------------------------- 1 | name: Run PHPUnit 2 | 3 | on: 4 | # Run on all pushes and on all pull requests. 5 | # Prevent the "push" build from running when there are only irrelevant changes. 6 | push: 7 | paths-ignore: 8 | - "**.md" 9 | pull_request: 10 | # Allow manually triggering the workflow. 11 | workflow_dispatch: 12 | 13 | jobs: 14 | test: 15 | name: WP ${{ matrix.wordpress }} on PHP ${{ matrix.php }} 16 | # Ubuntu-20.x includes MySQL 8.0, which causes `caching_sha2_password` issues with PHP < 7.4 17 | # https://www.php.net/manual/en/mysqli.requirements.php 18 | # TODO: change to ubuntu-latest when we no longer support PHP < 7.4 19 | runs-on: ubuntu-18.04 20 | 21 | env: 22 | WP_VERSION: ${{ matrix.wordpress }} 23 | 24 | strategy: 25 | matrix: 26 | wordpress: ["5.5", "5.6", "5.7"] 27 | php: ["5.6", "7.0", "7.1", "7.2", "7.3", "7.4"] 28 | include: 29 | - php: "8.0" 30 | # Ignore platform requirements, so that PHPUnit 7.5 can be installed on PHP 8.0 (and above). 31 | composer-options: "--ignore-platform-reqs" 32 | extensions: pcov 33 | ini-values: pcov.directory=., "pcov.exclude=\"~(vendor|tests)~\"" 34 | coverage: pcov 35 | exclude: 36 | - php: "8.0" 37 | wordpress: "5.5" 38 | fail-fast: false 39 | 40 | steps: 41 | - name: Checkout code 42 | uses: actions/checkout@v2 43 | 44 | - name: Setup PHP ${{ matrix.php }} 45 | uses: shivammathur/setup-php@v2 46 | with: 47 | php-version: ${{ matrix.php }} 48 | extensions: ${{ matrix.extensions }} 49 | ini-values: ${{ matrix.ini-values }} 50 | coverage: ${{ matrix.coverage }} 51 | 52 | - name: Setup problem matchers for PHP 53 | run: echo "::add-matcher::${{ runner.tool_cache }}/php.json" 54 | 55 | # Setup PCOV since we're using PHPUnit < 8 which has it integrated. Requires PHP 7.1. 56 | # Ignore platform reqs to make it install on PHP 8. 57 | # https://github.com/krakjoe/pcov-clobber 58 | - name: Setup PCOV 59 | if: ${{ matrix.php == 8.0 }} 60 | run: | 61 | composer require pcov/clobber --ignore-platform-reqs 62 | vendor/bin/pcov clobber 63 | 64 | - name: Setup Problem Matchers for PHPUnit 65 | run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 66 | 67 | - name: Install Composer dependencies 68 | uses: ramsey/composer-install@v1 69 | with: 70 | composer-options: "${{ matrix.composer-options }}" 71 | 72 | - name: Start MySQL Service 73 | run: sudo systemctl start mysql.service 74 | 75 | - name: Prepare environment for integration tests 76 | run: composer prepare-ci 77 | 78 | - name: Run integration tests (single site) 79 | if: ${{ matrix.php != 8.0 }} 80 | run: composer test 81 | - name: Run integration tests (single site with code coverage) 82 | if: ${{ matrix.php == 8.0 }} 83 | run: composer coverage-ci 84 | - name: Run integration tests (multisite) 85 | run: composer test-ms 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | /vendor 3 | -------------------------------------------------------------------------------- /.phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | Custom ruleset for media-explorer plugin. 4 | 5 | 6 | 7 | 8 | 9 | . 10 | 12 | /vendor/ 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 31 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Media Explorer 2 | ============== 3 | Media Explorer gives you the ability to insert content from social media 4 | services in your posts. 5 | 6 | Setup 7 | ----- 8 | In order to get this working in your WordPress installation, you have to follow 9 | the next steps: 10 | 11 | * Clone this repo in the plugins folder of your WordPress install with `git 12 | clone https://github.com/Automattic/media-explorer.git`. 13 | * Get your credentials: 14 | * [Twitter](https://dev.twitter.com) 15 | * [Instagram](https://instagram.com/developer). 16 | * [YouTube](https://developers.google.com/youtube/v3/). 17 | * For YouTube, you'll have to create or use an existing project in your [Google Developers Console](https://cloud.google.com/console/project) 18 | * Ensure that this project has the "YouTube Data API v3" API enabled. 19 | * Create and use a public access API Key for your project. 20 | * Write your credentials in [mexp-creds.php](https://github.com/Automattic/media-explorer/blob/master/mexp-creds.php) 21 | * Activate the "MEXP oAuth Creditials" plugin to enable the configured API keys. 22 | * Enjoy! 23 | -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | SKIP_DB_CREATE=${6-false} 14 | 15 | TMPDIR=${TMPDIR-/tmp} 16 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") 17 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} 18 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress} 19 | 20 | download() { 21 | if [ `which curl` ]; then 22 | curl -s "$1" > "$2"; 23 | elif [ `which wget` ]; then 24 | wget -nv -O "$2" "$1" 25 | fi 26 | } 27 | 28 | if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then 29 | WP_BRANCH=${WP_VERSION%\-*} 30 | WP_TESTS_TAG="branches/$WP_BRANCH" 31 | 32 | elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then 33 | WP_TESTS_TAG="branches/$WP_VERSION" 34 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then 35 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 36 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 37 | WP_TESTS_TAG="tags/${WP_VERSION%??}" 38 | else 39 | WP_TESTS_TAG="tags/$WP_VERSION" 40 | fi 41 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 42 | WP_TESTS_TAG="trunk" 43 | else 44 | # http serves a single offer, whereas https serves multiple. we only want one 45 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 46 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 47 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 48 | if [[ -z "$LATEST_VERSION" ]]; then 49 | echo "Latest WordPress version could not be found" 50 | exit 1 51 | fi 52 | WP_TESTS_TAG="tags/$LATEST_VERSION" 53 | fi 54 | set -ex 55 | 56 | install_wp() { 57 | 58 | if [ -d $WP_CORE_DIR ]; then 59 | return; 60 | fi 61 | 62 | mkdir -p $WP_CORE_DIR 63 | 64 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 65 | mkdir -p $TMPDIR/wordpress-trunk 66 | rm -rf $TMPDIR/wordpress-trunk/* 67 | svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress 68 | mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR 69 | else 70 | if [ $WP_VERSION == 'latest' ]; then 71 | local ARCHIVE_NAME='latest' 72 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then 73 | # https serves multiple offers, whereas http serves single. 74 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json 75 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 76 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 77 | LATEST_VERSION=${WP_VERSION%??} 78 | else 79 | # otherwise, scan the releases and get the most up to date minor version of the major release 80 | local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` 81 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) 82 | fi 83 | if [[ -z "$LATEST_VERSION" ]]; then 84 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 85 | else 86 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION" 87 | fi 88 | else 89 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 90 | fi 91 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz 92 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR 93 | fi 94 | 95 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 96 | } 97 | 98 | install_test_suite() { 99 | # portable in-place argument for both GNU sed and Mac OSX sed 100 | if [[ $(uname -s) == 'Darwin' ]]; then 101 | local ioption='-i.bak' 102 | else 103 | local ioption='-i' 104 | fi 105 | 106 | # set up testing suite if it doesn't yet exist 107 | if [ ! -d $WP_TESTS_DIR ]; then 108 | # set up testing suite 109 | mkdir -p $WP_TESTS_DIR 110 | rm -rf $WP_TESTS_DIR/{includes,data} 111 | svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 112 | svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 113 | fi 114 | 115 | if [ ! -f wp-tests-config.php ]; then 116 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 117 | # remove all forward slashes in the end 118 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 119 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 120 | sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 121 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 122 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 123 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 124 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 125 | fi 126 | 127 | } 128 | 129 | recreate_db() { 130 | shopt -s nocasematch 131 | if [[ $1 =~ ^(y|yes)$ ]] 132 | then 133 | mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA 134 | create_db 135 | echo "Recreated the database ($DB_NAME)." 136 | else 137 | echo "Leaving the existing database ($DB_NAME) in place." 138 | fi 139 | shopt -u nocasematch 140 | } 141 | 142 | create_db() { 143 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 144 | } 145 | 146 | install_db() { 147 | 148 | if [ ${SKIP_DB_CREATE} = "true" ]; then 149 | return 0 150 | fi 151 | 152 | # parse DB_HOST for port or socket references 153 | local PARTS=(${DB_HOST//\:/ }) 154 | local DB_HOSTNAME=${PARTS[0]}; 155 | local DB_SOCK_OR_PORT=${PARTS[1]}; 156 | local EXTRA="" 157 | 158 | if ! [ -z $DB_HOSTNAME ] ; then 159 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 160 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 161 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 162 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 163 | elif ! [ -z $DB_HOSTNAME ] ; then 164 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 165 | fi 166 | fi 167 | 168 | # create database 169 | if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ] 170 | then 171 | echo "Reinstalling will delete the existing test database ($DB_NAME)" 172 | read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB 173 | recreate_db $DELETE_EXISTING_DB 174 | else 175 | create_db 176 | fi 177 | } 178 | 179 | install_wp 180 | install_test_suite 181 | install_db 182 | 183 | -------------------------------------------------------------------------------- /class.mexp.php: -------------------------------------------------------------------------------- 1 | services[$service_id] ) ) 55 | return $this->services[$service_id]; 56 | 57 | return new WP_Error( 58 | 'invalid_service', 59 | sprintf( __( 'Media service "%s" was not found', 'mexp' ), esc_html( $service_id ) ) 60 | ); 61 | 62 | } 63 | 64 | /** 65 | * Retrieve all the registered services. 66 | * 67 | * @return array An array of registered Service objects. 68 | */ 69 | public function get_services() { 70 | return $this->services; 71 | } 72 | 73 | /** 74 | * Load the Backbone templates for each of our registered services. 75 | * 76 | * @action print_media_templates 77 | * @return null 78 | */ 79 | public function action_print_media_templates() { 80 | 81 | foreach ( $this->get_services() as $service_id => $service ) { 82 | 83 | if ( ! $template = $service->get_template() ) 84 | continue; 85 | 86 | // apply filters for tabs 87 | $tabs = apply_filters( 'mexp_tabs', array() ); 88 | 89 | # @TODO this list of templates should be somewhere else. where? 90 | foreach ( array( 'search', 'item' ) as $t ) { 91 | 92 | foreach ( $tabs[$service_id] as $tab_id => $tab ) { 93 | 94 | $id = sprintf( 'mexp-%s-%s-%s', 95 | esc_attr( $service_id ), 96 | esc_attr( $t ), 97 | esc_attr( $tab_id ) 98 | ); 99 | 100 | $template->before_template( $id, $tab_id ); 101 | call_user_func( array( $template, $t ), $id, $tab_id ); 102 | $template->after_template( $id, $tab_id ); 103 | 104 | } 105 | 106 | } 107 | 108 | foreach ( array( 'thumbnail' ) as $t ) { 109 | 110 | $id = sprintf( 'mexp-%s-%s', 111 | esc_attr( $service_id ), 112 | esc_attr( $t ) 113 | ); 114 | 115 | $template->before_template( $id ); 116 | call_user_func( array( $template, $t ), $id ); 117 | $template->after_template( $id ); 118 | 119 | } 120 | 121 | } 122 | 123 | } 124 | 125 | /** 126 | * Process an AJAX request and output the resulting JSON. 127 | * 128 | * @action wp_ajax_mexp_request 129 | * @return null 130 | */ 131 | public function ajax_request() { 132 | 133 | if ( !isset( $_POST['_nonce'] ) or !wp_verify_nonce( $_POST['_nonce'], 'mexp_request' ) ) 134 | die( '-1' ); 135 | 136 | $service = $this->get_service( stripslashes( $_POST['service'] ) ); 137 | 138 | if ( is_wp_error( $service ) ) { 139 | do_action( 'mexp_ajax_request_error', $service ); 140 | wp_send_json_error( array( 141 | 'error_code' => $service->get_error_code(), 142 | 'error_message' => $service->get_error_message() 143 | ) ); 144 | } 145 | 146 | foreach ( $service->requires() as $file => $class ) { 147 | 148 | if ( class_exists( $class ) ) 149 | continue; 150 | 151 | require_once sprintf( '%s/class.%s.php', 152 | dirname( __FILE__ ), 153 | $file 154 | ); 155 | 156 | } 157 | 158 | $request = wp_parse_args( stripslashes_deep( $_POST ), array( 159 | 'params' => array(), 160 | 'tab' => null, 161 | 'min_id' => null, 162 | 'max_id' => null, 163 | 'page' => 1, 164 | ) ); 165 | $request['page'] = absint( $request['page'] ); 166 | $request['user_id'] = absint( get_current_user_id() ); 167 | $request = apply_filters( 'mexp_ajax_request_args', $request, $service ); 168 | 169 | $response = $service->request( $request ); 170 | 171 | if ( is_wp_error( $response ) ) { 172 | do_action( 'mexp_ajax_request_error', $response ); 173 | wp_send_json_error( array( 174 | 'error_code' => $response->get_error_code(), 175 | 'error_message' => $response->get_error_message() 176 | ) ); 177 | 178 | } else if ( is_a( $response, 'MEXP_Response' ) ) { 179 | do_action( 'mexp_ajax_request_success', $response ); 180 | wp_send_json_success( $response->output() ); 181 | 182 | } else { 183 | do_action( 'mexp_ajax_request_success', false ); 184 | wp_send_json_success( false ); 185 | 186 | } 187 | 188 | } 189 | 190 | /** 191 | * Enqueue and localise the JS and CSS we need for the media manager. 192 | * 193 | * @action enqueue_media 194 | * @return null 195 | */ 196 | public function action_enqueue_media() { 197 | 198 | $mexp = array( 199 | '_nonce' => wp_create_nonce( 'mexp_request' ), 200 | 'labels' => array( 201 | 'insert' => __( 'Insert into post', 'mexp' ), 202 | 'loadmore' => __( 'Load more', 'mexp' ), 203 | ), 204 | 'base_url' => untrailingslashit( $this->plugin_url() ), 205 | 'admin_url' => untrailingslashit( admin_url() ), 206 | ); 207 | 208 | foreach ( $this->get_services() as $service_id => $service ) { 209 | $service->load(); 210 | 211 | $tabs = apply_filters( 'mexp_tabs', array() ); 212 | $labels = apply_filters( 'mexp_labels', array() ); 213 | 214 | $mexp['services'][$service_id] = array( 215 | 'id' => $service_id, 216 | 'labels' => $labels[$service_id], 217 | 'tabs' => $tabs[$service_id], 218 | ); 219 | } 220 | 221 | // this action enqueues all the statics for each service 222 | do_action( 'mexp_enqueue' ); 223 | 224 | wp_enqueue_script( 225 | 'mexp', 226 | $this->plugin_url( 'js/mexp.js' ), 227 | array( 'jquery', 'media-views' ), 228 | $this->plugin_ver( 'js/mexp.js' ) 229 | ); 230 | 231 | wp_localize_script( 232 | 'mexp', 233 | 'mexp', 234 | $mexp 235 | ); 236 | 237 | wp_enqueue_style( 238 | 'mexp', 239 | $this->plugin_url( 'css/mexp.css' ), 240 | array( /*'wp-admin'*/ ), 241 | $this->plugin_ver( 'css/mexp.css' ) 242 | ); 243 | 244 | } 245 | 246 | /** 247 | * Fire the `mexp_init` action for plugins to hook into. 248 | * 249 | * @action plugins_loaded 250 | * @return null 251 | */ 252 | public function action_plugins_loaded() { 253 | do_action( 'mexp_init' ); 254 | } 255 | 256 | /** 257 | * Load text domain and localisation files. 258 | * Populate the array of Service objects. 259 | * 260 | * @action init 261 | * @return null 262 | */ 263 | public function action_init() { 264 | 265 | load_plugin_textdomain( 'mexp', false, dirname( $this->plugin_base() ) . '/languages/' ); 266 | 267 | foreach ( apply_filters( 'mexp_services', array() ) as $service_id => $service ) { 268 | if ( is_a( $service, 'MEXP_Service' ) ) 269 | $this->services[$service_id] = $service; 270 | } 271 | 272 | } 273 | 274 | /** 275 | * Singleton instantiator. 276 | * 277 | * @param string $file The plugin file (usually __FILE__) (optional) 278 | * @return Media_Explorer 279 | */ 280 | public static function init( $file = null ) { 281 | 282 | static $instance = null; 283 | 284 | if ( !$instance ) 285 | $instance = new Media_Explorer( $file ); 286 | 287 | return $instance; 288 | 289 | } 290 | 291 | } 292 | -------------------------------------------------------------------------------- /class.oauth.php: -------------------------------------------------------------------------------- 1 | key = $key; 21 | $this->secret = $secret; 22 | $this->callback_url = $callback_url; 23 | } 24 | 25 | function __toString() { 26 | return "OAuthConsumer[key=$this->key,secret=$this->secret]"; 27 | } 28 | } 29 | } 30 | 31 | if ( !class_exists( 'OAuthToken' ) ) { 32 | class OAuthToken { 33 | // access tokens and request tokens 34 | public $key; 35 | public $secret; 36 | 37 | /** 38 | * key = the token 39 | * secret = the token secret 40 | */ 41 | function __construct($key, $secret) { 42 | $this->key = $key; 43 | $this->secret = $secret; 44 | } 45 | 46 | /** 47 | * generates the basic string serialization of a token that a server 48 | * would respond to request_token and access_token calls with 49 | */ 50 | function to_string() { 51 | return "oauth_token=" . 52 | OAuthUtil::urlencode_rfc3986($this->key) . 53 | "&oauth_token_secret=" . 54 | OAuthUtil::urlencode_rfc3986($this->secret); 55 | } 56 | 57 | function __toString() { 58 | return $this->to_string(); 59 | } 60 | } 61 | } 62 | 63 | /** 64 | * A class for implementing a Signature Method 65 | * See section 9 ("Signing Requests") in the spec 66 | */ 67 | if ( !class_exists( 'OAuthSignatureMethod' ) ) { 68 | abstract class OAuthSignatureMethod { 69 | /** 70 | * Needs to return the name of the Signature Method (ie HMAC-SHA1) 71 | * @return string 72 | */ 73 | abstract public function get_name(); 74 | 75 | /** 76 | * Build up the signature 77 | * NOTE: The output of this function MUST NOT be urlencoded. 78 | * the encoding is handled in OAuthRequest when the final 79 | * request is serialized 80 | * @param OAuthRequest $request 81 | * @param OAuthConsumer $consumer 82 | * @param OAuthToken $token 83 | * @return string 84 | */ 85 | abstract public function build_signature($request, $consumer, $token); 86 | 87 | /** 88 | * Verifies that a given signature is correct 89 | * @param OAuthRequest $request 90 | * @param OAuthConsumer $consumer 91 | * @param OAuthToken $token 92 | * @param string $signature 93 | * @return bool 94 | */ 95 | public function check_signature($request, $consumer, $token, $signature) { 96 | $built = $this->build_signature($request, $consumer, $token); 97 | return $built == $signature; 98 | } 99 | } 100 | } 101 | 102 | /** 103 | * The HMAC-SHA1 signature method uses the HMAC-SHA1 signature algorithm as defined in [RFC2104] 104 | * where the Signature Base String is the text and the key is the concatenated values (each first 105 | * encoded per Parameter Encoding) of the Consumer Secret and Token Secret, separated by an '&' 106 | * character (ASCII code 38) even if empty. 107 | * - Chapter 9.2 ("HMAC-SHA1") 108 | */ 109 | if ( !class_exists( 'OAuthSignatureMethod_HMAC_SHA1' ) ) { 110 | class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod { 111 | function get_name() { 112 | return "HMAC-SHA1"; 113 | } 114 | 115 | public function build_signature($request, $consumer, $token) { 116 | $base_string = $request->get_signature_base_string(); 117 | $request->base_string = $base_string; 118 | 119 | $key_parts = array( 120 | $consumer->secret, 121 | ($token) ? $token->secret : "" 122 | ); 123 | 124 | $key_parts = OAuthUtil::urlencode_rfc3986($key_parts); 125 | $key = implode('&', $key_parts); 126 | 127 | return base64_encode(hash_hmac('sha1', $base_string, $key, true)); 128 | } 129 | } 130 | } 131 | 132 | /** 133 | * The PLAINTEXT method does not provide any security protection and SHOULD only be used 134 | * over a secure channel such as HTTPS. It does not use the Signature Base String. 135 | * - Chapter 9.4 ("PLAINTEXT") 136 | */ 137 | if ( !class_exists( 'OAuthSignatureMethod_PLAINTEXT' ) ) { 138 | class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod { 139 | public function get_name() { 140 | return "PLAINTEXT"; 141 | } 142 | 143 | /** 144 | * oauth_signature is set to the concatenated encoded values of the Consumer Secret and 145 | * Token Secret, separated by a '&' character (ASCII code 38), even if either secret is 146 | * empty. The result MUST be encoded again. 147 | * - Chapter 9.4.1 ("Generating Signatures") 148 | * 149 | * Please note that the second encoding MUST NOT happen in the SignatureMethod, as 150 | * OAuthRequest handles this! 151 | */ 152 | public function build_signature($request, $consumer, $token) { 153 | $key_parts = array( 154 | $consumer->secret, 155 | ($token) ? $token->secret : "" 156 | ); 157 | 158 | $key_parts = OAuthUtil::urlencode_rfc3986($key_parts); 159 | $key = implode('&', $key_parts); 160 | $request->base_string = $key; 161 | 162 | return $key; 163 | } 164 | } 165 | } 166 | 167 | /** 168 | * The RSA-SHA1 signature method uses the RSASSA-PKCS1-v1_5 signature algorithm as defined in 169 | * [RFC3447] section 8.2 (more simply known as PKCS#1), using SHA-1 as the hash function for 170 | * EMSA-PKCS1-v1_5. It is assumed that the Consumer has provided its RSA public key in a 171 | * verified way to the Service Provider, in a manner which is beyond the scope of this 172 | * specification. 173 | * - Chapter 9.3 ("RSA-SHA1") 174 | */ 175 | if ( !class_exists( 'OAuthSignatureMethod_RSA_SHA1' ) ) { 176 | abstract class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod { 177 | public function get_name() { 178 | return "RSA-SHA1"; 179 | } 180 | 181 | // Up to the SP to implement this lookup of keys. Possible ideas are: 182 | // (1) do a lookup in a table of trusted certs keyed off of consumer 183 | // (2) fetch via http using a url provided by the requester 184 | // (3) some sort of specific discovery code based on request 185 | // 186 | // Either way should return a string representation of the certificate 187 | protected abstract function fetch_public_cert(&$request); 188 | 189 | // Up to the SP to implement this lookup of keys. Possible ideas are: 190 | // (1) do a lookup in a table of trusted certs keyed off of consumer 191 | // 192 | // Either way should return a string representation of the certificate 193 | protected abstract function fetch_private_cert(&$request); 194 | 195 | public function build_signature($request, $consumer, $token) { 196 | $base_string = $request->get_signature_base_string(); 197 | $request->base_string = $base_string; 198 | 199 | // Fetch the private key cert based on the request 200 | $cert = $this->fetch_private_cert($request); 201 | 202 | // Pull the private key ID from the certificate 203 | $privatekeyid = openssl_get_privatekey($cert); 204 | 205 | // Sign using the key 206 | $ok = openssl_sign($base_string, $signature, $privatekeyid); 207 | 208 | // Release the key resource 209 | openssl_free_key($privatekeyid); 210 | 211 | return base64_encode($signature); 212 | } 213 | 214 | public function check_signature($request, $consumer, $token, $signature) { 215 | $decoded_sig = base64_decode($signature); 216 | 217 | $base_string = $request->get_signature_base_string(); 218 | 219 | // Fetch the public key cert based on the request 220 | $cert = $this->fetch_public_cert($request); 221 | 222 | // Pull the public key ID from the certificate 223 | $publickeyid = openssl_get_publickey($cert); 224 | 225 | // Check the computed signature against the one passed in the query 226 | $ok = openssl_verify($base_string, $decoded_sig, $publickeyid); 227 | 228 | // Release the key resource 229 | openssl_free_key($publickeyid); 230 | 231 | return $ok == 1; 232 | } 233 | } 234 | } 235 | 236 | if ( !class_exists( 'OAuthRequest' ) ) { 237 | class OAuthRequest { 238 | private $parameters; 239 | private $http_method; 240 | private $http_url; 241 | // for debug purposes 242 | public $base_string; 243 | public static $version = '1.0'; 244 | public static $POST_INPUT = 'php://input'; 245 | 246 | function __construct($http_method, $http_url, $parameters=NULL) { 247 | @$parameters or $parameters = array(); 248 | $parameters = array_merge( OAuthUtil::parse_parameters(parse_url($http_url, PHP_URL_QUERY)), $parameters); 249 | $this->parameters = $parameters; 250 | $this->http_method = $http_method; 251 | $this->http_url = $http_url; 252 | } 253 | 254 | 255 | /** 256 | * attempt to build up a request from what was passed to the server 257 | */ 258 | public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) { 259 | $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on") 260 | ? 'http' 261 | : 'https'; 262 | @$http_url or $http_url = $scheme . 263 | '://' . $_SERVER['HTTP_HOST'] . 264 | ':' . 265 | $_SERVER['SERVER_PORT'] . 266 | $_SERVER['REQUEST_URI']; 267 | @$http_method or $http_method = $_SERVER['REQUEST_METHOD']; 268 | 269 | // We weren't handed any parameters, so let's find the ones relevant to 270 | // this request. 271 | // If you run XML-RPC or similar you should use this to provide your own 272 | // parsed parameter-list 273 | if (!$parameters) { 274 | // Find request headers 275 | $request_headers = OAuthUtil::get_headers(); 276 | 277 | // Parse the query-string to find GET parameters 278 | $parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']); 279 | 280 | // It's a POST request of the proper content-type, so parse POST 281 | // parameters and add those overriding any duplicates from GET 282 | if ($http_method == "POST" 283 | && @strstr($request_headers["Content-Type"], 284 | "application/x-www-form-urlencoded") 285 | ) { 286 | $post_data = OAuthUtil::parse_parameters( 287 | file_get_contents(self::$POST_INPUT) 288 | ); 289 | $parameters = array_merge($parameters, $post_data); 290 | } 291 | 292 | // We have a Authorization-header with OAuth data. Parse the header 293 | // and add those overriding any duplicates from GET or POST 294 | if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") { 295 | $header_parameters = OAuthUtil::split_header( 296 | $request_headers['Authorization'] 297 | ); 298 | $parameters = array_merge($parameters, $header_parameters); 299 | } 300 | 301 | } 302 | 303 | return new OAuthRequest($http_method, $http_url, $parameters); 304 | } 305 | 306 | /** 307 | * pretty much a helper function to set up the request 308 | */ 309 | public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) { 310 | @$parameters or $parameters = array(); 311 | $defaults = array("oauth_version" => OAuthRequest::$version, 312 | "oauth_nonce" => OAuthRequest::generate_nonce(), 313 | "oauth_timestamp" => OAuthRequest::generate_timestamp(), 314 | "oauth_consumer_key" => $consumer->key); 315 | if ($token) 316 | $defaults['oauth_token'] = $token->key; 317 | 318 | $parameters = array_merge($defaults, $parameters); 319 | 320 | return new OAuthRequest($http_method, $http_url, $parameters); 321 | } 322 | 323 | public function set_parameter($name, $value, $allow_duplicates = true) { 324 | if ($allow_duplicates && isset($this->parameters[$name])) { 325 | // We have already added parameter(s) with this name, so add to the list 326 | if (is_scalar($this->parameters[$name])) { 327 | // This is the first duplicate, so transform scalar (string) 328 | // into an array so we can add the duplicates 329 | $this->parameters[$name] = array($this->parameters[$name]); 330 | } 331 | 332 | $this->parameters[$name][] = $value; 333 | } else { 334 | $this->parameters[$name] = $value; 335 | } 336 | } 337 | 338 | public function get_parameter($name) { 339 | return isset($this->parameters[$name]) ? $this->parameters[$name] : null; 340 | } 341 | 342 | public function get_parameters() { 343 | return $this->parameters; 344 | } 345 | 346 | public function unset_parameter($name) { 347 | unset($this->parameters[$name]); 348 | } 349 | 350 | /** 351 | * The request parameters, sorted and concatenated into a normalized string. 352 | * @return string 353 | */ 354 | public function get_signable_parameters() { 355 | // Grab all parameters 356 | $params = $this->parameters; 357 | 358 | // Remove oauth_signature if present 359 | // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.") 360 | if (isset($params['oauth_signature'])) { 361 | unset($params['oauth_signature']); 362 | } 363 | 364 | return OAuthUtil::build_http_query($params); 365 | } 366 | 367 | /** 368 | * Returns the base string of this request 369 | * 370 | * The base string defined as the method, the url 371 | * and the parameters (normalized), each urlencoded 372 | * and the concated with &. 373 | */ 374 | public function get_signature_base_string() { 375 | $parts = array( 376 | $this->get_normalized_http_method(), 377 | $this->get_normalized_http_url(), 378 | $this->get_signable_parameters() 379 | ); 380 | 381 | $parts = OAuthUtil::urlencode_rfc3986($parts); 382 | 383 | return implode('&', $parts); 384 | } 385 | 386 | /** 387 | * just uppercases the http method 388 | */ 389 | public function get_normalized_http_method() { 390 | return strtoupper($this->http_method); 391 | } 392 | 393 | /** 394 | * parses the url and rebuilds it to be 395 | * scheme://host/path 396 | */ 397 | public function get_normalized_http_url() { 398 | $parts = parse_url($this->http_url); 399 | 400 | $port = @$parts['port']; 401 | $scheme = $parts['scheme']; 402 | $host = $parts['host']; 403 | $path = @$parts['path']; 404 | 405 | $port or $port = ($scheme == 'https') ? '443' : '80'; 406 | 407 | if (($scheme == 'https' && $port != '443') 408 | || ($scheme == 'http' && $port != '80')) { 409 | $host = "$host:$port"; 410 | } 411 | return "$scheme://$host$path"; 412 | } 413 | 414 | /** 415 | * builds a url usable for a GET request 416 | */ 417 | public function to_url() { 418 | $post_data = $this->to_postdata(); 419 | $out = $this->get_normalized_http_url(); 420 | 421 | if ($post_data) { 422 | $out .= '?'.$post_data; 423 | } 424 | return $out; 425 | } 426 | 427 | /** 428 | * builds the data one would send in a POST request 429 | */ 430 | public function to_postdata() { 431 | return OAuthUtil::build_http_query($this->parameters); 432 | } 433 | 434 | /** 435 | * builds the Authorization: header 436 | */ 437 | public function to_header($realm=null) { 438 | $first = true; 439 | if($realm) { 440 | $out = 'Authorization: OAuth realm="' . OAuthUtil::urlencode_rfc3986($realm) . '"'; 441 | $first = false; 442 | } else 443 | $out = 'Authorization: OAuth'; 444 | 445 | $total = array(); 446 | foreach ($this->parameters as $k => $v) { 447 | if (substr($k, 0, 5) != "oauth") continue; 448 | if (is_array($v)) { 449 | throw new OAuthException('Arrays not supported in headers'); 450 | } 451 | $out .= ($first) ? ' ' : ','; 452 | $out .= OAuthUtil::urlencode_rfc3986($k) . 453 | '="' . 454 | OAuthUtil::urlencode_rfc3986($v) . 455 | '"'; 456 | $first = false; 457 | } 458 | return $out; 459 | } 460 | 461 | public function __toString() { 462 | return $this->to_url(); 463 | } 464 | 465 | 466 | public function sign_request($signature_method, $consumer, $token) { 467 | $this->set_parameter( 468 | "oauth_signature_method", 469 | $signature_method->get_name(), 470 | false 471 | ); 472 | $signature = $this->build_signature($signature_method, $consumer, $token); 473 | $this->set_parameter("oauth_signature", $signature, false); 474 | } 475 | 476 | public function build_signature($signature_method, $consumer, $token) { 477 | $signature = $signature_method->build_signature($this, $consumer, $token); 478 | return $signature; 479 | } 480 | 481 | /** 482 | * util function: current timestamp 483 | */ 484 | private static function generate_timestamp() { 485 | return time(); 486 | } 487 | 488 | /** 489 | * util function: current nonce 490 | */ 491 | private static function generate_nonce() { 492 | $mt = microtime(); 493 | $rand = mt_rand(); 494 | 495 | return md5($mt . $rand); // md5s look nicer than numbers 496 | } 497 | } 498 | } 499 | 500 | if ( !class_exists( 'OAuthServer' ) ) { 501 | class OAuthServer { 502 | protected $timestamp_threshold = 300; // in seconds, five minutes 503 | protected $version = '1.0'; // hi blaine 504 | protected $signature_methods = array(); 505 | 506 | protected $data_store; 507 | 508 | function __construct($data_store) { 509 | $this->data_store = $data_store; 510 | } 511 | 512 | public function add_signature_method($signature_method) { 513 | $this->signature_methods[$signature_method->get_name()] = 514 | $signature_method; 515 | } 516 | 517 | // high level functions 518 | 519 | /** 520 | * process a request_token request 521 | * returns the request token on success 522 | */ 523 | public function fetch_request_token(&$request) { 524 | $this->get_version($request); 525 | 526 | $consumer = $this->get_consumer($request); 527 | 528 | // no token required for the initial token request 529 | $token = NULL; 530 | 531 | $this->check_signature($request, $consumer, $token); 532 | 533 | // Rev A change 534 | $callback = $request->get_parameter('oauth_callback'); 535 | $new_token = $this->data_store->new_request_token($consumer, $callback); 536 | 537 | return $new_token; 538 | } 539 | 540 | /** 541 | * process an access_token request 542 | * returns the access token on success 543 | */ 544 | public function fetch_access_token(&$request) { 545 | $this->get_version($request); 546 | 547 | $consumer = $this->get_consumer($request); 548 | 549 | // requires authorized request token 550 | $token = $this->get_token($request, $consumer, "request"); 551 | 552 | $this->check_signature($request, $consumer, $token); 553 | 554 | // Rev A change 555 | $verifier = $request->get_parameter('oauth_verifier'); 556 | $new_token = $this->data_store->new_access_token($token, $consumer, $verifier); 557 | 558 | return $new_token; 559 | } 560 | 561 | /** 562 | * verify an api call, checks all the parameters 563 | */ 564 | public function verify_request(&$request) { 565 | $this->get_version($request); 566 | $consumer = $this->get_consumer($request); 567 | $token = $this->get_token($request, $consumer, "access"); 568 | $this->check_signature($request, $consumer, $token); 569 | return array($consumer, $token); 570 | } 571 | 572 | // Internals from here 573 | /** 574 | * version 1 575 | */ 576 | private function get_version(&$request) { 577 | $version = $request->get_parameter("oauth_version"); 578 | if (!$version) { 579 | // Service Providers MUST assume the protocol version to be 1.0 if this parameter is not present. 580 | // Chapter 7.0 ("Accessing Protected Ressources") 581 | $version = '1.0'; 582 | } 583 | if ($version !== $this->version) { 584 | throw new OAuthException("OAuth version '$version' not supported"); 585 | } 586 | return $version; 587 | } 588 | 589 | /** 590 | * figure out the signature with some defaults 591 | */ 592 | private function get_signature_method(&$request) { 593 | $signature_method = 594 | @$request->get_parameter("oauth_signature_method"); 595 | 596 | if (!$signature_method) { 597 | // According to chapter 7 ("Accessing Protected Ressources") the signature-method 598 | // parameter is required, and we can't just fallback to PLAINTEXT 599 | throw new OAuthException('No signature method parameter. This parameter is required'); 600 | } 601 | 602 | if (!in_array($signature_method, 603 | array_keys($this->signature_methods))) { 604 | throw new OAuthException( 605 | "Signature method '$signature_method' not supported " . 606 | "try one of the following: " . 607 | implode(", ", array_keys($this->signature_methods)) 608 | ); 609 | } 610 | return $this->signature_methods[$signature_method]; 611 | } 612 | 613 | /** 614 | * try to find the consumer for the provided request's consumer key 615 | */ 616 | private function get_consumer(&$request) { 617 | $consumer_key = @$request->get_parameter("oauth_consumer_key"); 618 | if (!$consumer_key) { 619 | throw new OAuthException("Invalid consumer key"); 620 | } 621 | 622 | $consumer = $this->data_store->lookup_consumer($consumer_key); 623 | if (!$consumer) { 624 | throw new OAuthException("Invalid consumer"); 625 | } 626 | 627 | return $consumer; 628 | } 629 | 630 | /** 631 | * try to find the token for the provided request's token key 632 | */ 633 | private function get_token(&$request, $consumer, $token_type="access") { 634 | $token_field = @$request->get_parameter('oauth_token'); 635 | $token = $this->data_store->lookup_token( 636 | $consumer, $token_type, $token_field 637 | ); 638 | if (!$token) { 639 | throw new OAuthException("Invalid $token_type token: $token_field"); 640 | } 641 | return $token; 642 | } 643 | 644 | /** 645 | * all-in-one function to check the signature on a request 646 | * should guess the signature method appropriately 647 | */ 648 | private function check_signature(&$request, $consumer, $token) { 649 | // this should probably be in a different method 650 | $timestamp = @$request->get_parameter('oauth_timestamp'); 651 | $nonce = @$request->get_parameter('oauth_nonce'); 652 | 653 | $this->check_timestamp($timestamp); 654 | $this->check_nonce($consumer, $token, $nonce, $timestamp); 655 | 656 | $signature_method = $this->get_signature_method($request); 657 | 658 | $signature = $request->get_parameter('oauth_signature'); 659 | $valid_sig = $signature_method->check_signature( 660 | $request, 661 | $consumer, 662 | $token, 663 | $signature 664 | ); 665 | 666 | if (!$valid_sig) { 667 | throw new OAuthException("Invalid signature"); 668 | } 669 | } 670 | 671 | /** 672 | * check that the timestamp is new enough 673 | */ 674 | private function check_timestamp($timestamp) { 675 | if( ! $timestamp ) 676 | throw new OAuthException( 677 | 'Missing timestamp parameter. The parameter is required' 678 | ); 679 | 680 | // verify that timestamp is recentish 681 | $now = time(); 682 | if (abs($now - $timestamp) > $this->timestamp_threshold) { 683 | throw new OAuthException( 684 | "Expired timestamp, yours $timestamp, ours $now" 685 | ); 686 | } 687 | } 688 | 689 | /** 690 | * check that the nonce is not repeated 691 | */ 692 | private function check_nonce($consumer, $token, $nonce, $timestamp) { 693 | if( ! $nonce ) 694 | throw new OAuthException( 695 | 'Missing nonce parameter. The parameter is required' 696 | ); 697 | 698 | // verify that the nonce is uniqueish 699 | $found = $this->data_store->lookup_nonce( 700 | $consumer, 701 | $token, 702 | $nonce, 703 | $timestamp 704 | ); 705 | if ($found) { 706 | throw new OAuthException("Nonce already used: $nonce"); 707 | } 708 | } 709 | 710 | } 711 | 712 | class OAuthDataStore { 713 | function lookup_consumer($consumer_key) { 714 | // implement me 715 | } 716 | 717 | function lookup_token($consumer, $token_type, $token) { 718 | // implement me 719 | } 720 | 721 | function lookup_nonce($consumer, $token, $nonce, $timestamp) { 722 | // implement me 723 | } 724 | 725 | function new_request_token($consumer, $callback = null) { 726 | // return a new token attached to this consumer 727 | } 728 | 729 | function new_access_token($token, $consumer, $verifier = null) { 730 | // return a new access token attached to this consumer 731 | // for the user associated with this token if the request token 732 | // is authorized 733 | // should also invalidate the request token 734 | } 735 | 736 | } 737 | } 738 | 739 | if ( !class_exists( 'OAuthUtil' ) ) { 740 | class OAuthUtil { 741 | public static function urlencode_rfc3986($input) { 742 | if (is_array($input)) { 743 | return array_map(array('OAuthUtil', 'urlencode_rfc3986'), $input); 744 | } else if (is_scalar($input)) { 745 | return str_replace( 746 | '+', 747 | ' ', 748 | str_replace('%7E', '~', rawurlencode($input)) 749 | ); 750 | } else { 751 | return ''; 752 | } 753 | } 754 | 755 | 756 | // This decode function isn't taking into consideration the above 757 | // modifications to the encoding process. However, this method doesn't 758 | // seem to be used anywhere so leaving it as is. 759 | public static function urldecode_rfc3986($string) { 760 | return urldecode($string); 761 | } 762 | 763 | // Utility function for turning the Authorization: header into 764 | // parameters, has to do some unescaping 765 | // Can filter out any non-oauth parameters if needed (default behaviour) 766 | public static function split_header($header, $only_allow_oauth_parameters = true) { 767 | $pattern = '/(([-_a-z]*)=("([^"]*)"|([^,]*)),?)/'; 768 | $offset = 0; 769 | $params = array(); 770 | while (preg_match($pattern, $header, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) { 771 | $match = $matches[0]; 772 | $header_name = $matches[2][0]; 773 | $header_content = (isset($matches[5])) ? $matches[5][0] : $matches[4][0]; 774 | if (preg_match('/^oauth_/', $header_name) || !$only_allow_oauth_parameters) { 775 | $params[$header_name] = OAuthUtil::urldecode_rfc3986($header_content); 776 | } 777 | $offset = $match[1] + strlen($match[0]); 778 | } 779 | 780 | if (isset($params['realm'])) { 781 | unset($params['realm']); 782 | } 783 | 784 | return $params; 785 | } 786 | 787 | // helper to try to sort out headers for people who aren't running apache 788 | public static function get_headers() { 789 | if (function_exists('apache_request_headers')) { 790 | // we need this to get the actual Authorization: header 791 | // because apache tends to tell us it doesn't exist 792 | $headers = apache_request_headers(); 793 | 794 | // sanitize the output of apache_request_headers because 795 | // we always want the keys to be Cased-Like-This and arh() 796 | // returns the headers in the same case as they are in the 797 | // request 798 | $out = array(); 799 | foreach( $headers AS $key => $value ) { 800 | $key = str_replace( 801 | " ", 802 | "-", 803 | ucwords(strtolower(str_replace("-", " ", $key))) 804 | ); 805 | $out[$key] = $value; 806 | } 807 | } else { 808 | // otherwise we don't have apache and are just going to have to hope 809 | // that $_SERVER actually contains what we need 810 | $out = array(); 811 | if( isset($_SERVER['CONTENT_TYPE']) ) 812 | $out['Content-Type'] = $_SERVER['CONTENT_TYPE']; 813 | if( isset($_ENV['CONTENT_TYPE']) ) 814 | $out['Content-Type'] = $_ENV['CONTENT_TYPE']; 815 | 816 | foreach ($_SERVER as $key => $value) { 817 | if (substr($key, 0, 5) == "HTTP_") { 818 | // this is chaos, basically it is just there to capitalize the first 819 | // letter of every word that is not an initial HTTP and strip HTTP 820 | // code from przemek 821 | $key = str_replace( 822 | " ", 823 | "-", 824 | ucwords(strtolower(str_replace("_", " ", substr($key, 5)))) 825 | ); 826 | $out[$key] = $value; 827 | } 828 | } 829 | } 830 | return $out; 831 | } 832 | 833 | // This function takes a input like a=b&a=c&d=e and returns the parsed 834 | // parameters like this 835 | // array('a' => array('b','c'), 'd' => 'e') 836 | public static function parse_parameters( $input ) { 837 | if (!isset($input) || !$input) return array(); 838 | 839 | $pairs = explode('&', $input); 840 | 841 | $parsed_parameters = array(); 842 | foreach ($pairs as $pair) { 843 | $split = explode('=', $pair, 2); 844 | $parameter = OAuthUtil::urldecode_rfc3986($split[0]); 845 | $value = isset($split[1]) ? OAuthUtil::urldecode_rfc3986($split[1]) : ''; 846 | 847 | if (isset($parsed_parameters[$parameter])) { 848 | // We have already recieved parameter(s) with this name, so add to the list 849 | // of parameters with this name 850 | 851 | if (is_scalar($parsed_parameters[$parameter])) { 852 | // This is the first duplicate, so transform scalar (string) into an array 853 | // so we can add the duplicates 854 | $parsed_parameters[$parameter] = array($parsed_parameters[$parameter]); 855 | } 856 | 857 | $parsed_parameters[$parameter][] = $value; 858 | } else { 859 | $parsed_parameters[$parameter] = $value; 860 | } 861 | } 862 | return $parsed_parameters; 863 | } 864 | 865 | public static function build_http_query($params) { 866 | if (!$params) return ''; 867 | // Urlencode both keys and values 868 | $keys = OAuthUtil::urlencode_rfc3986(array_keys($params)); 869 | $values = OAuthUtil::urlencode_rfc3986(array_values($params)); 870 | $params = array_combine($keys, $values); 871 | // Parameters are sorted by name, using lexicographical byte value ordering. 872 | // Ref: Spec: 9.1.1 (1) 873 | uksort($params, 'strcmp'); 874 | 875 | $pairs = array(); 876 | foreach ($params as $parameter => $value) { 877 | if (is_array($value)) { 878 | // If two or more parameters share the same name, they are sorted by their value 879 | // Ref: Spec: 9.1.1 (1) 880 | natsort($value); 881 | foreach ($value as $duplicate_value) { 882 | $pairs[] = $parameter . '=' . $duplicate_value; 883 | } 884 | } else { 885 | $pairs[] = $parameter . '=' . $value; 886 | } 887 | } 888 | // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61) 889 | // Each name-value pair is separated by an '&' character (ASCII code 38) 890 | return implode('&', $pairs); 891 | } 892 | } 893 | } 894 | -------------------------------------------------------------------------------- /class.plugin.php: -------------------------------------------------------------------------------- 1 | file = $file; 28 | } 29 | 30 | /** 31 | * Returns the URL for for a file/dir within this plugin. 32 | * 33 | * @param string $path The path within this plugin, e.g. '/js/clever-fx.js' 34 | * @return string URL 35 | * @author John Blackbourn 36 | */ 37 | public function plugin_url( $file = '' ) { 38 | return $this->_plugin( 'url', $file ); 39 | } 40 | 41 | /** 42 | * Returns the filesystem path for a file/dir within this plugin. 43 | * 44 | * @param string $path The path within this plugin, e.g. '/js/clever-fx.js' 45 | * @return string Filesystem path 46 | * @author John Blackbourn 47 | */ 48 | public function plugin_path( $file = '' ) { 49 | return $this->_plugin( 'path', $file ); 50 | } 51 | 52 | /** 53 | * Returns a version number for the given plugin file. 54 | * 55 | * @param string $path The path within this plugin, e.g. '/js/clever-fx.js' 56 | * @return string Version 57 | * @author John Blackbourn 58 | */ 59 | public function plugin_ver( $file ) { 60 | return filemtime( $this->plugin_path( $file ) ); 61 | } 62 | 63 | /** 64 | * Returns the current plugin's basename, eg. 'my_plugin/my_plugin.php'. 65 | * 66 | * @return string Basename 67 | * @author John Blackbourn 68 | */ 69 | public function plugin_base() { 70 | return $this->_plugin( 'base' ); 71 | } 72 | 73 | /** 74 | * Populates the current plugin info if necessary, and returns the requested item. 75 | * 76 | * @param string $item The name of the requested item. One of 'url', 'path', or 'base'. 77 | * @param string $file The file name to append to the returned value (optional). 78 | * @return string The value of the requested item. 79 | * @author John Blackbourn 80 | */ 81 | protected function _plugin( $item, $file = '' ) { 82 | if ( !isset( $this->plugin ) ) { 83 | $this->plugin = array( 84 | 'url' => plugin_dir_url( $this->file ), 85 | 'path' => plugin_dir_path( $this->file ), 86 | 'base' => plugin_basename( $this->file ) 87 | ); 88 | } 89 | return $this->plugin[$item] . ltrim( $file, '/' ); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /class.response.php: -------------------------------------------------------------------------------- 1 | null, 24 | 'max_id' => null, 25 | 'min_id' => null, 26 | ); 27 | 28 | /** 29 | * Add a meta value to the response. Accepts a key/value pair or an associative array. 30 | * 31 | * @param string|array $key The meta key, or an associative array of meta keys/values. 32 | * @param mixed $value The meta value. 33 | * @return null 34 | */ 35 | public function add_meta( $key, $value = null ) { 36 | 37 | if ( is_array( $key ) ) { 38 | 39 | foreach ( $key as $k => $v ) 40 | $this->meta[$k] = $v; 41 | 42 | } else { 43 | 44 | $this->meta[$key] = $value; 45 | 46 | } 47 | 48 | } 49 | 50 | /** 51 | * Add a response item to the response. 52 | * 53 | * @param Response_Item A response item. 54 | * @return null 55 | */ 56 | public function add_item( MEXP_Response_Item $item ) { 57 | $this->items[] = $item; 58 | } 59 | 60 | /** 61 | * Retrieve the response output. 62 | * 63 | * @return array|bool The response output, or boolean false if there's nothing to output. 64 | */ 65 | public function output() { 66 | 67 | if ( empty( $this->items ) ) 68 | return false; 69 | 70 | if ( is_null( $this->meta['count'] ) ) 71 | $this->meta['count'] = count( $this->items ); 72 | if ( is_null( $this->meta['min_id'] ) ) 73 | $this->meta['min_id'] = reset( $this->items )->id; 74 | 75 | $output = array( 76 | 'meta' => $this->meta, 77 | 'items' => array() 78 | ); 79 | 80 | foreach ( $this->items as $item ) 81 | $output['items'][] = $item->output(); 82 | 83 | return $output; 84 | 85 | } 86 | 87 | } 88 | 89 | /** 90 | * Response Item class. Used within the Response class to populate the items in a response. 91 | */ 92 | final class MEXP_Response_Item { 93 | 94 | public $id = null; 95 | public $url = null; 96 | public $thumbnail = null; 97 | public $content = null; 98 | public $date = null; 99 | public $date_format = null; 100 | public $meta = array(); 101 | 102 | /** 103 | * Set the ID for the response item. 104 | * 105 | * @param int $id The response item ID. 106 | * @return null 107 | */ 108 | public function set_id( $id ) { 109 | $this->id = $id; 110 | } 111 | 112 | /** 113 | * Set the URL for the response item. 114 | * 115 | * @param string $url The response item URL. 116 | * @return null 117 | */ 118 | public function set_url( $url ) { 119 | $this->url = esc_url_raw( $url ); 120 | } 121 | 122 | /** 123 | * Set the thumbnail URL for the response item. 124 | * 125 | * @param string $thumbnail The response item thumbnail URL. 126 | * @return null 127 | */ 128 | public function set_thumbnail( $thumbnail ) { 129 | $this->thumbnail = esc_url_raw( $thumbnail ); 130 | } 131 | 132 | /** 133 | * Set the content for the response item. 134 | * 135 | * @param string $content The response item content. 136 | * @return null 137 | */ 138 | public function set_content( $content ) { 139 | $this->content = $content; 140 | } 141 | 142 | /** 143 | * Set the date for the response item. 144 | * 145 | * @param int $date The response item date in UNIX timestamp format. 146 | * @return null 147 | */ 148 | public function set_date( $date ) { 149 | $this->date = $date; 150 | } 151 | 152 | /** 153 | * Set the date format for the response item date. 154 | * 155 | * @param string $date_format The date format in PHP date() format. 156 | * @return null 157 | */ 158 | public function set_date_format( $date_format ) { 159 | $this->date_format = $date_format; 160 | } 161 | 162 | /** 163 | * Add a meta value to the response item. Accepts a key/value pair or an associative array. 164 | * 165 | * @param string|array $key The meta key, or an associative array of meta keys/values. 166 | * @param mixed $value The meta value. 167 | * @return null 168 | */ 169 | public function add_meta( $key, $value = null ) { 170 | 171 | if ( is_array( $key ) ) { 172 | 173 | foreach ( $key as $k => $v ) 174 | $this->meta[$k] = $v; 175 | 176 | } else { 177 | 178 | $this->meta[$key] = $value; 179 | 180 | } 181 | 182 | } 183 | 184 | /** 185 | * Retrieve the response item output. 186 | * 187 | * @return array The response item output. 188 | */ 189 | public function output() { 190 | 191 | if ( is_null( $this->date_format ) ) 192 | $this->date_format = get_option( 'date_format' ); 193 | 194 | return array( 195 | 'id' => $this->id, 196 | 'url' => $this->url, 197 | 'thumbnail' => $this->thumbnail, 198 | 'content' => $this->content, 199 | 'date' => date( $this->date_format, $this->date ), 200 | 'meta' => $this->meta, 201 | ); 202 | 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /class.service.php: -------------------------------------------------------------------------------- 1 | template = $template; 71 | 72 | } 73 | 74 | /** 75 | * Returns the template object for this service. 76 | * 77 | * @return Template|null A Template object, or null if a template isn't set. 78 | */ 79 | final public function get_template() { 80 | 81 | return $this->template; 82 | 83 | } 84 | 85 | } 86 | 87 | -------------------------------------------------------------------------------- /class.template.php: -------------------------------------------------------------------------------- 1 | 55 | 69 | =5.6", 19 | "composer/installers": "~1.0" 20 | }, 21 | "require-dev": { 22 | "automattic/vipwpcs": "^2.2", 23 | "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7", 24 | "php-parallel-lint/php-parallel-lint": "^1.0", 25 | "phpcompatibility/phpcompatibility-wp": "^2.1", 26 | "phpunit/phpunit": "^4 || ^5 || ^6 || ^7", 27 | "squizlabs/php_codesniffer": "^3.5", 28 | "wp-coding-standards/wpcs": "^2.3.0", 29 | "yoast/phpunit-polyfills": "^0.2.0" 30 | }, 31 | "scripts": { 32 | "cbf": [ 33 | "@php ./vendor/bin/phpcbf" 34 | ], 35 | "coverage": [ 36 | "@php ./vendor/bin/phpunit --coverage-html ./build/coverage-html" 37 | ], 38 | "coverage-ci": [ 39 | "@php ./vendor/bin/phpunit" 40 | ], 41 | "cs": [ 42 | "@php ./vendor/bin/phpcs" 43 | ], 44 | "lint": [ 45 | "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git" 46 | ], 47 | "lint-ci": [ 48 | "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git --checkstyle" 49 | ], 50 | "prepare-ci": [ 51 | "bash bin/install-wp-tests.sh wordpress_test root root localhost" 52 | ], 53 | "test": [ 54 | "@php ./vendor/bin/phpunit --testsuite WP_Tests" 55 | ], 56 | "test-ms": [ 57 | "@putenv WP_MULTISITE=1", 58 | "@composer test" 59 | ] 60 | }, 61 | "support": { 62 | "issues": "https://github.com/Automattic/media-explorer/issues", 63 | "source": "https://github.com/Automattic/media-explorer" 64 | }, 65 | "config": { 66 | "allow-plugins": { 67 | "composer/installers": true, 68 | "dealerdirect/phpcodesniffer-composer-installer": true 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /css/mexp.css: -------------------------------------------------------------------------------- 1 | /* 2 | This program is free software; you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation; either version 2 of the License, or 5 | (at your option) any later version. 6 | 7 | This program is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | */ 13 | 14 | .media-frame .mexp-content { 15 | background: #fff; 16 | } 17 | 18 | .media-frame .mexp-content .mexp-error { 19 | background: #fdd; 20 | padding: 12px 16px; 21 | margin: 0 0 10px 0; 22 | bottom: auto; 23 | display: none; 24 | } 25 | 26 | .media-frame .mexp-content .mexp-empty { 27 | background: #ffd; 28 | padding: 12px 16px; 29 | margin: 0 0 10px 0; 30 | bottom: auto; 31 | display: none; 32 | } 33 | 34 | .media-frame .mexp-content .mexp-toolbar { 35 | background: #f5f5f5; 36 | padding: 0; 37 | right: 0; 38 | } 39 | body.mp6 .media-frame .mexp-content .mexp-toolbar { 40 | background: #fff; 41 | border-bottom: 1px solid #ddd; 42 | } 43 | 44 | .media-frame .mexp-content .mexp-toolbar-container { 45 | padding: 10px 9px; 46 | } 47 | 48 | .media-frame .mexp-content .attachments { 49 | right: 0; 50 | } 51 | 52 | .media-frame .mexp-content .mexp-toolbar label, 53 | .media-frame .mexp-content .mexp-toolbar select, 54 | .media-frame .mexp-content .mexp-toolbar input { 55 | float: left; 56 | } 57 | 58 | .media-frame .mexp-content .media-toolbar .mexp-input-search { 59 | margin-left: 10px; 60 | } 61 | 62 | /* WP media manager style tweaks: */ 63 | .media-frame .mexp-content .mexp-toolbar label{ 64 | margin: 7px; 65 | color: #555; 66 | } 67 | .media-frame .mexp-content .mexp-toolbar select { 68 | margin: 1px; 69 | padding: 6px 6px 5px; 70 | height: 2.5em; 71 | } 72 | .media-frame .mexp-content .mexp-toolbar .button { 73 | margin: 1px; 74 | } 75 | 76 | .media-frame .mexp-content .mexp-toolbar .spinner { 77 | float: left; 78 | margin: 7px 10px; 79 | } 80 | 81 | .media-frame .mexp-content .mexp-pagination .spinner { 82 | float: left; 83 | margin: 3px 10px; 84 | } 85 | 86 | .media-frame .mexp-content .clearfix:after { 87 | clear: both; 88 | content: "."; 89 | display: block; 90 | height: 0; 91 | visibility: hidden; 92 | } 93 | 94 | .button.mexp-pagination { 95 | margin-top: 15px; 96 | } 97 | 98 | /* ************************** */ 99 | /* Generic media item styles: */ 100 | /* ************************** */ 101 | 102 | .media-frame .mexp-content .mexp-items { 103 | padding-top: 10px; 104 | } 105 | 106 | .media-frame .mexp-content .mexp-item { 107 | cursor: pointer; 108 | border: 1px solid #fff; 109 | } 110 | 111 | .media-frame .mexp-content .mexp-item:hover { 112 | border-color: #999; 113 | } 114 | 115 | .media-frame .mexp-content .mexp-item.selected { 116 | border-color: transparent; 117 | } 118 | 119 | html.ie8 .media-frame .mexp-content .mexp-item.selected { 120 | border-color: #999; 121 | } 122 | 123 | /* ************************ */ 124 | /* Twitter-specific styles: */ 125 | /* ************************ */ 126 | 127 | .media-frame .mexp-content-twitter .mexp-item { 128 | float: none; 129 | max-width: 800px; 130 | text-align: left; 131 | margin: 6px 10px; 132 | } 133 | 134 | .media-frame .mexp-content-twitter .mexp-item .mexp-item-container { 135 | padding: 13px; 136 | } 137 | 138 | .media-frame .mexp-content-twitter .mexp-item .mexp-item-thumb { 139 | float: left; 140 | margin: 0 10px 10px 0; 141 | } 142 | 143 | .media-frame .mexp-content-twitter .mexp-item .mexp-item-thumb img { 144 | width: 48px; 145 | height: 48px; 146 | } 147 | 148 | .media-frame .mexp-content-twitter .mexp-item .mexp-item-author { 149 | font-size: 18px; 150 | } 151 | 152 | .media-frame .mexp-content-twitter .mexp-item .mexp-item-author-screen-name { 153 | color: #bbb; 154 | } 155 | 156 | .media-frame .mexp-content-twitter .mexp-item .mexp-item-content { 157 | margin: 8px 0; 158 | } 159 | 160 | .media-frame .mexp-content-twitter .mexp-item .mexp-item-date { 161 | color: #bbb; 162 | clear: left; 163 | } 164 | 165 | .media-frame .mexp-content-twitter-location .mexp-toolbar { 166 | height: 210px; 167 | } 168 | 169 | .media-frame .mexp-content-twitter-location .attachments { 170 | top: 210px; 171 | } 172 | 173 | #mexp_twitter_map_canvas { 174 | width: 100%; 175 | height: 160px; 176 | background: transparent no-repeat center center; 177 | } 178 | 179 | #twitter-loadmore { 180 | display: none; 181 | } 182 | 183 | 184 | /* ************************ */ 185 | /* YouTube-specific styles: */ 186 | /* ************************ */ 187 | 188 | #youtube-loadmore { 189 | display: none !important; 190 | } 191 | 192 | .media-frame .mexp-item-youtube .mexp-item-main { 193 | max-height: 48px; 194 | overflow: hidden; 195 | } 196 | 197 | .media-frame .mexp-item-youtube .mexp-item-content { 198 | font-weight: bold; 199 | } 200 | 201 | .media-frame .spinner-bottom { 202 | bottom: 18px; 203 | position: absolute; 204 | margin: auto; 205 | left: 50%; 206 | } 207 | 208 | .mexp-item-thumb img { 209 | width: 99%; 210 | } 211 | 212 | /* 213 | * Fixes the 180px right margin for the ul.attachments in lo-res revices 214 | * */ 215 | @media (max-width: 782px) { 216 | .mp6 .mexp-content-youtube ul.mexp-items, .mp6 .mexp-content-youtube ul.mexp-items { 217 | margin-right: 0px; 218 | overflow: hidden; 219 | } 220 | } 221 | 222 | /** 223 | * One column layout 224 | * */ 225 | @media (max-width: 699px) { 226 | .mexp-content-youtube .attachment { 227 | width: 100%; 228 | } 229 | .media-frame .mexp-content .media-toolbar .mexp-input-search { 230 | width: 128px; 231 | } 232 | } 233 | 234 | /** 235 | * Two column layout 236 | * */ 237 | @media (min-width: 700px) and (max-width: 1199px) { 238 | .mexp-content-youtube .attachment { 239 | width: 45%; 240 | } 241 | } 242 | 243 | /** 244 | * Three column layout 245 | * */ 246 | @media (min-width: 1200px) and (max-width: 1499px) { 247 | .mexp-content-youtube .attachment { 248 | width: 31%; 249 | } 250 | } 251 | 252 | /** 253 | * Four column layout 254 | * */ 255 | @media (min-width: 1500px) { 256 | .mexp-content-youtube .attachment { 257 | width: 23%; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /js/mexp.js: -------------------------------------------------------------------------------- 1 | /* 2 | This program is free software; you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation; either version 2 of the License, or 5 | (at your option) any later version. 6 | 7 | This program is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | */ 13 | 14 | var media = wp.media; 15 | 16 | // VIEW: MEDIA ITEM: 17 | 18 | media.view.MEXPItem = wp.Backbone.View.extend({ 19 | 20 | tagName : 'li', 21 | className : 'mexp-item attachment', 22 | 23 | render: function() { 24 | 25 | this.template = media.template( 'mexp-' + this.options.service.id + '-item-' + this.options.tab ); 26 | this.$el.html( this.template( this.model.toJSON() ) ); 27 | 28 | return this; 29 | 30 | } 31 | 32 | }); 33 | 34 | // VIEW - BOTTOM TOOLBAR 35 | 36 | media.view.Toolbar.MEXP = media.view.Toolbar.extend({ 37 | 38 | initialize: function() { 39 | 40 | _.defaults( this.options, { 41 | event : 'inserter', 42 | close : false, 43 | items : { 44 | // See wp.media.view.Button 45 | inserter : { 46 | id : 'mexp-button', 47 | style : 'primary', 48 | text : mexp.labels.insert, 49 | priority : 80, 50 | click : function() { 51 | this.controller.state().mexpInsert(); 52 | } 53 | } 54 | } 55 | }); 56 | 57 | media.view.Toolbar.prototype.initialize.apply( this, arguments ); 58 | 59 | var serviceName = this.controller.state().id.replace( /mexp-service-/g, ''); 60 | 61 | this.set( 'pagination', new media.view.Button({ 62 | tagName: 'button', 63 | classes: 'mexp-pagination button button-secondary', 64 | id: serviceName + '-loadmore', 65 | text: mexp.labels.loadmore, 66 | priority: -20, 67 | }) ); 68 | }, 69 | 70 | refresh: function() { 71 | 72 | var selection = this.controller.state().props.get( '_all' ).get( 'selection' ); 73 | 74 | // @TODO i think this is redundant 75 | this.get( 'inserter' ).model.set( 'disabled', !selection.length ); 76 | 77 | media.view.Toolbar.prototype.refresh.apply( this, arguments ); 78 | 79 | } 80 | 81 | }); 82 | 83 | // VIEW - MEDIA CONTENT AREA 84 | 85 | media.view.MEXP = media.View.extend({ 86 | 87 | events: { 88 | 'click .mexp-item-area' : 'toggleSelectionHandler', 89 | 'click .mexp-item .check' : 'removeSelectionHandler', 90 | 'submit .mexp-toolbar form' : 'updateInput' 91 | }, 92 | 93 | initialize: function() { 94 | 95 | /* fired when you switch router tabs */ 96 | 97 | var _this = this; 98 | 99 | this.collection = new Backbone.Collection(); 100 | this.service = this.options.service; 101 | this.tab = this.options.tab; 102 | 103 | this.createToolbar(); 104 | this.clearItems(); 105 | 106 | if ( this.model.get( 'items' ) ) { 107 | 108 | this.collection = new Backbone.Collection(); 109 | this.collection.reset( this.model.get( 'items' ) ); 110 | 111 | jQuery( '#' + this.service.id + '-loadmore' ).attr( 'disabled', false ).show(); 112 | } else { 113 | jQuery( '#' + this.service.id + '-loadmore' ).hide(); 114 | } 115 | 116 | // @TODO do this somewhere else: 117 | // @TODO this gets reverted anyway when the button model's disabled state changes. look into it. 118 | //jQuery( '#mexp-button' ).text( this.service.labels.insert ); 119 | 120 | this.collection.on( 'reset', this.render, this ); 121 | 122 | this.model.on( 'change:params', this.changedParams, this ); 123 | 124 | this.on( 'loading', this.loading, this ); 125 | this.on( 'loaded', this.loaded, this ); 126 | this.on( 'change:params', this.changedParams, this ); 127 | this.on( 'change:page', this.changedPage, this ); 128 | 129 | jQuery( '.mexp-pagination' ).click( function( event ) { 130 | _this.paginate( event ); 131 | } ); 132 | 133 | if ( _this.model.get( 'fetchOnRender' ) ) { 134 | _this.model.set( 'fetchOnRender', false ); 135 | _this.fetchItems(); 136 | } 137 | 138 | }, 139 | 140 | render: function() { 141 | 142 | /* fired when you switch router tabs */ 143 | 144 | var selection = this.getSelection(); 145 | 146 | if ( this.collection && this.collection.models.length ) { 147 | 148 | this.clearItems(); 149 | 150 | var container = document.createDocumentFragment(); 151 | 152 | this.collection.each( function( model ) { 153 | container.appendChild( this.renderItem( model ) ); 154 | }, this ); 155 | 156 | this.$el.find( '.mexp-items' ).append( container ); 157 | 158 | } 159 | 160 | selection.each( function( model ) { 161 | var id = '#mexp-item-' + this.service.id + '-' + this.tab + '-' + model.get( 'id' ); 162 | this.$el.find( id ).closest( '.mexp-item' ).addClass( 'selected details' ); 163 | }, this ); 164 | 165 | jQuery( '#mexp-button' ).prop( 'disabled', !selection.length ); 166 | 167 | return this; 168 | 169 | }, 170 | 171 | renderItem : function( model ) { 172 | 173 | var view = new media.view.MEXPItem({ 174 | model : model, 175 | service : this.service, 176 | tab : this.tab 177 | }); 178 | 179 | return view.render().el; 180 | 181 | }, 182 | 183 | createToolbar: function() { 184 | 185 | // @TODO this could be a separate view: 186 | html = '
'; 187 | this.$el.prepend( html ); 188 | 189 | // @TODO this could be a separate view: 190 | html = '
'; 191 | this.$el.prepend( html ); 192 | 193 | // @TODO this could be a separate view: 194 | html = '
    '; 195 | this.$el.append( html ); 196 | 197 | // @TODO this could be a separate view: 198 | var toolbar_template = media.template( 'mexp-' + this.service.id + '-search-' + this.tab ); 199 | html = '
    ' + toolbar_template( this.model.toJSON() ) + '
    '; 200 | this.$el.prepend( html ); 201 | 202 | }, 203 | 204 | removeSelectionHandler: function( event ) { 205 | 206 | var target = jQuery( '#' + event.currentTarget.id ); 207 | var id = target.attr( 'data-id' ); 208 | 209 | this.removeFromSelection( target, id ); 210 | 211 | event.preventDefault(); 212 | 213 | }, 214 | 215 | toggleSelectionHandler: function( event ) { 216 | 217 | if ( event.target.href ) 218 | return; 219 | 220 | var target = jQuery( '#' + event.currentTarget.id ); 221 | var id = target.attr( 'data-id' ); 222 | 223 | if ( this.getSelection().get( id ) ) 224 | this.removeFromSelection( target, id ); 225 | else 226 | this.addToSelection( target, id ); 227 | 228 | }, 229 | 230 | addToSelection: function( target, id ) { 231 | 232 | target.closest( '.mexp-item' ).addClass( 'selected details' ); 233 | 234 | this.getSelection().add( this.collection._byId[id] ); 235 | 236 | // @TODO why isn't this triggered by the above line? 237 | this.controller.state().props.trigger( 'change:selection' ); 238 | 239 | }, 240 | 241 | removeFromSelection: function( target, id ) { 242 | 243 | target.closest( '.mexp-item' ).removeClass( 'selected details' ); 244 | 245 | this.getSelection().remove( this.collection._byId[id] ); 246 | 247 | // @TODO why isn't this triggered by the above line? 248 | this.controller.state().props.trigger( 'change:selection' ); 249 | 250 | }, 251 | 252 | clearSelection: function() { 253 | this.getSelection().reset(); 254 | }, 255 | 256 | getSelection : function() { 257 | return this.controller.state().props.get( '_all' ).get( 'selection' ); 258 | }, 259 | 260 | clearItems: function() { 261 | 262 | this.$el.find( '.mexp-item' ).removeClass( 'selected details' ); 263 | this.$el.find( '.mexp-items' ).empty(); 264 | this.$el.find( '.mexp-pagination' ).hide(); 265 | 266 | }, 267 | 268 | loading: function() { 269 | 270 | // show spinner 271 | this.$el.find( '.spinner' ).addClass( 'is-active' ); 272 | 273 | // hide messages 274 | this.$el.find( '.mexp-error' ).hide().text(''); 275 | this.$el.find( '.mexp-empty' ).hide().text(''); 276 | 277 | // disable 'load more' button 278 | jQuery( '#' + this.service.id + '-loadmore' ).attr( 'disabled', true ); 279 | }, 280 | 281 | loaded: function( response ) { 282 | 283 | // hide spinner 284 | this.$el.find( '.spinner' ).removeClass( 'is-active' ); 285 | 286 | }, 287 | 288 | fetchItems: function() { 289 | 290 | this.trigger( 'loading' ); 291 | 292 | var data = { 293 | _nonce : mexp._nonce, 294 | service : this.service.id, 295 | tab : this.tab, 296 | params : this.model.get( 'params' ), 297 | page : this.model.get( 'page' ), 298 | max_id : this.model.get( 'max_id' ) 299 | }; 300 | 301 | media.ajax( 'mexp_request', { 302 | context : this, 303 | success : this.fetchedSuccess, 304 | error : this.fetchedError, 305 | data : data 306 | } ); 307 | 308 | }, 309 | 310 | fetchedSuccess: function( response ) { 311 | 312 | if ( !this.model.get( 'page' ) ) { 313 | 314 | if ( !response.items ) { 315 | this.fetchedEmpty( response ); 316 | return; 317 | } 318 | 319 | this.model.set( 'min_id', response.meta.min_id ); 320 | this.model.set( 'items', response.items ); 321 | 322 | this.collection.reset( response.items ); 323 | 324 | } else { 325 | 326 | if ( !response.items ) { 327 | this.moreEmpty( response ); 328 | return; 329 | } 330 | 331 | this.model.set( 'items', this.model.get( 'items' ).concat( response.items ) ); 332 | 333 | var collection = new Backbone.Collection( response.items ); 334 | var container = document.createDocumentFragment(); 335 | 336 | this.collection.add( collection.models ); 337 | 338 | collection.each( function( model ) { 339 | container.appendChild( this.renderItem( model ) ); 340 | }, this ); 341 | 342 | this.$el.find( '.mexp-items' ).append( container ); 343 | 344 | } 345 | 346 | jQuery( '#' + this.service.id + '-loadmore' ).attr( 'disabled', false ).show(); 347 | this.model.set( 'max_id', response.meta.max_id ); 348 | 349 | this.trigger( 'loaded loaded:success', response ); 350 | 351 | }, 352 | 353 | fetchedEmpty: function( response ) { 354 | 355 | this.$el.find( '.mexp-empty' ).text( this.service.labels.noresults ).show(); 356 | this.$el.find( '.mexp-pagination' ).hide(); 357 | 358 | this.trigger( 'loaded loaded:noresults', response ); 359 | 360 | }, 361 | 362 | fetchedError: function( response ) { 363 | 364 | this.$el.find( '.mexp-error' ).text( response.error_message ).show(); 365 | jQuery( '#' + this.service.id + '-loadmore' ).attr( 'disabled', false ).show(); 366 | this.trigger( 'loaded loaded:error', response ); 367 | 368 | }, 369 | 370 | updateInput: function( event ) { 371 | 372 | // triggered when a search is submitted 373 | 374 | var params = this.model.get( 'params' ); 375 | var els = this.$el.find( '.mexp-toolbar' ).find( ':input' ).each( function( k, el ) { 376 | var n = jQuery(this).attr('name'); 377 | if ( n ) 378 | params[n] = jQuery(this).val(); 379 | } ); 380 | 381 | this.clearSelection(); 382 | jQuery( '#mexp-button' ).attr( 'disabled', 'disabled' ); 383 | this.model.set( 'params', params ); 384 | this.trigger( 'change:params' ); // why isn't this triggering automatically? might be because params is an object 385 | 386 | event.preventDefault(); 387 | 388 | }, 389 | 390 | paginate : function( event ) { 391 | 392 | if( 0 == this.collection.length ) 393 | return; 394 | 395 | var page = this.model.get( 'page' ) || 1; 396 | 397 | this.model.set( 'page', page + 1 ); 398 | this.trigger( 'change:page' ); 399 | 400 | event.preventDefault(); 401 | 402 | }, 403 | 404 | changedPage: function() { 405 | 406 | // triggered when the pagination is changed 407 | 408 | this.fetchItems(); 409 | 410 | }, 411 | 412 | changedParams: function() { 413 | 414 | // triggered when the search parameters are changed 415 | 416 | this.model.set( 'page', null ); 417 | this.model.set( 'min_id', null ); 418 | this.model.set( 'max_id', null ); 419 | 420 | this.clearItems(); 421 | this.fetchItems(); 422 | 423 | } 424 | 425 | }); 426 | 427 | // VIEW - MEDIA FRAME (MENU BAR) 428 | 429 | var post_frame = media.view.MediaFrame.Post; 430 | 431 | media.view.MediaFrame.Post = post_frame.extend({ 432 | 433 | initialize: function() { 434 | 435 | post_frame.prototype.initialize.apply( this, arguments ); 436 | 437 | _.each( mexp.services, function( service, service_id ) { 438 | 439 | var id = 'mexp-service-' + service.id; 440 | var controller = { 441 | id : id, 442 | router : id + '-router', 443 | toolbar : id + '-toolbar', 444 | menu : 'default', 445 | title : service.labels.title, 446 | tabs : service.tabs, 447 | priority: 100 // places it above Insert From URL 448 | }; 449 | 450 | for ( var tab in service.tabs ) { 451 | 452 | // Content 453 | this.on( 'content:render:' + id + '-content-' + tab, _.bind( this.mexpContentRender, this, service, tab ) ); 454 | 455 | // Set the default tab 456 | if ( service.tabs[tab].defaultTab ) 457 | controller.content = id + '-content-' + tab; 458 | 459 | } 460 | 461 | this.states.add([ 462 | new media.controller.MEXP( controller ) 463 | ]); 464 | 465 | // Tabs 466 | this.on( 'router:create:' + id + '-router', this.createRouter, this ); 467 | this.on( 'router:render:' + id + '-router', _.bind( this.mexpRouterRender, this, service ) ); 468 | 469 | // Toolbar 470 | this.on( 'toolbar:create:' + id + '-toolbar', this.mexpToolbarCreate, this ); 471 | //this.on( 'toolbar:render:' + id + '-toolbar', _.bind( this.mexpToolbarRender, this, service ) ); 472 | 473 | }, this ); 474 | 475 | }, 476 | 477 | mexpRouterRender : function( service, view ) { 478 | 479 | var id = 'mexp-service-' + service.id; 480 | var tabs = {}; 481 | 482 | for ( var tab in service.tabs ) { 483 | tab_id = id + '-content-' + tab; 484 | tabs[tab_id] = { 485 | text : service.tabs[tab].text 486 | }; 487 | } 488 | 489 | view.set( tabs ); 490 | 491 | }, 492 | 493 | mexpToolbarRender : function( service, view ) { 494 | 495 | view.set( 'selection', new media.view.Selection.MEXP({ 496 | service : service, 497 | controller : this, 498 | collection : this.state().props.get('_all').get('selection'), 499 | priority : -40 500 | }).render() ); 501 | 502 | }, 503 | 504 | mexpContentRender : function( service, tab ) { 505 | 506 | /* called when a tab becomes active */ 507 | 508 | this.content.set( new media.view.MEXP( { 509 | service : service, 510 | controller : this, 511 | model : this.state().props.get( tab ), 512 | tab : tab, 513 | className : 'clearfix attachments-browser mexp-content mexp-content-' + service.id + ' mexp-content-' + service.id + '-' + tab 514 | } ) ); 515 | 516 | }, 517 | 518 | mexpToolbarCreate : function( toolbar ) { 519 | 520 | toolbar.view = new media.view.Toolbar.MEXP( { 521 | controller : this 522 | } ); 523 | 524 | } 525 | 526 | }); 527 | 528 | // CONTROLLER: 529 | 530 | media.controller.MEXP = media.controller.State.extend({ 531 | 532 | initialize: function( options ) { 533 | 534 | this.props = new Backbone.Collection(); 535 | 536 | for ( var tab in options.tabs ) { 537 | 538 | this.props.add( new Backbone.Model({ 539 | id : tab, 540 | params : {}, 541 | page : null, 542 | min_id : null, 543 | max_id : null, 544 | fetchOnRender : options.tabs[ tab ].fetchOnRender, 545 | }) ); 546 | 547 | } 548 | 549 | this.props.add( new Backbone.Model({ 550 | id : '_all', 551 | selection : new Backbone.Collection() 552 | }) ); 553 | 554 | this.props.on( 'change:selection', this.refresh, this ); 555 | 556 | }, 557 | 558 | refresh: function() { 559 | this.frame.toolbar.get().refresh(); 560 | }, 561 | 562 | mexpInsert: function() { 563 | 564 | var selection = this.frame.content.get().getSelection(), 565 | urls = []; 566 | 567 | selection.each( function( model ) { 568 | urls.push( model.get( 'url' ) ); 569 | }, this ); 570 | 571 | if ( typeof(tinymce) === 'undefined' || tinymce.activeEditor === null || tinymce.activeEditor.isHidden() ) { 572 | media.editor.insert( _.toArray( urls ).join( "\n\n" ) ); 573 | } else { 574 | media.editor.insert( "

    " + _.toArray( urls ).join( "

    " ) + "

    " ); 575 | } 576 | 577 | selection.reset(); 578 | this.frame.close(); 579 | 580 | } 581 | 582 | }); 583 | -------------------------------------------------------------------------------- /media-explorer.php: -------------------------------------------------------------------------------- 1 | '', 27 | 'consumer_secret' => '', 28 | 'oauth_token' => '', 29 | 'oauth_token_secret' => '' 30 | ); 31 | 32 | } 33 | 34 | add_filter( 'mexp_youtube_developer_key', 'mexp_youtube_developer_key_callback' ); 35 | 36 | function mexp_youtube_developer_key_callback() { 37 | 38 | // Add your developer key here. 39 | // Get your developer key at: 40 | return ''; 41 | 42 | } 43 | 44 | add_filter( 'mexp_instagram_credentials', 'mexp_instagram_credentials_callback' ); 45 | 46 | function mexp_instagram_credentials_callback( $credentials ) { 47 | 48 | // Add your developer key here. 49 | // Get your developer key at: 50 | return array( 51 | 'access_token' => '', 52 | ); 53 | 54 | } 55 | -------------------------------------------------------------------------------- /mexp-keyring-user-creds.php: -------------------------------------------------------------------------------- 1 | get_service_by_name( 'instagram' ); 31 | if ( is_null( $keyring ) ) { 32 | return $credentials; 33 | } 34 | 35 | $keyring_store = Keyring::init()->get_token_store(); 36 | 37 | // Hacky time, Keyring is designed to handle requests, but we're just stealing its access_token. 38 | if ( method_exists( $keyring_store, 'get_tokens_by_user' ) ) { 39 | 40 | // The wpcom version uses the get_tokens_by_user method 41 | $users_tokens = $keyring_store->get_tokens_by_user( get_current_user_id() ); 42 | 43 | if ( in_array( 'instagram', $users_tokens ) ) { 44 | $credentials['access_token'] = $users_tokens['instagram'][0]->token; 45 | } 46 | 47 | } elseif ( method_exists( $keyring_store, 'get_tokens' ) ) { 48 | 49 | // The released version uses the get_tokens method 50 | $users_tokens = $keyring_store->get_tokens( 51 | array( 52 | 'service' => 'instagram', 53 | 'user_id' => get_current_user_id(), 54 | ) 55 | ); 56 | 57 | if ( count( $users_tokens ) > 0 ) { 58 | $credentials['access_token'] = $users_tokens[0]->token; 59 | } 60 | 61 | } 62 | 63 | return $credentials; 64 | 65 | } 66 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Media Explorer === 2 | Contributors: Automattic, johnbillion, garhdez, djpaul 3 | Tags: media, social media, twitter, youtube, media explorer 4 | Stable tag: 1.2 5 | 6 | Insert social media content into your posts. 7 | 8 | == Description == 9 | A new media curation tool that lets you add trending content from Twitter and YouTube without ever leaving your post editor! 10 | 11 | == Upgrade notice == 12 | 13 | = 1.2 (Sep. 3, 2013 ) = 14 | * We've found where the pagination button for the Twitter service was hiding! You can now "get more" results for the same Twitter search. [29] 15 | * Added a readme for the WordPress plugin, and the Github repo. [32] 16 | * Twitter re-tweets have been hidden from search results. [40] 17 | 18 | = 1.1 (Aug. ?, 2013) = 19 | * Unreleased internal version. 20 | 21 | = 1.0 (Aug. 28, 2013) = 22 | * First stable release 23 | -------------------------------------------------------------------------------- /services/instagram/js.js: -------------------------------------------------------------------------------- 1 | /* 2 | This program is free software; you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation; either version 2 of the License, or 5 | (at your option) any later version. 6 | 7 | This program is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | */ 13 | 14 | // EMPTY SOURCE FILE IS EMPTY 15 | -------------------------------------------------------------------------------- /services/instagram/service.php: -------------------------------------------------------------------------------- 1 | set_template( new MEXP_Instagram_Template ); 28 | 29 | } 30 | 31 | public function load() { 32 | 33 | add_action( 'mexp_enqueue', array( $this, 'enqueue_statics' ) ); 34 | 35 | add_filter( 'mexp_tabs', array( $this, 'tabs' ), 10, 1 ); 36 | 37 | add_filter( 'mexp_labels', array( $this, 'labels' ), 10, 1 ); 38 | } 39 | 40 | public function enqueue_statics() { 41 | 42 | $mexp = Media_Explorer::init(); 43 | 44 | wp_enqueue_script( 45 | 'mexp-service-instagram', 46 | $mexp->plugin_url( 'services/instagram/js.js' ), 47 | array( 'jquery', 'mexp' ), 48 | $mexp->plugin_ver( 'services/instagram/js.js' ) 49 | ); 50 | 51 | wp_enqueue_style( 52 | 'mexp-service-instagram-css', 53 | $mexp->plugin_url( 'services/instagram/style.css' ), 54 | array(), 55 | $mexp->plugin_ver( 'services/instagram/style.css' ) 56 | ); 57 | 58 | } 59 | 60 | public function request( array $request ) { 61 | $params = $request['params']; 62 | $tab = $request['tab']; 63 | 64 | $query_params = array(); 65 | 66 | if ( isset( $params['q'] ) ) { 67 | $q = $query_params['q'] = sanitize_title_with_dashes( $params['q'] ); 68 | } 69 | 70 | switch ( $tab ) { 71 | case 'tag': 72 | $endpoint = "tags/{$q}/media/recent"; 73 | break; 74 | 75 | case 'by_user': 76 | $user_id = $this->get_user_id( $q ); 77 | $endpoint = "users/{$user_id}/media/recent"; 78 | break; 79 | 80 | case 'mine': 81 | $credentials = $this->get_user_credentials(); 82 | $query_params['access_token'] = $credentials['access_token']; 83 | $endpoint = 'users/self/media/recent'; 84 | break; 85 | 86 | case 'feed': 87 | $credentials = $this->get_user_credentials(); 88 | $query_params['access_token'] = $credentials['access_token']; 89 | $endpoint = 'users/self/feed'; 90 | break; 91 | 92 | case 'popular': 93 | default: 94 | $endpoint = 'media/popular'; 95 | } 96 | 97 | if ( !empty( $request['max_id'] ) ) { 98 | $query_params['max_id'] = $request['max_id']; 99 | } 100 | 101 | $response = $this->do_request( $endpoint, $query_params ); 102 | 103 | if ( is_wp_error( $response ) ) { 104 | 105 | return $response; 106 | 107 | } elseif ( 200 == $response['code'] || 400 == $response['code'] ) { 108 | 109 | return $this->response( $response ); 110 | 111 | } else { 112 | 113 | return new WP_Error( 114 | 'mexp_instagram_failed_request', 115 | sprintf( __( 'Could not connect to Instagram (error %s).', 'mexp' ), 116 | esc_html( $response['code'] ) 117 | ) 118 | ); 119 | 120 | } 121 | 122 | } 123 | 124 | public function do_request( $endpoint, $params = array() ) { 125 | $host = 'https://api.instagram.com'; 126 | $version = 'v1'; 127 | if ( !isset( $params['access_token'] ) ) { 128 | $credentials = $this->get_generic_credentials(); 129 | 130 | if ( ! isset( $credentials['access_token'] ) ) { 131 | return new WP_Error( 132 | 'mexp_instagram_no_connection', 133 | __( 'oAuth connection to Instagram not found.', 'mexp' ) 134 | ); 135 | } 136 | 137 | $params['access_token'] = $credentials['access_token']; 138 | } 139 | 140 | $url = add_query_arg( $params, "$host/$version/$endpoint/" ); 141 | 142 | $response = wp_remote_get( $url ); 143 | 144 | $code = wp_remote_retrieve_response_code( $response ); 145 | 146 | $data = array(); 147 | if ( 200 == $code ) { 148 | $data = json_decode( wp_remote_retrieve_body( $response ) ); 149 | } 150 | 151 | return array( 152 | 'code' => $code, 153 | 'data' => $data, 154 | ); 155 | 156 | } 157 | 158 | public function get_user_id( $username ) { 159 | $response = $this->do_request( 'users/search', array( 'q' => $username ) ); 160 | 161 | if ( ! is_wp_error( $response ) && 200 == $response['code'] ) { 162 | foreach ( $response['data']->data as $user ) { 163 | if ( $user->username == $username ) { 164 | return $user->id; 165 | } 166 | } 167 | } 168 | 169 | return 0; 170 | } 171 | 172 | public function response( $r ) { 173 | 174 | if ( empty( $r['data'] ) ) { 175 | return false; 176 | } 177 | 178 | $response = new MEXP_Response; 179 | 180 | foreach ( $r['data']->data as $result ) { 181 | $item = new MEXP_Response_Item; 182 | 183 | $item->set_id( $result->id ); 184 | $item->set_url( $result->link ); 185 | 186 | // Not all results have a caption 187 | if ( is_object( $result->caption ) ) { 188 | $item->set_content( $result->caption->text ); 189 | } 190 | 191 | $item->set_thumbnail( set_url_scheme( $result->images->thumbnail->url ) ); 192 | $item->set_date( $result->created_time ); 193 | $item->set_date_format( 'g:i A - j M y' ); 194 | 195 | $item->add_meta( 'user', array( 196 | 'username' => $result->user->username, 197 | ) ); 198 | 199 | $response->add_item( $item ); 200 | 201 | } 202 | 203 | // Pagination details 204 | if ( !empty( $r['data']->pagination ) ) { 205 | if ( isset( $r['data']->pagination->next_max_id ) ) { 206 | $response->add_meta( 'max_id', $r['data']->pagination->next_max_id ); 207 | } 208 | 209 | if ( isset( $r['data']->pagination->next_min_id ) ) { 210 | $response->add_meta( 'min_id', $r['data']->pagination->next_min_id ); 211 | } 212 | } 213 | 214 | return $response; 215 | 216 | } 217 | 218 | public function tabs( array $tabs ) { 219 | $tabs['instagram'] = array(); 220 | 221 | $user_creds = $this->get_user_credentials(); 222 | if ( ! empty( $user_creds ) ) { 223 | $tabs['instagram']['mine'] = array( 224 | 'text' => _x( 'My Instagrams', 'Tab title', 'mexp' ), 225 | 'defaultTab' => true, 226 | 'fetchOnRender' => true, 227 | ); 228 | $tabs['instagram']['feed'] = array( 229 | 'text' => _x( 'My Feed', 'Tab title', 'mexp' ), 230 | 'fetchOnRender' => true, 231 | ); 232 | } 233 | 234 | $tabs['instagram']['popular'] = array( 235 | 'text' => _x( 'Browse Popular', 'Tab title', 'mexp'), 236 | 'defaultTab' => empty( $tabs['instagram'] ), 237 | 'fetchOnRender' => true, 238 | ); 239 | $tabs['instagram']['tag'] = array( 240 | 'text' => _x( 'With Tag', 'Tab title', 'mexp'), 241 | ); 242 | $tabs['instagram']['by_user'] = array( 243 | 'text' => _x( 'By User', 'Tab title', 'mexp'), 244 | ); 245 | 246 | return $tabs; 247 | } 248 | 249 | public function labels( array $labels ) { 250 | $labels['instagram'] = array( 251 | 'title' => __( 'Insert Instagram', 'mexp' ), 252 | // @TODO the 'insert' button text gets reset when selecting items. find out why. 253 | 'insert' => __( 'Insert Instagram', 'mexp' ), 254 | 'noresults' => __( 'No pics matched your search query', 'mexp' ), 255 | 'loadmore' => __( 'Load more pics', 'mexp' ), 256 | ); 257 | 258 | return $labels; 259 | } 260 | 261 | private function get_generic_credentials() { 262 | 263 | if ( is_null( $this->generic_credentials ) ) { 264 | $this->generic_credentials = (array) apply_filters( 'mexp_instagram_credentials', array() ); 265 | } 266 | 267 | return $this->generic_credentials; 268 | 269 | } 270 | 271 | private function get_user_credentials() { 272 | 273 | if ( is_null( $this->user_credentials ) ) { 274 | $this->user_credentials = (array) apply_filters( 'mexp_instagram_user_credentials', array() ); 275 | } 276 | 277 | return $this->user_credentials; 278 | 279 | } 280 | 281 | } 282 | 283 | add_filter( 'mexp_services', 'mexp_service_instagram' ); 284 | 285 | function mexp_service_instagram( array $services ) { 286 | $services['instagram'] = new MEXP_Instagram_Service; 287 | 288 | return $services; 289 | } 290 | -------------------------------------------------------------------------------- /services/instagram/style.css: -------------------------------------------------------------------------------- 1 | .mexp-content-instagram .attachment { 2 | width: 200px; 3 | } 4 | 5 | .mexp-content-instagram .attachment .mexp-item-instagram { 6 | height: 235px; 7 | overflow: hidden; 8 | padding: 10px; 9 | } 10 | 11 | .mexp-content-instagram .mexp-item-thumb img { 12 | width: 150px; 13 | height: 150px; 14 | } 15 | 16 | .mexp-content-instagram .description { 17 | float: left; 18 | margin: 7px 10px; 19 | } 20 | -------------------------------------------------------------------------------- /services/instagram/template.php: -------------------------------------------------------------------------------- 1 | 9 |
    10 |
    11 |
    12 | 13 |
    14 |
    15 |
    16 | {{ data.meta.user.username }}{{ data.content ? ':' : '' }} {{ data.content }} 17 |
    18 |
    19 | {{ data.date }} 20 |
    21 |
    22 |
    23 |
    24 | 25 |
    26 |
    27 | 36 |
    37 | 45 | 46 | 47 |
    48 |
    49 | 55 |
    56 | 64 | 65 | 66 |
    67 |
    68 | 73 |
    74 | 75 | 76 |
    77 |
    78 | 83 |
    84 | 85 | 86 |
    87 |
    88 | 93 |
    94 | 95 | 96 |
    97 |
    98 | sha1_method = new OAuthSignatureMethod_HMAC_SHA1(); 49 | $this->consumer = new OAuthConsumer($consumer_key, $consumer_secret); 50 | if (!empty($oauth_token) && !empty($oauth_token_secret)) { 51 | $this->token = new OAuthConsumer($oauth_token, $oauth_token_secret); 52 | } else { 53 | $this->token = NULL; 54 | } 55 | } 56 | 57 | 58 | /** 59 | * Get a request_token from Twitter 60 | * 61 | * @returns a key/value array containing oauth_token and oauth_token_secret 62 | */ 63 | function getRequestToken($oauth_callback) { 64 | $parameters = array(); 65 | $parameters['oauth_callback'] = $oauth_callback; 66 | $request = $this->oAuthRequest($this->requestTokenURL(), 'GET', $parameters); 67 | $token = OAuthUtil::parse_parameters($request); 68 | $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']); 69 | return $token; 70 | } 71 | 72 | /** 73 | * Get the authorize URL 74 | * 75 | * @returns a string 76 | */ 77 | function getAuthorizeURL($token, $sign_in_with_twitter = TRUE) { 78 | if (is_array($token)) { 79 | $token = $token['oauth_token']; 80 | } 81 | if (empty($sign_in_with_twitter)) { 82 | return $this->authorizeURL() . "?oauth_token={$token}"; 83 | } else { 84 | return $this->authenticateURL() . "?oauth_token={$token}"; 85 | } 86 | } 87 | 88 | /** 89 | * Exchange request token and secret for an access token and 90 | * secret, to sign API calls. 91 | * 92 | * @returns array("oauth_token" => "the-access-token", 93 | * "oauth_token_secret" => "the-access-secret", 94 | * "user_id" => "9436992", 95 | * "screen_name" => "abraham") 96 | */ 97 | function getAccessToken($oauth_verifier) { 98 | $parameters = array(); 99 | $parameters['oauth_verifier'] = $oauth_verifier; 100 | $request = $this->oAuthRequest($this->accessTokenURL(), 'GET', $parameters); 101 | $token = OAuthUtil::parse_parameters($request); 102 | $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']); 103 | return $token; 104 | } 105 | 106 | /** 107 | * One time exchange of username and password for access token and secret. 108 | * 109 | * @returns array("oauth_token" => "the-access-token", 110 | * "oauth_token_secret" => "the-access-secret", 111 | * "user_id" => "9436992", 112 | * "screen_name" => "abraham", 113 | * "x_auth_expires" => "0") 114 | */ 115 | function getXAuthToken($username, $password) { 116 | $parameters = array(); 117 | $parameters['x_auth_username'] = $username; 118 | $parameters['x_auth_password'] = $password; 119 | $parameters['x_auth_mode'] = 'client_auth'; 120 | $request = $this->oAuthRequest($this->accessTokenURL(), 'POST', $parameters); 121 | $token = OAuthUtil::parse_parameters($request); 122 | $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']); 123 | return $token; 124 | } 125 | 126 | /** 127 | * GET wrapper for oAuthRequest. 128 | */ 129 | function get($url, $parameters = array()) { 130 | $response = $this->oAuthRequest($url, 'GET', $parameters); 131 | if ($this->format === 'json' && $this->decode_json) { 132 | return json_decode($response); 133 | } 134 | return $response; 135 | } 136 | 137 | /** 138 | * POST wrapper for oAuthRequest. 139 | */ 140 | function post($url, $parameters = array()) { 141 | $response = $this->oAuthRequest($url, 'POST', $parameters); 142 | if ($this->format === 'json' && $this->decode_json) { 143 | return json_decode($response); 144 | } 145 | return $response; 146 | } 147 | 148 | /** 149 | * DELETE wrapper for oAuthReqeust. 150 | */ 151 | function delete($url, $parameters = array()) { 152 | $response = $this->oAuthRequest($url, 'DELETE', $parameters); 153 | if ($this->format === 'json' && $this->decode_json) { 154 | return json_decode($response); 155 | } 156 | return $response; 157 | } 158 | 159 | /** 160 | * Format and sign an OAuth / API request 161 | */ 162 | function oAuthRequest($url, $method, $parameters) { 163 | if (strrpos($url, 'https://') !== 0 && strrpos($url, 'http://') !== 0) { 164 | $url = "{$this->host}{$url}.{$this->format}"; 165 | } 166 | $request = OAuthRequest::from_consumer_and_token($this->consumer, $this->token, $method, $url, $parameters); 167 | $request->sign_request($this->sha1_method, $this->consumer, $this->token); 168 | 169 | switch ($method) { 170 | case 'GET': 171 | return $this->http($request->to_url(), 'GET'); 172 | default: 173 | return $this->http($request->get_normalized_http_url(), $method, $request->to_postdata()); 174 | } 175 | } 176 | 177 | /** 178 | * Make an HTTP request 179 | * 180 | * @return string|WP_Error API results on success or WP_Error object on unsupported HTTP method 181 | */ 182 | function http($url, $method, $postfields = NULL) { 183 | 184 | $args = array( 185 | 'timeout' => $this->timeout, 186 | 'sslverify' => $this->ssl_verifypeer, 187 | 'user-agent' => $this->useragent, 188 | ); 189 | 190 | switch ( $method ) { 191 | case 'GET' : 192 | $response = wp_remote_get( $url, $args ); 193 | break; 194 | # @TODO POST 195 | default: 196 | return new WP_Error( 'unsupported_http_method', sprintf( 'The HTTP method "%s", which you requested, is not supported', esc_html( $method ) ) ); 197 | } 198 | 199 | $this->url = $url; 200 | $this->http_code = wp_remote_retrieve_response_code( $response ); 201 | $this->http_headers = wp_remote_retrieve_headers( $response ); 202 | 203 | return wp_remote_retrieve_body( $response ); 204 | } 205 | 206 | /** 207 | * Get the header info to store. 208 | */ 209 | function getHeader($ch, $header) { 210 | $i = strpos($header, ':'); 211 | if (!empty($i)) { 212 | $key = str_replace('-', '_', strtolower(substr($header, 0, $i))); 213 | $value = trim(substr($header, $i + 2)); 214 | $this->http_header[$key] = $value; 215 | } 216 | return strlen($header); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /services/twitter/js.js: -------------------------------------------------------------------------------- 1 | /* 2 | This program is free software; you can redistribute it and/or modify 3 | it under the terms of the GNU General Public License as published by 4 | the Free Software Foundation; either version 2 of the License, or 5 | (at your option) any later version. 6 | 7 | This program is distributed in the hope that it will be useful, 8 | but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | GNU General Public License for more details. 11 | 12 | */ 13 | 14 | var mexp_twitter_location_js_loaded = false; 15 | var mexp_twitter_location_map = null; 16 | var mexp_twitter_location_marker = null; 17 | var mexp_twitter_location_timeout = null; 18 | var pf = wp.media.view.MediaFrame.Post; 19 | 20 | wp.media.view.MediaFrame.Post = pf.extend({ 21 | 22 | initialize: function() { 23 | 24 | pf.prototype.initialize.apply( this, arguments ); 25 | 26 | this.on( 'content:render:mexp-service-twitter-content-location', _.bind( function() { 27 | 28 | this.state().frame.content.get().on( 'loaded', function( response ) { 29 | 30 | if ( ! response || ! response.meta || ! response.meta.coords ) 31 | return; 32 | 33 | var ll = new google.maps.LatLng( response.meta.coords.lat, response.meta.coords.lng ); 34 | 35 | mexp_twitter_location_marker.setPosition( ll ); 36 | mexp_twitter_location_map.panTo( ll ); 37 | 38 | } ); 39 | 40 | if ( !mexp_twitter_location_js_loaded ) { 41 | 42 | $('#mexp_twitter_map_canvas').css( 'background-image', 'url(' + mexp.admin_url + '/images/wpspin_light.gif)'); 43 | 44 | var script = document.createElement("script"); 45 | script.type = "text/javascript"; 46 | script.src = mexp.services.twitter.labels.gmaps_url + '?sensor=false&callback=mexp_twitter_location_initialize'; 47 | document.body.appendChild(script); 48 | 49 | } else { 50 | 51 | mexp_twitter_location_initialize(); 52 | 53 | } 54 | 55 | }, this ) ); 56 | 57 | } 58 | 59 | }); 60 | 61 | function mexp_twitter_location_initialize() { 62 | 63 | var callback = function() { 64 | mexp_twitter_location_fetch( mexp_twitter_location_load ); 65 | }; 66 | 67 | if ( navigator.geolocation ) { 68 | navigator.geolocation.getCurrentPosition( mexp_twitter_location_load, callback ); 69 | mexp_twitter_location_timeout = window.setTimeout( callback, 8000 ); 70 | } else { 71 | mexp_twitter_location_fetch( callback ); 72 | } 73 | 74 | mexp_twitter_location_js_loaded = true; 75 | 76 | } 77 | 78 | function mexp_twitter_location_fetch( callback ) { 79 | 80 | callback( { 81 | coords : google.loader.ClientLocation 82 | } ); 83 | 84 | } 85 | 86 | function mexp_twitter_location_load( position ) { 87 | 88 | var lat, lng; 89 | $ = jQuery; 90 | 91 | window.clearTimeout( mexp_twitter_location_timeout ); 92 | 93 | // Enable the visual refresh 94 | google.maps.visualRefresh = true; 95 | 96 | var loc = $('#mexp-twitter-search-location-coords').val(); 97 | 98 | if ( loc ) { 99 | ll = loc.split( ',' ); 100 | lat = ll[0]; 101 | lng = ll[1]; 102 | } else { 103 | lat = position.coords.latitude; 104 | lng = position.coords.longitude; 105 | $('#mexp-twitter-search-location-coords').val( lat + ',' + lng ); 106 | } 107 | 108 | var radius = $('#mexp-twitter-search-location-radius').val(); 109 | var mapOptions = { 110 | center : new google.maps.LatLng( lat, lng ), 111 | zoom : 10, 112 | mapTypeId : google.maps.MapTypeId.ROADMAP, 113 | mapTypeControl : false, 114 | streetViewControl : false 115 | }; 116 | mexp_twitter_location_map = new google.maps.Map( document.getElementById( 'mexp_twitter_map_canvas' ), mapOptions ); 117 | mexp_twitter_location_marker = new google.maps.Marker({ 118 | position : new google.maps.LatLng( lat, lng ), 119 | draggable : true, 120 | map : mexp_twitter_location_map 121 | }); 122 | var circle = new google.maps.Circle({ 123 | map : mexp_twitter_location_map, 124 | radius : ( radius * 1000 ), // metres 125 | strokeWeight : 1, 126 | fillColor : 'blue', 127 | fillOpacity : 0.15, 128 | strokeColor : '#fff' 129 | }); 130 | circle.bindTo( 'center', mexp_twitter_location_marker, 'position' ); 131 | 132 | $('#mexp-twitter-search-location-radius').on('change',function(){ 133 | circle.setRadius( $(this).val() * 1000 ); 134 | }); 135 | $('#mexp-twitter-search-location-name').on('change',function(){ 136 | $('#mexp-twitter-search-location-coords').val(''); 137 | }); 138 | 139 | google.maps.event.addListener(mexp_twitter_location_marker, 'dragend', function() { 140 | p = mexp_twitter_location_marker.getPosition(); 141 | mexp_twitter_location_map.panTo( p ); 142 | $('#mexp-twitter-search-location-coords').val( p.lat() + ',' + p.lng() ).closest('form').submit(); 143 | }); 144 | 145 | } 146 | -------------------------------------------------------------------------------- /services/twitter/service.php: -------------------------------------------------------------------------------- 1 | set_template( new MEXP_Twitter_Template ); 28 | 29 | } 30 | 31 | public function load() { 32 | 33 | add_action( 'mexp_enqueue', array( $this, 'enqueue_statics' ) ); 34 | 35 | add_filter( 'mexp_tabs', array( $this, 'tabs' ), 10, 1 ); 36 | 37 | add_filter( 'mexp_labels', array( $this, 'labels' ), 10, 1 ); 38 | 39 | } 40 | 41 | public function enqueue_statics() { 42 | 43 | $mexp = Media_Explorer::init(); 44 | 45 | wp_enqueue_script( 46 | 'google-jsapi', 47 | 'https://www.google.com/jsapi', 48 | array(), 49 | false 50 | ); 51 | wp_enqueue_script( 52 | 'mexp-service-twitter', 53 | $mexp->plugin_url( 'services/twitter/js.js' ), 54 | array( 'jquery', 'mexp' ), 55 | $mexp->plugin_ver( 'services/twitter/js.js' ) 56 | ); 57 | 58 | } 59 | 60 | public function request( array $request ) { 61 | 62 | if ( is_wp_error( $connection = $this->get_connection() ) ) 63 | return $connection; 64 | 65 | $params = $request['params']; 66 | 67 | if ( isset( $params['location'] ) and empty( $params['coords'] ) ) { 68 | if ( is_wp_error( $coords = $this->get_coords( $params['location'] ) ) ) { 69 | return $coords; 70 | } else { 71 | $this->response_meta['coords'] = $coords; 72 | $params['coords'] = sprintf( '%s,%s', $coords->lat, $coords->lng ); 73 | } 74 | } 75 | 76 | # operators: https://dev.twitter.com/docs/using-search 77 | # @TODO +exclude:retweets 78 | 79 | $q = array(); 80 | 81 | if ( isset( $params['q'] ) ) 82 | $q[] = trim( $params['q'] ); 83 | 84 | if ( isset( $params['hashtag'] ) ) 85 | $q[] = sprintf( '#%s', ltrim( $params['hashtag'], '#' ) ); 86 | 87 | if ( isset( $params['by_user'] ) ) 88 | $q[] = sprintf( 'from:%s', ltrim( $params['by_user'], '@' ) ); 89 | 90 | if ( isset( $params['to_user'] ) ) 91 | $q[] = sprintf( '@%s', ltrim( $params['to_user'], '@' ) ); 92 | 93 | if ( 'images' == $request['tab'] ) 94 | $q[] = 'filter:images'; 95 | 96 | // Exclude retweets from certain searches 97 | if ( ! isset( $params['by_user'] ) && ! isset( $params['to_user'] ) ) 98 | $q[] = '+exclude:retweets'; 99 | 100 | $args = array( 101 | 'q' => implode( ' ', $q ), 102 | 'result_type' => 'recent', 103 | 'count' => 20, 104 | ); 105 | 106 | if ( isset( $params['coords'] ) and isset( $params['radius'] ) ) { 107 | if ( is_array( $params['radius'] ) ) 108 | $params['radius'] = reset( $params['radius'] ); 109 | $args['geocode'] = sprintf( '%s,%dkm', $params['coords'], $params['radius'] ); 110 | } 111 | 112 | if ( !empty( $request['min_id'] ) ) 113 | $args['since_id'] = $request['min_id']; 114 | else if ( !empty( $request['max_id'] ) ) 115 | $args['max_id'] = $request['max_id']; 116 | 117 | $response = $connection->get( sprintf( '%s/search/tweets.json', untrailingslashit( $connection->host ) ), $args ); 118 | 119 | if ( 200 == $connection->http_code ) { 120 | 121 | return $this->response( $response ); 122 | 123 | } else { 124 | 125 | return new WP_Error( 126 | 'mexp_twitter_failed_request', 127 | sprintf( __( 'Could not connect to Twitter (error %s).', 'mexp' ), 128 | esc_html( $connection->http_code ) 129 | ) 130 | ); 131 | 132 | } 133 | 134 | } 135 | 136 | public function get_coords( $location ) { 137 | 138 | $url = sprintf( 'https://maps.googleapis.com/maps/api/geocode/json?address=%s&sensor=false', 139 | urlencode( trim( $location ) ) 140 | ); 141 | $result = wp_remote_get( $url ); 142 | 143 | if ( is_wp_error( $result ) ) 144 | return $result; 145 | 146 | $error = new WP_Error( 147 | 'mexp_twitter_failed_location', 148 | __( 'Could not find your requested location.', 'mexp' ) 149 | ); 150 | 151 | if ( 200 != wp_remote_retrieve_response_code( $result ) ) 152 | return $error; 153 | if ( ! $data = wp_remote_retrieve_body( $result ) ) 154 | return $error; 155 | 156 | $data = json_decode( $data ); 157 | 158 | if ( 'OK' != $data->status ) 159 | return $error; 160 | 161 | $location = reset( $data->results ); 162 | 163 | if ( ! isset( $location->geometry->location ) ) 164 | return $error; 165 | 166 | return $location->geometry->location; 167 | 168 | } 169 | 170 | public function status_url( $status ) { 171 | 172 | return sprintf( 'https://twitter.com/%s/status/%s', 173 | $status->user->screen_name, 174 | $status->id_str 175 | ); 176 | 177 | } 178 | 179 | public function status_content( $status ) { 180 | 181 | $text = $status->text; 182 | 183 | # @TODO more processing (hashtags, @s etc) 184 | $text = make_clickable( $text ); 185 | $text = str_replace( ' href="', ' target="_blank" href="', $text ); 186 | 187 | return $text; 188 | 189 | } 190 | 191 | public function get_max_id( $next ) { 192 | 193 | parse_str( ltrim( $next, '?' ), $vars ); 194 | 195 | if ( isset( $vars['max_id'] ) ) 196 | return $vars['max_id']; 197 | else 198 | return null; 199 | 200 | } 201 | 202 | public function response( $r ) { 203 | 204 | if ( !isset( $r->statuses ) or empty( $r->statuses ) ) 205 | return false; 206 | 207 | $response = new MEXP_Response; 208 | 209 | if ( isset( $r->search_metadata->next_results ) ) 210 | $response->add_meta( 'max_id', self::get_max_id( $r->search_metadata->next_results ) ); 211 | 212 | if ( isset( $this->response_meta ) ) 213 | $response->add_meta( $this->response_meta ); 214 | 215 | foreach ( $r->statuses as $status ) { 216 | 217 | $item = new MEXP_Response_Item; 218 | 219 | $item->set_id( $status->id_str ); 220 | $item->set_url( self::status_url( $status ) ); 221 | $item->set_content( self::status_content( $status ) ); 222 | $item->set_thumbnail( is_ssl() ? $status->user->profile_image_url_https : $status->user->profile_image_url ); 223 | $item->set_date( strtotime( $status->created_at ) ); 224 | $item->set_date_format( 'g:i A - j M y' ); 225 | 226 | $item->add_meta( 'user', array( 227 | 'name' => $status->user->name, 228 | 'screen_name' => $status->user->screen_name, 229 | ) ); 230 | 231 | $response->add_item( $item ); 232 | 233 | } 234 | 235 | return $response; 236 | 237 | } 238 | 239 | public function tabs( array $tabs ) { 240 | $tabs['twitter'] = array( 241 | 'all' => array( 242 | 'text' => _x( 'All', 'Tab title', 'mexp'), 243 | 'defaultTab' => true 244 | ), 245 | 'hashtag' => array( 246 | 'text' => _x( 'With Hashtag', 'Tab title', 'mexp'), 247 | ), 248 | #'images' => array( 249 | # 'text' => _x( 'With Images', 'Tab title', 'mexp'), 250 | #), 251 | 'by_user' => array( 252 | 'text' => _x( 'By User', 'Tab title', 'mexp'), 253 | ), 254 | 'to_user' => array( 255 | 'text' => _x( 'To User', 'Tab title', 'mexp'), 256 | ), 257 | 'location' => array( 258 | 'text' => _x( 'By Location', 'Tab title', 'mexp'), 259 | ), 260 | ); 261 | 262 | return $tabs; 263 | } 264 | 265 | public function requires() { 266 | return array( 267 | 'oauth' => 'OAuthConsumer' 268 | ); 269 | } 270 | 271 | public function labels( array $labels ) { 272 | $labels['twitter'] = array( 273 | 'title' => __( 'Insert Tweet', 'mexp' ), 274 | # @TODO the 'insert' button text gets reset when selecting items. find out why. 275 | 'insert' => __( 'Insert Tweet', 'mexp' ), 276 | 'noresults' => __( 'No tweets matched your search query', 'mexp' ), 277 | 'gmaps_url' => set_url_scheme( 'https://maps.google.com/maps/api/js', 'https' ), 278 | 'loadmore' => __( 'Load more tweets', 'mexp' ), 279 | ); 280 | 281 | return $labels; 282 | } 283 | 284 | private function get_connection() { 285 | 286 | $credentials = $this->get_credentials(); 287 | 288 | # Despite saying that application-only authentication for search would be available by the 289 | # end of March 2013, Twitter has still not implemented it. This means that for API v1.1 we 290 | # still need user-level authentication in addition to application-level authentication. 291 | # 292 | # If the time comes that application-only authentication is made available for search, the 293 | # use of the oauth_token and oauth_token_secret fields below can simply be removed. 294 | # 295 | # Further bedtime reading: 296 | # 297 | # https://dev.twitter.com/discussions/11079 298 | # https://dev.twitter.com/discussions/13210 299 | # https://dev.twitter.com/discussions/14016 300 | # https://dev.twitter.com/discussions/15744 301 | 302 | foreach ( array( 'consumer_key', 'consumer_secret', 'oauth_token', 'oauth_token_secret' ) as $field ) { 303 | if ( !isset( $credentials[$field] ) or empty( $credentials[$field] ) ) { 304 | return new WP_Error( 305 | 'mexp_twitter_no_connection', 306 | __( 'oAuth connection to Twitter not found.', 'mexp' ) 307 | ); 308 | } 309 | } 310 | 311 | if ( !class_exists( 'WP_Twitter_OAuth' ) ) 312 | require_once dirname( __FILE__ ) . '/class.wp-twitter-oauth.php'; 313 | 314 | $connection = new WP_Twitter_OAuth( 315 | $credentials['consumer_key'], 316 | $credentials['consumer_secret'], 317 | $credentials['oauth_token'], 318 | $credentials['oauth_token_secret'] 319 | ); 320 | 321 | $connection->useragent = sprintf( 'Extended Media Manager at %s', home_url() ); 322 | 323 | return $connection; 324 | 325 | } 326 | 327 | private function get_credentials() { 328 | 329 | if ( is_null( $this->credentials ) ) 330 | $this->credentials = (array) apply_filters( 'mexp_twitter_credentials', array() ); 331 | 332 | return $this->credentials; 333 | 334 | } 335 | 336 | } 337 | 338 | add_filter( 'mexp_services', 'mexp_service_twitter' ); 339 | 340 | function mexp_service_twitter( array $services ) { 341 | $services['twitter'] = new MEXP_Twitter_Service; 342 | return $services; 343 | } 344 | -------------------------------------------------------------------------------- /services/twitter/template.php: -------------------------------------------------------------------------------- 1 | 19 |
    20 |
    21 |
    22 | 23 |
    24 |
    25 |
    26 | {{ data.meta.user.name }} 27 | @{{ data.meta.user.screen_name }} 28 |
    29 |
    30 | {{{ data.content }}} 31 |
    32 |
    33 | {{ data.date }} 34 |
    35 |
    36 |
    37 |
    38 | 39 |
    40 |
    41 | 46 | 56 |
    57 | 65 | 66 |
    67 |
    68 | 75 |
    76 | 84 | 85 |
    86 |
    87 | 94 |
    95 | 103 | 104 |
    105 |
    106 | 113 |
    114 |
    115 | 121 | 129 | 132 | 141 | 152 | 157 |
    158 |
    159 | 168 |
    169 | 177 | 178 |
    179 |
    180 | developer_key = $developer_key; 11 | } 12 | 13 | /** 14 | * example request: 15 | * https://www.googleapis.com/youtube/v3/search 16 | * ?part=snippet 17 | * &q=YouTube+Data+API 18 | * &type=video 19 | * &videoCaption=closedCaption 20 | * 21 | * this method performs a query to the search endpoint of the YouTube API 22 | * 23 | * @param array $query an array containing the parameters of the query 24 | * @return string 25 | */ 26 | public function get_videos( $query ) { 27 | $request = $this->create_url( $query ); 28 | return self::get_json_as_array( $request ); 29 | } 30 | 31 | /** 32 | * This method returns the videos of a channel by channel name. 33 | * 34 | * @param array $query an array containing the parameters of the query 35 | * @return string 36 | */ 37 | public function get_videos_from_channel( $query ) { 38 | 39 | $channel_url_query = $this->create_url( $query, 'channels' ); 40 | 41 | // First request, in which we are trying to get the uploads playlist id of the user 42 | $channel_response = self::get_json_as_array( $channel_url_query ); 43 | 44 | if ( $channel_response['pageInfo']['totalResults'] == 0 ) 45 | return false; 46 | 47 | // Every YouTube channel has a "uploads" playlist, containing all the uploads of the channel 48 | $playlist_params['uploads_id'] = $channel_response['items'][0]['contentDetails']['relatedPlaylists']['uploads']; 49 | $playlist_params['page_token'] = $query['page_token']; 50 | 51 | $playlist_url_query = $this->create_url( $playlist_params, 'playlistItems' ); 52 | 53 | // Second cURL, in this one we are going to get all the videos inside the uploads playlist of the user 54 | return self::get_json_as_array( $playlist_url_query ); 55 | } 56 | 57 | /** 58 | * This method creates an url from an array of parameters 59 | * 60 | * @param array $query an array containing the parameters for the request 61 | * @param string $resource a string containing the endpoint of the API 62 | * @return string 63 | */ 64 | private function create_url( $query, $resource = 'search' ) { 65 | // URL for channels 66 | if ( $resource == 'channels' ) { 67 | $channel_url_query = sprintf( '%s/channels?forUsername=%s&part=contentDetails&key=%s', $this->api_url, urlencode( $query['channel'] ), $this->developer_key ); 68 | return $channel_url_query; 69 | } 70 | 71 | // URL for playlists 72 | if ( $resource == 'playlistItems' ) { 73 | $playlist_url_query = sprintf( '%s/playlistItems?maxResults=%s&playlistId=%s&part=snippet&key=%s', $this->api_url, MEXP_YouTube_Service::DEFAULT_MAX_RESULTS, $query['uploads_id'], $this->developer_key ); 74 | if ( isset( $query['page_token'] ) && '' != $query['page_token'] ) 75 | $playlist_url_query .= '&pageToken=' . $query['page_token']; 76 | return $playlist_url_query; 77 | } 78 | 79 | $params = array(); 80 | 81 | if ( isset( $query['page_token'] ) && '' !== $query['page_token'] ) { 82 | $params[] = 'pageToken=' . $query['page_token']; 83 | } 84 | 85 | if ( isset( $query['q'] ) ) 86 | $params[] = 'q=' . urlencode( $query['q'] ); 87 | 88 | // Allow searching for playlists or videos 89 | if ( isset( $query['type'] ) && $query['type'] == 'playlist' ) 90 | $params[] = 'type=playlist'; 91 | else 92 | $params[] = 'type=video'; 93 | 94 | // Number of results we want to return 95 | if ( isset( $query['maxResults'] ) ) 96 | $params[] = 'maxResults=' . (int) $query['maxResults']; 97 | else 98 | $params[] = 'maxResults=' . MEXP_YouTube_Service::DEFAULT_MAX_RESULTS; 99 | 100 | // Mandatory field "part" 101 | if ( isset( $query['part'] ) ) 102 | $params[] = 'part=' . urlencode( $query['part'] ); 103 | else 104 | $params[] = 'part=snippet'; 105 | 106 | return $this->api_url . '/' . $resource . '?'. implode( '&', $params ) . '&key=' . $this->developer_key; 107 | } 108 | 109 | /** 110 | * Fetch an url and returns the json parsed as an array 111 | * 112 | * @param string $url the URL we want to curl 113 | * @return array 114 | */ 115 | private static function get_json_as_array( $url ) { 116 | $response = (array) wp_remote_get( $url ); 117 | if ( !isset( $response['response']['code'] ) || 200 != $response['response']['code'] ) 118 | return false; 119 | else 120 | return json_decode( $response['body'], true ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /services/youtube/js.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This js is going to handle the infinite scroll for the YouTube service in the 3 | * MEXP plugin 4 | * */ 5 | 6 | var toolbarView = wp.media.view.Toolbar.MEXP, 7 | mexpContentView = wp.media.view.MEXP, 8 | flagAjaxExecutions = '', 9 | isInfiniteScroll = false; 10 | 11 | wp.media.view.MEXP = mexpContentView.extend({ 12 | 13 | initialize: function() { 14 | mexpContentView.prototype.initialize.apply( this, arguments ); 15 | }, 16 | 17 | render: function() { 18 | var selection = this.getSelection(), 19 | _this = this; 20 | 21 | if ( this.collection && this.collection.models.length ) { 22 | var container = document.createDocumentFragment(); 23 | 24 | if ( isInfiniteScroll ) { 25 | this.collection.each( function( model, index ) { 26 | // This makes the collection to render only the last 18 items of 27 | // it, instead of all. Tweak for the infinite scroll to work ok 28 | if ( index >= this.collection.length - 18 && isInfiniteScroll ) 29 | container.appendChild( this.renderItem( model ) ); 30 | }, this ); 31 | } else { 32 | this.collection.each( function( model, index ) { 33 | container.appendChild( this.renderItem( model ) ); 34 | }, this ); 35 | } 36 | 37 | this.$el.find( '.mexp-items' ).append( container ); 38 | 39 | } 40 | 41 | selection.each( function( model ) { 42 | var id = '#mexp-item-' + this.service.id + '-' + this.tab + '-' + model.get( 'id' ); 43 | this.$el.find( id ).closest( '.mexp-item' ).addClass( 'selected details' ); 44 | }, this ); 45 | 46 | jQuery( '#mexp-button' ).prop( 'disabled', !selection.length ); 47 | 48 | // Infinite scrolling for youtube results 49 | jQuery( '.mexp-content-youtube ul.mexp-items' ).scroll( function() { 50 | var $container = jQuery( 'ul.mexp-items' ), 51 | totalHeight = $container.get( 0 ).scrollHeight, 52 | position = $container.height() + $container.scrollTop(), 53 | offset = ( totalHeight / 100 ) * 30; 54 | 55 | if( totalHeight - position <= offset ) { 56 | _this.fetchItems.apply( _this, [ jQuery( '.tab-all #page_token' ).val() ] ); 57 | } 58 | } ); 59 | 60 | return this; 61 | }, 62 | 63 | fetchItems: function( pageToken ) { 64 | 65 | if ( this.service.id !== 'youtube' ) { 66 | mexpContentView.prototype.fetchItems.apply( this, arguments ); 67 | return; 68 | } 69 | 70 | // This if-else block handles the concurrency for not calling to the 71 | // same set of videos several times. 72 | if ( "youtube" === this.service.id && pageToken && pageToken === flagAjaxExecutions ) 73 | return; 74 | else 75 | flagAjaxExecutions = pageToken; 76 | 77 | this.trigger( 'loading' ); 78 | 79 | var params = this.model.get( 'params' ); 80 | 81 | if ( undefined === pageToken ) { 82 | isInfiniteScroll = false; 83 | jQuery( '.tab-all #page_token' ).val( '' ); 84 | params.page_token = ''; 85 | } else { 86 | isInfiniteScroll = true; 87 | } 88 | 89 | params.startIndex = jQuery( '.mexp-item' ).length; 90 | 91 | var data = { 92 | _nonce : mexp._nonce, 93 | service : this.service.id, 94 | params : params, 95 | page : this.model.get( 'page' ), 96 | max_id : this.model.get( 'max_id' ) 97 | }; 98 | 99 | media.ajax( 'mexp_request', { 100 | context : this, 101 | success : this.fetchedSuccess, 102 | error : this.fetchedError, 103 | data : data 104 | } ); 105 | 106 | }, 107 | 108 | fetchedSuccess: function( response ) { 109 | 110 | var _this = this; 111 | 112 | if ( this.service.id !== 'youtube' ) { 113 | mexpContentView.prototype.fetchedSuccess.apply( this, arguments ); 114 | return; 115 | } 116 | 117 | if ( !this.model.get( 'page' ) ) { 118 | 119 | if ( !response.items ) { 120 | this.fetchedEmpty( response ); 121 | return; 122 | } 123 | 124 | if ( response.meta.page_token ) { 125 | var params = this.model.get( 'params' ); 126 | 127 | if ( this.tab == 'all' ) 128 | jQuery( '.tab-all #page_token' ).val( response.meta.page_token ); 129 | if ( this.tab == 'by_user' ) 130 | jQuery( '.tab-by_user #page_token' ).val( response.meta.page_token ); 131 | 132 | if ( params.page_token !== response.meta.page_token ) { 133 | params.page_token = response.meta.page_token; 134 | this.model.set( 'params', params ); 135 | } 136 | } 137 | 138 | this.model.set( 'min_id', response.meta.min_id ); 139 | this.model.set( 'items', response.items ); 140 | 141 | // Append the last elements to the collection. 142 | if ( isInfiniteScroll ) { 143 | _.each( response.items, function( item ) { 144 | _this.collection.add( item ); 145 | } ); 146 | this.collection.reset( this.collection.models ); 147 | } else { 148 | this.collection.reset( response.items ); 149 | } 150 | 151 | 152 | } else { 153 | 154 | if ( !response.items ) { 155 | this.moreEmpty( response ); 156 | return; 157 | } 158 | 159 | if ( response.meta.page_token ) { 160 | var params = this.model.get( 'params' ); 161 | 162 | if ( this.tab == 'all' ) 163 | jQuery( '.tab-all #page_token' ).val( response.meta.page_token ); 164 | if ( this.tab == 'by_user' ) 165 | jQuery( '.tab-by_user #page_token' ).val( response.meta.page_token ); 166 | 167 | if ( params.page_token !== response.meta.page_token ) { 168 | params.page_token = response.meta.page_token; 169 | this.model.set( 'params', params ); 170 | } 171 | } 172 | 173 | this.model.set( 'items', this.model.get( 'items' ).concat( response.items ) ); 174 | 175 | var collection = new Backbone.Collection( response.items ); 176 | var container = document.createDocumentFragment(); 177 | 178 | this.collection.add( collection.models ); 179 | 180 | collection.each( function( model ) { 181 | container.appendChild( this.renderItem( model ) ); 182 | }, this ); 183 | 184 | this.$el.find( '.mexp-items' ).append( container ); 185 | 186 | } 187 | 188 | this.trigger( 'loaded loaded:success', response ); 189 | 190 | }, 191 | 192 | fetchedError: function(response) { 193 | mexpContentView.prototype.fetchedError.apply( this, arguments ); 194 | }, 195 | 196 | fetchedEmpty: function() { 197 | mexpContentView.prototype.fetchedEmpty.apply( this, arguments ); 198 | }, 199 | 200 | loading: function() { 201 | mexpContentView.prototype.loading.apply( this, arguments ); 202 | 203 | if ( 'youtube' !== this.service.id ) return; 204 | 205 | // show bottom spinner 206 | jQuery( '.spinner-bottom' ).show(); 207 | }, 208 | 209 | loaded: function() { 210 | mexpContentView.prototype.loaded.apply( this, arguments ); 211 | 212 | if ( 'youtube' !== this.service.id ) return; 213 | 214 | // hide bottom spinner 215 | jQuery( '.spinner-bottom' ).hide(); 216 | }, 217 | }); 218 | 219 | wp.media.view.Toolbar.MEXP = toolbarView.extend({ 220 | 221 | initialize: function() { 222 | 223 | toolbarView.prototype.initialize.apply( this, arguments ); 224 | 225 | this.set( 'spinner', new wp.Backbone.View({ 226 | tagName: 'span', 227 | className: 'spinner spinner-bottom', 228 | priority: -20, 229 | }) ); 230 | 231 | } 232 | }); 233 | -------------------------------------------------------------------------------- /services/youtube/service.php: -------------------------------------------------------------------------------- 1 | set_template( new MEXP_YouTube_Template ); 12 | } 13 | 14 | public function load() { 15 | 16 | add_action( 'mexp_enqueue', array( $this, 'enqueue_statics' ) ); 17 | 18 | add_filter( 'mexp_tabs', array( $this, 'tabs' ), 10, 1 ); 19 | 20 | add_filter( 'mexp_labels', array( $this, 'labels' ), 10, 1 ); 21 | 22 | } 23 | 24 | public function enqueue_statics() { 25 | 26 | $mexp = Media_Explorer::init(); 27 | 28 | wp_enqueue_script( 29 | 'mexp-service-youtube', 30 | $mexp->plugin_url( 'services/youtube/js.js' ), 31 | array( 'jquery', 'mexp' ), 32 | $mexp->plugin_ver( 'services/youtube/js.js' ), 33 | true 34 | ); 35 | 36 | } 37 | 38 | public function request( array $request ) { 39 | if ( is_wp_error( $youtube = $this->get_connection() ) ) 40 | return $youtube; 41 | $params = $request['params']; 42 | 43 | switch ( $params['tab'] ) 44 | { 45 | case 'by_user': 46 | $request = array( 47 | 'channel' => sanitize_text_field( $params['channel'] ), 48 | 'type' => 'video', 49 | 'page_token' => sanitize_text_field( $params['page_token'] ), 50 | ); 51 | 52 | //if ( isset( $params['page_token'] ) && '' !== $params['page_token'] ) 53 | //$request['page_token'] = sanitize_text_field( $params['page_token'] ); 54 | 55 | // Make the request to the YouTube API 56 | $search_response = $youtube->get_videos_from_channel( $request ); 57 | break; 58 | 59 | default: 60 | case 'all': 61 | $request = array( 62 | 'q' => sanitize_text_field( $params['q'] ), 63 | 'maxResults' => self::DEFAULT_MAX_RESULTS, 64 | ); 65 | 66 | if ( isset( $params['page_token'] ) && '' !== $params['page_token'] ) 67 | $request['page_token'] = sanitize_text_field( $params['page_token'] ); 68 | 69 | if ( isset( $params['type'] ) ) 70 | $request['type'] = sanitize_text_field( $params['type'] ); 71 | 72 | // Make the request to the YouTube API 73 | $search_response = $youtube->get_videos( $request ); 74 | break; 75 | 76 | } 77 | 78 | // Create the response for the API 79 | $response = new MEXP_Response(); 80 | 81 | if ( !isset( $search_response['items'] ) ) 82 | return false; 83 | 84 | foreach ( $search_response['items'] as $index => $search_item ) { 85 | $item = new MEXP_Response_Item(); 86 | if ( $request['type'] == 'video' && isset( $request['q'] ) ) { // For videos searched by query 87 | $item->set_url( esc_url( sprintf( "https://www.youtube.com/watch?v=%s", $search_item['id']['videoId'] ) ) ); 88 | } elseif( $request['type'] == 'playlist' && isset( $request['q'] ) ) { // For playlists searched by query 89 | $item->set_url( esc_url( sprintf( "https://www.youtube.com/playlist?list=%s", $search_item['id']['playlistId'] ) ) ); 90 | } else { // For videos searched by channel name 91 | $item->set_url( esc_url( sprintf( "https://www.youtube.com/watch?v=%s", $search_item['snippet']['resourceId']['videoId'] ) ) ); 92 | } 93 | $item->add_meta( 'user', $search_item['snippet']['channelTitle'] ); 94 | $item->set_id( (int) $params['startIndex'] + (int) $index ); 95 | $item->set_content( $search_item['snippet']['title'] ); 96 | $item->set_thumbnail( $search_item['snippet']['thumbnails']['medium']['url'] ); 97 | $item->set_date( strtotime( $search_item['snippet']['publishedAt'] ) ); 98 | $item->set_date_format( 'g:i A - j M y' ); 99 | $response->add_item($item); 100 | } 101 | 102 | if ( isset( $search_response['nextPageToken'] ) ) 103 | $response->add_meta( 'page_token', $search_response['nextPageToken'] ); 104 | 105 | return $response; 106 | } 107 | 108 | public function tabs( array $tabs ) { 109 | $tabs['youtube'] = array( 110 | 'all' => array( 111 | 'text' => _x( 'All', 'Tab title', 'mexp'), 112 | 'defaultTab' => true 113 | ), 114 | 'by_user' => array( 115 | 'text' => _x( 'By User', 'Tab title', 'mexp'), 116 | ), 117 | ); 118 | return $tabs; 119 | } 120 | 121 | private function get_connection() { 122 | // Add the Google API classes to the runtime 123 | require_once plugin_dir_path( __FILE__) . '/class.wp-youtube-client.php'; 124 | 125 | $developer_key = (string) apply_filters( 'mexp_youtube_developer_key', '' ) ; 126 | 127 | if ( empty( $developer_key ) ) { 128 | return new WP_Error( 129 | 'mexp_youtube_no_connection', 130 | __( 'API connection to YouTube not found.', 'mexp' ) 131 | ); 132 | } 133 | 134 | return new MEXP_YouTube_Client( $developer_key ); 135 | } 136 | 137 | public function labels( array $labels ) { 138 | 139 | $labels['youtube'] = array( 140 | 'title' => __( 'Insert YouTube', 'mexp' ), 141 | 'insert' => __( 'Insert', 'mexp' ), 142 | 'noresults' => __( 'No videos matched your search query.', 'mexp' ), 143 | ); 144 | 145 | return $labels; 146 | } 147 | } 148 | 149 | add_filter( 'mexp_services', 'mexp_service_youtube' ); 150 | 151 | function mexp_service_youtube( array $services ) { 152 | $services['youtube'] = new MEXP_YouTube_Service; 153 | return $services; 154 | } 155 | -------------------------------------------------------------------------------- /services/youtube/template.php: -------------------------------------------------------------------------------- 1 | 13 |
    14 |
    15 |
    16 | 17 |
    18 |
    19 |
    20 | {{ data.content }} 21 |
    22 |
    23 | {{ data.meta.user }} 24 |
    25 |
    26 | {{ data.date }} 27 |
    28 |
    29 |
    30 |
    31 | 32 |
    33 |
    34 | 39 | 53 |
    54 | 62 | 63 | 64 | 65 | 69 | 70 |
    71 |
    72 | 77 |
    79 | 87 | 88 | 89 | 90 |
    91 |
    92 | _me = $media_explorer; 12 | } 13 | } 14 | --------------------------------------------------------------------------------