diff --git a/.travis.yml b/.travis.yml index a0a5dec..af66100 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,78 @@ +# Travis CI (MIT License) configuration file for the Syndication WordPress plugin. +# @link https://travis-ci.org/ + +# For use with the Syndication WordPress plugin. +# @link https://github.com/Automattic/syndication + +# Ditch sudo and use containers. +# @link http://docs.travis-ci.com/user/migrating-from-legacy/#Why-migrate-to-container-based-infrastructure%3F +# @link http://docs.travis-ci.com/user/workers/container-based-infrastructure/#Routing-your-build-to-container-based-infrastructure +sudo: false + +# Declare project language. +# @link http://about.travis-ci.org/docs/user/languages/php/ language: php -php: - - 5.5 - - 5.6 - - 7 - - hhvm - - nightly - -script: make lint + +# Declare versions of PHP to use. Use one decimal max. +# @link http://docs.travis-ci.com/user/build-configuration/ +matrix: + fast_finish: true + + include: + # Current $required_php_version for WordPress: 5.2.4 + # aliased to 5.2.17 + - php: '5.2' + # aliased to a recent 5.6.x version + - php: '5.6' + env: SNIFF=1 + # aliased to a recent 7.x version + - php: '7.0' + # aliased to a recent hhvm version + - php: 'hhvm' + + allow_failures: + - php: 'hhvm' + +# Use this to prepare the system to install prerequisites or dependencies. +# e.g. sudo apt-get update. +# Failures in this section will result in build status 'errored'. +# before_install: + +# Use this to prepare your build for testing. +# e.g. copy database configurations, environment variables, etc. +# Failures in this section will result in build status 'errored'. +before_script: + - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION + # PHPCS + - export PHPCS_DIR=/tmp/phpcs + - export SNIFFS_DIR=/tmp/sniffs + # Install CodeSniffer for WordPress Coding Standards checks. + - if [[ "$SNIFF" == "1" ]]; then git clone -b master --depth 1 https://github.com/squizlabs/PHP_CodeSniffer.git $PHPCS_DIR; fi + # Install WordPress Coding Standards. + - if [[ "$SNIFF" == "1" ]]; then git clone -b master --depth 1 https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards.git $SNIFFS_DIR; fi + # Install PHP Compatibility sniffs. + - if [[ "$SNIFF" == "1" ]]; then git clone -b master --depth 1 https://github.com/wimg/PHPCompatibility.git $SNIFFS_DIR/PHPCompatibility; fi + # Set install path for PHPCS sniffs. + # @link https://github.com/squizlabs/PHP_CodeSniffer/blob/4237c2fc98cc838730b76ee9cee316f99286a2a7/CodeSniffer.php#L1941 + - if [[ "$SNIFF" == "1" ]]; then $PHPCS_DIR/scripts/phpcs --config-set installed_paths $SNIFFS_DIR; fi + # After CodeSniffer install you should refresh your path. + - if [[ "$SNIFF" == "1" ]]; then phpenv rehash; fi + +# Run test script commands. +# Default is specific to project language. +# All commands must exit with code 0 on success. Anything else is considered failure. +script: + # Search for PHP syntax errors. + - find -L . -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l + # WordPress Coding Standards. + # @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + # @link http://pear.php.net/package/PHP_CodeSniffer/ + # -p flag: Show progress of the run. + # -s flag: Show sniff codes in all reports. + # -v flag: Print verbose output. + # -n flag: Do not print warnings. (shortcut for --warning-severity=0) + # --standard: Use WordPress as the standard. + # --extensions: Only sniff PHP files. + - if [[ "$SNIFF" == "1" ]]; then $PHPCS_DIR/scripts/phpcs -p -s -v -n . --standard="WordPress-VIP" --extensions=php; fi + # Unit tests + - phpunit diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh index 1e39064..cf709c2 100644 --- a/bin/install-wp-tests.sh +++ b/bin/install-wp-tests.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash if [ $# -lt 3 ]; then - echo "usage: $0 [db-host] [wp-version]" + echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" exit 1 fi @@ -10,25 +10,61 @@ DB_USER=$2 DB_PASS=$3 DB_HOST=${4-localhost} WP_VERSION=${5-latest} +SKIP_DB_CREATE=${6-false} WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} -WP_CORE_DIR=/tmp/wordpress/ +WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} + +download() { + if [ `which curl` ]; then + curl -s "$1" > "$2"; + elif [ `which wget` ]; then + wget -nv -O "$2" "$1" + fi +} + +if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then + WP_TESTS_TAG="tags/$WP_VERSION" +elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + WP_TESTS_TAG="trunk" +else + # http serves a single offer, whereas https serves multiple. we only want one + download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json + grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json + LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') + if [[ -z "$LATEST_VERSION" ]]; then + echo "Latest WordPress version could not be found" + exit 1 + fi + WP_TESTS_TAG="tags/$LATEST_VERSION" +fi set -ex install_wp() { + + if [ -d $WP_CORE_DIR ]; then + return; + fi + mkdir -p $WP_CORE_DIR - if [ $WP_VERSION == 'latest' ]; then - local ARCHIVE_NAME='latest' + if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + mkdir -p /tmp/wordpress-nightly + download https://wordpress.org/nightly-builds/wordpress-latest.zip /tmp/wordpress-nightly/wordpress-nightly.zip + unzip -q /tmp/wordpress-nightly/wordpress-nightly.zip -d /tmp/wordpress-nightly/ + mv /tmp/wordpress-nightly/wordpress/* $WP_CORE_DIR else - local ARCHIVE_NAME="wordpress-$WP_VERSION" + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + else + local ARCHIVE_NAME="wordpress-$WP_VERSION" + fi + download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz + tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR fi - wget -nv -O /tmp/wordpress.tar.gz http://wordpress.org/${ARCHIVE_NAME}.tar.gz - tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR - - wget -nv -O $WP_CORE_DIR/wp-content/db.php https://raw.github.com/markoheijnen/wp-mysqli/master/db.php + download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php } install_test_suite() { @@ -39,20 +75,32 @@ install_test_suite() { local ioption='-i' fi - # set up testing suite - mkdir -p $WP_TESTS_DIR - cd $WP_TESTS_DIR - svn co --quiet http://develop.svn.wordpress.org/trunk/tests/phpunit/includes/ - - wget -nv -O wp-tests-config.php http://develop.svn.wordpress.org/trunk/wp-tests-config-sample.php - sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR':" wp-tests-config.php - sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" wp-tests-config.php - sed $ioption "s/yourusernamehere/$DB_USER/" wp-tests-config.php - sed $ioption "s/yourpasswordhere/$DB_PASS/" wp-tests-config.php - sed $ioption "s|localhost|${DB_HOST}|" wp-tests-config.php + # set up testing suite if it doesn't yet exist + if [ ! -d $WP_TESTS_DIR ]; then + # set up testing suite + mkdir -p $WP_TESTS_DIR + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + fi + + if [ ! -f wp-tests-config.php ]; then + download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php + # remove all forward slashes in the end + WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") + sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + fi + } install_db() { + + if [ ${SKIP_DB_CREATE} = "true" ]; then + return 0 + fi + # parse DB_HOST for port or socket references local PARTS=(${DB_HOST//\:/ }) local DB_HOSTNAME=${PARTS[0]}; @@ -60,7 +108,7 @@ install_db() { local EXTRA="" if ! [ -z $DB_HOSTNAME ] ; then - if [[ "$DB_SOCK_OR_PORT" =~ ^[0-9]+$ ]] ; then + if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" elif ! [ -z $DB_SOCK_OR_PORT ] ; then EXTRA=" --socket=$DB_SOCK_OR_PORT" diff --git a/includes/class-puller.php b/includes/class-puller.php index ef8b9c6..5df7dcc 100644 --- a/includes/class-puller.php +++ b/includes/class-puller.php @@ -1,40 +1,50 @@ get_site_status( $site_id ), array( 'idle', '' ) ) ) { + // Fetch the site status. + if ( ! in_array( $site_manager->get_site_status( $site_id ), array( 'idle', '' ), true ) ) { return false; } - // Mark site as in progress + // Mark site as in progress. $site_manager->update_site_status( 'pulling' ); try { - // Fetch the site's posts by calling the class located at the - // namespace given during registration + /* + * Fetch the site's posts by calling the class located at the + * namespace given during registration. + */ $posts = $client->get_posts( $site_id ); /** @@ -46,9 +56,8 @@ public function process_site( $site_id, $client ) { */ $posts = apply_filters( 'syn_pre_pull_posts', $posts, $site_id, $client ); - // Process the posts we fetched + // Process the posts we fetched. $this->process_posts( $posts, $site_id, $client ); - } catch ( \Exception $e ) { Syndication_Logger::log_post_error( $site_id, @@ -59,10 +68,10 @@ public function process_site( $site_id, $client ) { ); } - // Update site status + // Update site status. $site_manager->update_site_status( 'idle' ); - if ( is_array( $posts ) && ! empty( $posts ) ) { + if ( ! empty( $posts ) && is_array( $posts ) ) { return $posts; } else { return false; @@ -72,9 +81,9 @@ public function process_site( $site_id, $client ) { /** * Process new posts fetched for a feed. * - * @param array $posts An array of new posts fetched. - * @param int $site_id The id of the site being processed. - * @param obj $client The syndication client class instance. + * @param array $posts An array of new posts fetched. + * @param int $site_id The id of the site being processed. + * @param object $client The syndication client class instance. * @throws \Exception. */ public function process_posts( $posts, $site_id, $client ) { @@ -116,8 +125,7 @@ public function process_posts( $posts, $site_id, $client ) { * * @param Types\Post $post The post for processing. * @param int $site_id The id of the site being processed. - * @param obj $client The syndication client class instance. - * + * @param object $client The syndication client class instance. * @return int $post_id The ID of the newly inserted post, or false if processing skipped. */ public function process_post( Types\Post $post, $site_id, $client ) { @@ -156,8 +164,9 @@ public function process_post( Types\Post $post, $site_id, $client ) { 'meta_key' => 'syn_post_guid', 'meta_value' => $syndicated_guid, 'post_status' => 'any', - 'post_per_page' => 1 + 'post_per_page' => 1, ); + $existing_post_query = new \WP_Query( $query_args ); // Get the client transport type, passed to some hooks. @@ -172,7 +181,9 @@ public function process_post( Types\Post $post, $site_id, $client ) { $status = 'skip_update_pulled_posts', $message = sprintf( __( 'skipping post update per update_pulled_posts setting', 'push-syndication' ) ), $log_time = null, - $extra = array( 'post' => $post ) ); + $extra = array( 'post' => $post ) + ); + return false; } @@ -193,7 +204,7 @@ public function process_post( Types\Post $post, $site_id, $client ) { * @param Types\Post $post The post being processed. * @param int $site_id The id of the site being processed. * @param string $client_transport_type The client transport type. - * @param obj $client The syndication client class instance. + * @param object $client The syndication client class instance. */ $edit_shortcircuit = apply_filters( 'syn_pre_pull_edit_post_shortcircuit', false, $post, $site_id, $client_transport_type, $client ); @@ -218,7 +229,7 @@ public function process_post( Types\Post $post, $site_id, $client ) { * * @param Types\Post $post The Post object containing the post update data. * @param int $site_id The id of the site being processed. - * @param obj $client The syndication client class instance. + * @param object $client The syndication client class instance. */ $post = apply_filters( 'syn_pull_edit_post', $post, $site_id, $client ); @@ -232,12 +243,10 @@ public function process_post( Types\Post $post, $site_id, $client ) { * @param Types\Post $post The Post object containing the post update data. * @param int $site_id The id of the site being processed. * @param string $client_transport_type The client transport type. - * @param obj $client The syndication client class instance. + * @param object $client The syndication client class instance. */ do_action( 'syn_post_pull_edit_post', $post_id, $post, $site_id, $client_transport_type, $client ); - } else { - /** * Filter to short circuit the processing of a pulled post insert. * @@ -246,9 +255,10 @@ public function process_post( Types\Post $post, $site_id, $client ) { * @param bool $insert_shortcircuit Whether to short-circuit the inserting of a post. * @param int $site_id The id of the site being processed. * @param string $client_transport_type The client transport type. - * @param obj $client The syndication client class instance. + * @param object $client The syndication client class instance. */ $insert_shortcircuit = apply_filters( 'syn_pre_pull_new_post_shortcircuit', false, $post, $site_id, $client_transport_type, $client ); + if ( true === $insert_shortcircuit ) { Syndication_Logger::log_post_info( $site_id, @@ -260,7 +270,6 @@ public function process_post( Types\Post $post, $site_id, $client ) { return false; } - // Include the syndicated_guid so we can update this post later. $post->post_meta['syn_post_guid'] = $post->post_data['post_guid']; @@ -271,7 +280,7 @@ public function process_post( Types\Post $post, $site_id, $client ) { * * @param Types\Post $post The Post object containing the post insert data. * @param int $site_id The id of the site being processed. - * @param obj $client The syndication client class instance. + * @param object $client The syndication client class instance. */ $post = apply_filters( 'syn_pull_new_post', $post, $site_id, $client ); @@ -285,11 +294,11 @@ public function process_post( Types\Post $post, $site_id, $client ) { * @param Types\Post $post The Post object containing the post insert data. * @param int $site_id The id of the site being processed. * @param string $client_transport_type The client transport type. - * @param obj $client The syndication client class instance. + * @param object $client The syndication client class instance. */ do_action( 'syn_post_pull_new_post', $post_id, $post, $site_id, $client_transport_type, $client ); - } + wp_reset_postdata(); if ( ! is_wp_error_do_throw( $post_id ) ) { @@ -305,10 +314,10 @@ public function process_post( Types\Post $post, $site_id, $client ) { /** * Add meta to a post * - * @param int $post_id The ID of the post for which to insert the given meta - * @param array $post_meta Associative array of meta to add to the post + * @param int $post_id The ID of the post for which to insert the given meta. + * @param array $post_meta Associative array of meta to add to the post. * @throws \Exception - * @return mixed False on failure + * @return mixed False on failure. */ public function process_post_meta( $post_id, $post_meta ) { // @todo Validate again if this method remains public. @@ -320,26 +329,28 @@ public function process_post_meta( $post_id, $post_meta ) { * @param int $post_id The ID of the post for which to insert the given meta. */ $post_meta = apply_filters( 'syn_before_update_post_meta', $post_meta, $post_id ); - //handle enclosures separately first - $enc_field = isset( $post_meta['enc_field'] ) ? $post_meta['enc_field'] : null; + + // Handle enclosures separately first. + $enc_field = isset( $post_meta['enc_field'] ) ? $post_meta['enc_field'] : null; $enclosures = isset( $post_meta['enclosures'] ) ? $post_meta['enclosures'] : null; - if ( isset( $enclosures ) && isset ( $enc_field ) ) { - // first remove all enclosures for the post (for updates) if any - delete_post_meta( $post_id, $enc_field); - foreach( $enclosures as $enclosure ) { - if (defined('ENCLOSURES_AS_STRINGS') && constant('ENCLOSURES_AS_STRINGS')) { - $enclosure = implode("\n", $enclosure); + + if ( isset( $enclosures ) && isset( $enc_field ) ) { + // First remove all enclosures for the post (for updates) if any. + delete_post_meta( $post_id, $enc_field ); + + foreach ( $enclosures as $enclosure ) { + if ( defined( 'ENCLOSURES_AS_STRINGS' ) && constant( 'ENCLOSURES_AS_STRINGS' ) ) { + $enclosure = implode( "\n", $enclosure ); } add_post_meta( $post_id, $enc_field, $enclosure, false ); } - // now remove them from the rest of the metadata before saving the rest - unset($post_meta['enclosures']); + // Now remove them from the rest of the metadata before saving the rest. + unset( $post_meta['enclosures'] ); } if ( is_array( $post_meta ) && ! empty( $post_meta ) ) { - foreach ( $post_meta as $key => $value ) { update_post_meta( $post_id, $key, $value ); } @@ -351,12 +362,11 @@ public function process_post_meta( $post_id, $post_meta ) { /** * Add taxonomy terms to a post * - * @param int $post_id The ID of the post for which to insert the given meta - * @param array $post_terms Associative array of taxonomy|terms to add to the post + * @param int $post_id The ID of the post for which to insert the given meta. + * @param array $post_terms Associative array of taxonomy|terms to add to the post. * @throws \Exception */ public function process_post_terms( $post_id, $post_terms ) { - // @todo Validate again if this method remains public. /** @@ -380,7 +390,7 @@ public function process_post_terms( $post_id, $post_terms ) { /** * Test the connection with the slave site. * - * @param string $remote_url The remote URL + * @param string $remote_url The remote URL. * @return bool True on success; false on failure. */ public function test_connection( $remote_url = '' ) { @@ -390,7 +400,7 @@ public function test_connection( $remote_url = '' ) { /** * Fetch a remote url. * - * @param string $remote_url The remote URL + * @param string $remote_url The remote URL. * @return string|\WP_Error The content of the remote feed, or error if there's a problem. */ public function remote_get( $remote_url = '' ) { @@ -418,12 +428,12 @@ public function remote_get( $remote_url = '' ) { * @param array $feed_enclosures Optional. * @param array $enc_nodes Optional. * @param bool $enc_is_photo - * @return array The list of enclosures in the feed. + * @return bool|array The list of enclosures in the feed. */ public function get_enclosures( $feed_enclosures = array(), $enc_nodes = array(), $enc_is_photo = false ) { $enclosures = array(); foreach ( $feed_enclosures as $count => $enc ) { - if ( isset( $enc_is_photo ) && 1 == $enc_is_photo ) { + if ( isset( $enc_is_photo ) && 1 === $enc_is_photo ) { $enc_array = array( 'caption' => '', 'credit' => '', @@ -441,24 +451,25 @@ public function get_enclosures( $feed_enclosures = array(), $enc_nodes = array() foreach ( $enc_nodes as $post_value ) { try { - if ( 'string(' == substr( $post_value['xpath'], 0, 7 ) ) { + if ( 'string(' === substr( $post_value['xpath'], 0, 7 ) ) { $enc_value[0] = substr( $post_value['xpath'], 7, strlen( $post_value['xpath'] ) - 8 ); } else { $enc_value = $enc->xpath( stripslashes( $post_value['xpath'] ) ); } $enc_array[ $post_value['field'] ] = esc_attr( (string) $enc_value[0] ); - } - catch ( Exception $e ) { + } catch ( \Exception $e ) { return false; } } - // if position is not provided in the feed, use the order in which they appear in the feed + + // If position is not provided in the feed, use the order in which they appear in the feed. if ( empty( $enc_array['position'] ) ) { $enc_array['position'] = $count; } + $enclosures[] = $enc_array; } + return $enclosures; } } - diff --git a/includes/class-syndication-logger.php b/includes/class-syndication-logger.php index f211882..9ff593b 100644 --- a/includes/class-syndication-logger.php +++ b/includes/class-syndication-logger.php @@ -1,48 +1,65 @@ log_id = md5( uniqid() . microtime() ); + if ( is_admin() ) { require_once( dirname( __FILE__ ) . '/class-syndication-logger-viewer.php' ); $viewer = new Syndication_Logger_Viewer; } } - /* - * Use this singleton to address non-static methods + /** + * Use this singleton to address non-static methods. + * + * @return object */ public static function instance() { - if ( self::$__instance == null ) { + if ( null === self::$__instance ) { self::$__instance = new self(); } return self::$__instance; @@ -110,63 +129,64 @@ public static function instance() { /** - * Log a new post creation event - * usually implemented via action hook + * Log a new post creation event usually implemented via action hook. + * * do_action( 'syn_post_pull_new_post', $result, $post, $site, $transport_type, $client ); * do_action( 'syn_post_push_new_post', $result, $post_ID, $site, $transport_type, $client, $info ); * - * @param mixed $result Result object of previous wp_insert_post action - * @param mixed $post Post object or post_id - * @param object $site Post object for the site doing the syndication - * @param string $transport_type Post meta syn_transport_type for site - * @param object $client Syndication_Client class + * @param mixed $result Result object of previous wp_insert_post action. + * @param mixed $post Post object or post_id. + * @param integer $site_id ID of the site doing the syndication. + * @param string $transport_type Post meta syn_transport_type for site. + * @param object $client Syndication_Client class. */ - public static function log_new( $result, $post, $site, $transport_type, $client ) { - self::instance()->log_post_event( 'new', $result, $post, $site, $transport_type, $client ); + public static function log_new( $result, $post, $site_id, $transport_type, $client ) { + self::instance()->log_post_event( 'new', $result, $post, $site_id, $transport_type, $client ); } /** - * Log a post update event - * usually implemented via action hook + * Log a post update event usually implemented via action hook + * * do_action( 'syn_post_pull_edit_post', $result, $post, $site, $transport_type, $client ); * do_action( 'syn_post_push_edit_post', $result, $post_ID, $site, $transport_type, $client, $info ); * - * @param mixed $result Result object of previous wp_insert_post action - * @param mixed $post Post object or post_id - * @param object $site Post object for the site doing the syndication - * @param string $transport_type Post meta syn_transport_type for site - * @param object $client Syndication_Client class + * @param mixed $result Result object of previous wp_insert_post action. + * @param mixed $post Post object or post_id. + * @param integer $site_id ID of the site doing the syndication. + * @param string $transport_type Post meta syn_transport_type for site. + * @param object $client Syndication_Client class. */ - public static function log_update( $result, $post, $site, $transport_type, $client ) { - self::instance()->log_post_event( 'update', $result, $post, $site, $transport_type, $client ); + public static function log_update( $result, $post, $site_id, $transport_type, $client ) { + self::instance()->log_post_event( 'update', $result, $post, $site_id, $transport_type, $client ); } /** - * Log a post delete event - * usually implemented via action hook + * Log a post delete event usually implemented via action hook. + * * do_action( 'syn_post_push_delete_post', $result, $ext_ID, $post_ID, $site_ID, $transport_type, $client ); * - * @param mixed $result Result object of previous wp_insert_post action - * @param mixed $external_id External post post_id - * @param mixed $post Post object or post_id - * @param object $site Post object for the site doing the syndication - * @param string $transport_type Post meta syn_transport_type for site - * @param object $client Syndication_Client class + * @param mixed $result Result object of previous wp_insert_post action. + * @param mixed $external_id External post post_id. + * @param mixed $post Post object or post_id. + * @param integer $site_id ID of the site doing the syndication. + * @param string $transport_type Post meta syn_transport_type for site. + * @param object $client Syndication_Client class. */ - public static function log_delete( $result, $external_id, $post, $site, $transport_type, $client ) { - self::instance()->log_post_event( 'delete', $result, $post, $site, $transport_type, $client ); + public static function log_delete( $result, $external_id, $post, $site_id, $transport_type, $client ) { + self::instance()->log_post_event( 'delete', $result, $post, $site_id, $transport_type, $client ); } /** - * Prepares data for the post level log events - * @param string $event Type of event new/update/delete - * @param mixed $result Result object of previous wp_insert_post action - * @param mixed $post Post object or post_id - * @param object $site Post object for the site doing the syndication - * @param string $transport_type Post meta syn_transport_type for site - * @param object $client Syndication_Client class + * Prepares data for the post level log events. + * + * @param string $event Type of event new/update/delete. + * @param mixed $result Result object of previous wp_insert_post action. + * @param mixed $post Post object or post_id. + * @param integer $site_id ID of the site doing the syndication. + * @param string $transport_type Post meta syn_transport_type for site. + * @param object $client Syndication_Client class. */ - private function log_post_event( $event, $result, $post, $site, $transport_type, $client ) { + private function log_post_event( $event, $result, $post, $site_id, $transport_type, $client ) { if ( is_int( $post ) ) { $post = get_post( $post, ARRAY_A ); } @@ -184,27 +204,28 @@ private function log_post_event( $event, $result, $post, $site, $transport_type, 'client' => $client, ); - if ( false == $result || is_wp_error( $result ) ) { + if ( false === $result || is_wp_error( $result ) ) { if ( is_wp_error( $result ) ) { $message = $result->get_error_message(); } else { $message = 'fail'; } - Syndication_Logger::log_post_error( $site->ID, $status = __( esc_attr( $event ), 'push-syndication' ), $message, $log_time, $extra ); + Syndication_Logger::log_post_error( $site_id, $status = esc_attr( $event ), $message, $log_time, $extra ); } else { $guid = isset( $post->post_data['post_guid'] ) ? sanitize_text_field( $post->post_data['post_guid'] ) : sanitize_text_field( $post->post_data['guid'] ); $message = sprintf( '%s,%d', $guid, intval( $result ) ); - Syndication_Logger::log_post_success( $site->ID, $status = __( esc_attr( $event ), 'push-syndication' ), $message, $log_time, $extra ); + Syndication_Logger::log_post_success( $site_id, $status = esc_attr( $event ), $message, $log_time, $extra ); } } /** - * Log a faulty post level event - * @param int $post_id post_id to attach the log entry to - * @param string $status status entry - * @param string $message log message - * @param string $log_time time of event - * @param array $extra additional data + * Log a faulty post level event. + * + * @param int $post_id Post_id to attach the log entry to. + * @param string $status Status entry. + * @param string $message Log message. + * @param string $log_time Time of event. + * @param array $extra Additional data. */ public static function log_post_error( $post_id, $status = 'error', $message = '', $log_time = '', $extra = array() ) { self::instance()->log_post( 'error', $post_id, $status, $message, $log_time, $extra ); @@ -212,56 +233,60 @@ public static function log_post_error( $post_id, $status = 'error', $message = ' /** * Log a successful post level event - * @param int $post_id post_id to attach the log entry to - * @param string $status status entry - * @param string $message log message - * @param string $log_time time of event - * @param array $extra additional data + * + * @param int $post_id Post_id to attach the log entry to. + * @param string $status Status entry. + * @param string $message Log message. + * @param string $log_time Time of event. + * @param array $extra Additional data. */ public function log_post_success( $post_id, $status = 'ok', $message = '', $log_time = '', $extra = array() ) { self::instance()->log_post( 'success', $post_id, $status, $message, $log_time, $extra ); } /** - * Log a post level informal event - * @param int $post_id post_id to attach the log entry to - * @param string $status status entry - * @param string $message log message - * @param string $log_time time of event - * @param array $extra additional data + * Log a post level informal event. + * + * @param int $post_id Post_id to attach the log entry to. + * @param string $status Status entry. + * @param string $message Log message. + * @param string $log_time Time of event. + * @param array $extra Additional data. */ public static function log_post_info( $post_id, $status = '', $message = '', $log_time = '', $extra = array() ) { self::instance()->log_post( 'info', $post_id, $status, $message, $log_time, $extra ); } /** - * Pass post level events to logger function - * @param string $msg_type event type success/error/info - * @param int $post_id post_id to attach the log entry to - * @param string $status status entry - * @param string $message log message - * @param string $log_time time of event - * @param array $extra additional data + * Pass post level events to logger function. + * + * @param string $msg_type Event type success/error/info. + * @param int $post_id Post_id to attach the log entry to. + * @param string $status Status entry. + * @param string $message Log message. + * @param string $log_time Time of event. + * @param array $extra Additional data. */ private function log_post( $msg_type, $post_id, $status, $message, $log_time, $extra ) { $this->log( $storage_type = 'object', $msg_type, $object_type = 'post', $object_id = $post_id, $status, $message, $log_time, $extra ); } /** - * Log an entry to the database - * @param string $storage_type Where the log entry will be stored. object / option - * @param string $msg_type event type success/error/info - * @param string $object_type Type of object to attach log entry to. Currently only "post" is supported - * @param int $object_id object_id (post_id) to attach the log entry to - * @param string $status status entry - * @param string $message log message - * @param string $log_time time of event - * @param array $extra additional data - * @return mixed true or WP_Error + * Log an entry to the database. + * + * @param string $storage_type Where the log entry will be stored. object / option. + * @param string $msg_type Event type success/error/info. + * @param string $object_type Type of object to attach log entry to. Currently only "post" is supported. + * @param mixed $object_id Object_id (post_id) to attach the log entry to. + * @param string $status Status entry. + * @param string $message Log message. + * @param string $log_time Time of event. + * @param array $extra Additional data. + * @return mixed True or WP_Error. */ private function log( $storage_type, $msg_type, $object_type = 'post', $object_id = '', $status, $message, $log_time, $extra ) { // Don't log infos depending on debug level - if ( 'info' == $msg_type && 'info' != $this->debug_level ) { + if ( 'info' === $msg_type && 'info' !== $this->debug_level ) { return; } @@ -287,9 +312,9 @@ private function log( $storage_type, $msg_type, $object_type = 'post', $object_i } if ( ! empty( $log_time ) ) { - $log_entry['time'] = date('Y-m-d H:i:s', strtotime( $log_time ) ); + $log_entry['time'] = date( 'Y-m-d H:i:s', strtotime( $log_time ) ); } else { - $log_entry['time'] = current_time('mysql'); + $log_entry['time'] = current_time( 'mysql' ); } if ( ! empty( $extra ) && is_array( $extra ) ) { @@ -302,10 +327,10 @@ private function log( $storage_type, $msg_type, $object_type = 'post', $object_i error_log( $this->format_log_message( $msg_type, $log_entry ) ); } - if ( 'object' == $storage_type ) { + if ( 'object' === $storage_type ) { // Storing the log alongside the object - if ( 'post' == $object_type ) { + if ( 'post' === $object_type ) { if ( ! is_integer( $object_id ) ) { return new \WP_Error( 'logger_no_post_id', __( 'You need to provide a valid post_id or use log_option instead', 'push-syndication' ) ); @@ -316,14 +341,14 @@ private function log( $storage_type, $msg_type, $object_type = 'post', $object_i return new \WP_Error( 'logger_no_post', __( 'The post_id provided does not exist.', 'push-syndication' ) ); } - $log = get_post_meta( $post->ID, 'syn_log', true); + $log = get_post_meta( $post->ID, 'syn_log', true ); if ( empty( $log ) ) { $log[0] = $log_entry; } else { if ( count( $log ) >= $this->log_entry_limit ) { - // Slice the array to keep the log size in the limits - $offset = count ( $log ) - $this->log_entry_limit; + // Slice the array to keep the log size in the limits. + $offset = count( $log ) - $this->log_entry_limit; $log = array_slice( $log, $offset + 1 ); } @@ -331,9 +356,9 @@ private function log( $storage_type, $msg_type, $object_type = 'post', $object_i } update_post_meta( $post->ID, 'syn_log', $log ); - if ( 'success' == $msg_type ) { + if ( 'success' === $msg_type ) { update_post_meta( $post->ID, 'syn_log_errors', 0 ); - } else if ( 'error' == $msg_type ) { + } elseif ( 'error' === $msg_type ) { // track failures since last success $errors = get_post_meta( $post->ID, 'syn_log_errors', true ); $errors = (int) $errors + 1; @@ -345,20 +370,19 @@ private function log( $storage_type, $msg_type, $object_type = 'post', $object_i } // @TODO log error counter - } else if ( 'term' == $object_type ) { + } elseif ( 'term' === $object_type ) { // @TODO implement if needed } - - } else if ( 'option' == $storage_type ) { - // Storing the log in an option value - + } elseif ( 'option' === $storage_type ) { + // Storing the log in an option value. $log = get_option( 'syn_log', true ); + if ( empty( $log ) ) { $log[0] = $log_entry; } else { if ( count( $log ) >= $this->log_entry_limit ) { - // Slice the array to keep the log size in the limits - $offset = count ( $log ) - $this->log_entry_limit; + // Slice the array to keep the log size in the limits. + $offset = count( $log ) - $this->log_entry_limit; $log = array_slice( $log, $offset + 1 ); } @@ -366,14 +390,15 @@ private function log( $storage_type, $msg_type, $object_type = 'post', $object_i } update_option( 'syn_log', $log ); - if ( 'success' == $msg_type ) { + if ( 'success' === $msg_type ) { update_option( 'syn_log_errors', 0 ); - } else if ( 'error' == $msg_type ) { - // track failures since last success + } elseif ( 'error' === $msg_type ) { + // Track failures since last success. $errors = get_option( 'syn_log_errors' ); $errors = (int) $errors + 1; update_option( 'syn_log_errors', $errors ); - // track overall failures + + // Track overall failures. $errors = get_option( 'syn_log_errors_overall' ); $errors = (int) $errors + 1; update_option( 'syn_log_errors_overall', $errors ); @@ -383,10 +408,11 @@ private function log( $storage_type, $msg_type, $object_type = 'post', $object_i } /** - * Format log message for error_log() - * @param string $msg_type Type of message - * @param array $log_entry Prepared log_entry array - * @return string Formatted log message + * Format log message for error_log(). + * + * @param string $msg_type Type of message. + * @param array $log_entry Prepared log_entry array. + * @return string Formatted log message. */ private function format_log_message( $msg_type, $log_entry ) { /** @@ -401,82 +427,86 @@ private function format_log_message( $msg_type, $log_entry ) { } /** - * Retrieve and filter log messages from database - * @param string $log_id unique log session id - * @param string $msg_type event type success/error/info - * @param int $object_id object_id (post_id) to attach the log entry to - * @param string $object_type Type of object to attach log entry to. Currently only "post" is supported - * @param string $status status entry - * @param string $date_start Date string for starting date filter - * @param string $date_end Date string for starting date filter - * @param string $message regular expression for message text matching - * @param string $storage_type Where the log entry will be stored. object / option - * @return array Array of matching log entries + * Retrieve and filter log messages from database. + * + * @param string $log_id Unique log session id. + * @param string $msg_type Event type success/error/info. + * @param int $object_id Object_id (post_id) to attach the log entry to. + * @param string $object_type Type of object to attach log entry to. Currently only "post" is supported. + * @param string $status Status entry. + * @param string $date_start Date string for starting date filter. + * @param string $date_end Date string for end date filter. + * @param string $message Regular expression for message text matching. + * @param string $storage_type Where the log entry will be stored. object / option. + * @return array Array of matching log entries. */ public static function get_messages( $log_id = null, $msg_type = null, $object_id = null, $object_type = 'post', $status = null, $date_start = null, $date_end = null, $message = null, $storage_type = 'object' ) { - $log_entries = array(); - if ( 'object' == $storage_type ) { - if ( 'post' == $object_type ) { + if ( 'object' === $storage_type ) { + if ( 'post' === $object_type ) { if ( ! empty( $object_id ) ) { - $log_entries[$object_id] = get_post_meta( $object_id, 'syn_log' ); + $log_entries[ $object_id ] = get_post_meta( $object_id, 'syn_log' ); } else { global $wpdb; - /** + /* * Direct database call without caching: * This call may return objects larger than 1 MB and is usually only called infrequently * from the admin dashboard. Implementing a segmented object caching for this result seems * unnecessary. - * @TODO implement walker + * + * @todo implement walker */ $all_log_entries = $wpdb->get_results( "SELECT post_id, meta_value FROM $wpdb->postmeta WHERE meta_key = 'syn_log' GROUP BY post_id ORDER BY meta_id DESC LIMIT 0, 100" ); // cache pass (see note above) - foreach( $all_log_entries as $log_entry ) { - $log_entries[$log_entry->post_id] = unserialize( $log_entry->meta_value ); + + foreach ( $all_log_entries as $log_entry ) { + $log_entries[ $log_entry->post_id ] = unserialize( $log_entry->meta_value ); } } } } $filter = array(); - foreach( array( 'log_id', 'msg_type', 'object_type', 'status', 'date_start', 'date_end', 'message' ) as $filter_key ) { + foreach ( array( 'log_id', 'msg_type', 'object_type', 'status', 'date_start', 'date_end', 'message' ) as $filter_key ) { if ( ! empty( ${$filter_key} ) ) { - $filter[$filter_key] = ${$filter_key}; + $filter[ $filter_key ] = ${$filter_key}; } } self::instance()->log_filter = $filter; - foreach( $log_entries as $object_id => $entries ) { + foreach ( $log_entries as $object_id => $entries ) { $entries = array_filter( $entries, array( self::instance(), 'filter_log_entries' ) ); - $log_entries[$object_id] = $entries; + $log_entries[ $object_id ] = $entries; } return $log_entries; } /** - * Retrieve log_filter variable - * @return array Log filter conditions + * Retrieve log_filter variable. + * + * @return array Log filter conditions. */ public function get_log_filter() { return $this->log_filter; } /** - * Filter retrieved log entries by log_filter conditions - * @uses array_filter() - * @param array $data Array Element - * @return boolean True to keep the entry, false to skip it + * Filter retrieved log entries by log_filter conditions. + * + * @uses array_filter() + * @param array $data Array Element. + * @return boolean True to keep the entry, false to skip it. */ public function filter_log_entries( $data ) { $filter = $this->get_log_filter(); - foreach( $filter as $key => $value ) { - switch( $key ) { + foreach ( $filter as $key => $value ) { + switch ( $key ) { case 'log_id': case 'msg_type': case 'object_type': case 'status': - if ( ! isset( $data[$key] ) || $data[$key] <> $value ) { + if ( ! isset( $data[ $key ] ) || $data[ $key ] <> $value ) { return false; } break; diff --git a/includes/class-syndication-runner.php b/includes/class-syndication-runner.php index e05c5ba..fe61973 100644 --- a/includes/class-syndication-runner.php +++ b/includes/class-syndication-runner.php @@ -1,9 +1,6 @@ site_ID = $site_ID; + + $this->set_feed_url( get_post_meta( $site_ID, 'syn_feed_url', true ) ); + + $this->set_cache_class( 'WP_Feed_Cache' ); + + $this->default_post_type = get_post_meta( $site_ID, 'syn_default_post_type', true ); + $this->default_post_status = get_post_meta( $site_ID, 'syn_default_post_status', true ); + $this->default_comment_status = get_post_meta( $site_ID, 'syn_default_comment_status', true ); + $this->default_ping_status = get_post_meta( $site_ID, 'syn_default_ping_status', true ); + $this->default_cat_status = get_post_meta( $site_ID, 'syn_default_cat_status', true ); + + add_action( 'syn_post_pull_new_post', array( __CLASS__, 'save_meta' ), 10, 5 ); + add_action( 'syn_post_pull_new_post', array( __CLASS__, 'save_tax' ), 10, 5 ); + add_action( 'syn_post_pull_edit_post', array( __CLASS__, 'update_meta' ), 10, 5 ); + add_action( 'syn_post_pull_edit_post', array( __CLASS__, 'update_tax' ), 10, 5 ); + } + + public static function get_client_data() { + return array( 'id' => 'WP_RSS', 'modes' => array( 'pull' ), 'name' => 'RSS' ); + } + + public function new_post($post_ID) { + // Not supported + return false; + } + + public function edit_post($post_ID, $ext_ID) { + // Not supported + return false; + } + + public function delete_post($ext_ID) { + // Not supported + return false; + } + + public function test_connection() { + // TODO: Implement test_connection() method. + return true; + } + + public function is_post_exists($ext_ID) { + // Not supported + return false; + } + + public static function display_settings($site) { + + $feed_url = get_post_meta( $site->ID, 'syn_feed_url', true ); + $default_post_type = get_post_meta( $site->ID, 'syn_default_post_type', true ); + $default_post_status = get_post_meta( $site->ID, 'syn_default_post_status', true ); + $default_comment_status = get_post_meta( $site->ID, 'syn_default_comment_status', true ); + $default_ping_status = get_post_meta( $site->ID, 'syn_default_ping_status', true ); + $default_cat_status = get_post_meta( $site->ID, 'syn_default_cat_status', true ); + + ?> + +

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+ + site_ID ); + + $rss_init = $this->init(); + + if ( false === $rss_init ) { + Syndication_Logger::log_post_error( $this->site_ID, $status = 'error', $message = sprintf( __( 'Failed to parse feed at: %s', 'push-syndication' ), $this->feed_url ), $log_time = isset( $site_post->postmeta['is_update'] ) ? $site_post->postmeta['is_update'] : null, $extra = array( 'error' => $this->error() ) ); + + // Track the event. + do_action( 'push_syndication_event', 'pull_failure', $this->site_ID ); + } else { + Syndication_Logger::log_post_info( $this->site_ID, $status = 'fetch_feed', $message = sprintf( __( 'fetched feed with %d bytes', 'push-syndication' ), strlen( $this->get_raw_data() ) ), $log_time = null, $extra = array() ); + + // Track the event. + do_action( 'push_syndication_event', 'pull_success', $this->site_ID ); + } + + $this->handle_content_type(); + + // hold all the posts + $posts = array(); + $taxonomy = array( 'cats' => array(), 'tags' => array() ); + + foreach( $this->get_items() as $item ) { + if ( 'yes' == $this->default_cat_status ) { + $taxonomy = $this->set_taxonomy( $item ); + } + + $post = array( + 'post_title' => $item->get_title(), + 'post_content' => $item->get_content(), + 'post_excerpt' => $item->get_description(), + 'post_type' => $this->default_post_type, + 'post_status' => $this->default_post_status, + 'post_date' => date( 'Y-m-d H:i:s', strtotime( $item->get_date() ) ), + 'comment_status' => $this->default_comment_status, + 'ping_status' => $this->default_ping_status, + 'post_guid' => $item->get_id(), + 'post_category' => $taxonomy['cats'], + 'tags_input' => $taxonomy['tags'] + ); + // This filter can be used to exclude or alter posts during a pull import + $post = apply_filters( 'syn_rss_pull_filter_post', $post, $args, $item ); + if ( false === $post ) + continue; + $posts[] = $post; + } + + return $posts; + + } + + public function set_taxonomy( $item ) { + $cats = $item->get_categories(); + $ids = array( + 'cats' => array(), + 'tags' => array() + ); + + foreach ( $cats as $cat ) { + // checks if term exists + if ( $result = get_term_by( 'name', $cat->term, 'category' ) ) { + if ( isset( $result->term_id ) ) { + $ids['cats'][] = $result->term_id; + } + } elseif ( $result = get_term_by( 'name', $cat->term, 'post_tag' ) ) { + if ( isset( $result->term_id ) ) { + $ids['tags'][] = $result->term_id; + } + } else { + // creates if not + $result = wp_insert_term( $cat->term, 'category' ); + if ( isset( $result->term_id ) ) { + $ids['cats'][] = $result->term_id; + } + } + } + + // returns array ready for post creation + return $ids; + } + + public static function save_meta( $result, $post, $site, $transport_type, $client ) { + if ( ! $result || is_wp_error( $result ) || ! isset( $post['postmeta'] ) ) { + return false; + } + $categories = $post['post_category']; + wp_set_post_terms($result, $categories, 'category', true); + $metas = $post['postmeta']; + + //handle enclosures separately first + $enc_field = isset( $metas['enc_field'] ) ? $metas['enc_field'] : null; + $enclosures = isset( $metas['enclosures'] ) ? $metas['enclosures'] : null; + if ( isset( $enclosures ) && isset ( $enc_field ) ) { + // first remove all enclosures for the post (for updates) if any + delete_post_meta( $result, $enc_field); + foreach( $enclosures as $enclosure ) { + if (defined('ENCLOSURES_AS_STRINGS') && constant('ENCLOSURES_AS_STRINGS')) { + $enclosure = implode("\n", $enclosure); + } + add_post_meta($result, $enc_field, $enclosure, false); + } + + // now remove them from the rest of the metadata before saving the rest + unset($metas['enclosures']); + } + + foreach ($metas as $meta_key => $meta_value) { + add_post_meta($result, $meta_key, $meta_value, true); + } + } + + public static function update_meta( $result, $post, $site, $transport_type, $client ) { + if ( ! $result || is_wp_error( $result ) || ! isset( $post['postmeta'] ) ) { + return false; + } + $categories = $post['post_category']; + wp_set_post_terms($result, $categories, 'category', true); + $metas = $post['postmeta']; + + // handle enclosures separately first + $enc_field = isset( $metas['enc_field'] ) ? $metas['enc_field'] : null; + $enclosures = isset( $metas['enclosures'] ) ? $metas['enclosures'] : null; + if ( isset( $enclosures ) && isset( $enc_field ) ) { + // first remove all enclosures for the post (for updates) + delete_post_meta( $result, $enc_field); + foreach( $enclosures as $enclosure ) { + if (defined('ENCLOSURES_AS_STRINGS') && constant('ENCLOSURES_AS_STRINGS')) { + $enclosure = implode("\n", $enclosure); + } + add_post_meta($result, $enc_field, $enclosure, false); + } + + // now remove them from the rest of the metadata before saving the rest + unset($metas['enclosures']); + } + + foreach ($metas as $meta_key => $meta_value) { + update_post_meta($result, $meta_key, $meta_value); + } + } + + public static function save_tax( $result, $post, $site, $transport_type, $client ) { + if ( ! $result || is_wp_error( $result ) || ! isset( $post['tax'] ) ) { + return false; + } + $taxonomies = $post['tax']; + foreach ( $taxonomies as $tax_name => $tax_value ) { + // post cannot be used to create new taxonomy + if ( ! taxonomy_exists( $tax_name ) ) { + continue; + } + wp_set_object_terms($result, (string)$tax_value, $tax_name, true); + } + } + + public static function update_tax( $result, $post, $site, $transport_type, $client ) { + if ( ! $result || is_wp_error( $result ) || ! isset( $post['tax'] ) ) { + return false; + } + $taxonomies = $post['tax']; + $replace_tax_list = array(); + foreach ( $taxonomies as $tax_name => $tax_value ) { + //post cannot be used to create new taxonomy + if ( ! taxonomy_exists( $tax_name ) ) { + continue; + } + if ( !in_array($tax_name, $replace_tax_list ) ) { + //if we haven't processed this taxonomy before, replace any terms on the post with the first new one + wp_set_object_terms($result, (string)$tax_value, $tax_name ); + $replace_tax_list[] = $tax_name; + } else { + //if we've already added one term for this taxonomy, append any others + wp_set_object_terms($result, (string)$tax_value, $tax_name, true); + } + } + } +} diff --git a/includes/clients/rss-pull/class-pull-client.php b/includes/clients/rss-pull/class-pull-client.php index 256d480..84c80bc 100644 --- a/includes/clients/rss-pull/class-pull-client.php +++ b/includes/clients/rss-pull/class-pull-client.php @@ -92,8 +92,7 @@ function( $args ) { $taxonomy = $this->set_taxonomy( $item ); } - $new_post = new Types\Post(); - + $new_post = new Types\Post(); $new_post->post_data['post_title'] = $item->get_title(); $new_post->post_data['post_content'] = $item->get_content(); $new_post->post_data['post_excerpt'] = $item->get_description(); @@ -105,10 +104,8 @@ function( $args ) { $new_post->post_data['post_guid'] = $item->get_id(); $new_post->post_data['post_category'] = isset( $taxonomy['cats'] ) ? $taxonomy['cats'] : ''; $new_post->post_data['tags_input'] = isset( $taxonomy['tags'] ) ? $taxonomy['tags'] : ''; + $new_post->post_meta['site_id'] = $site_id; - $new_post->post_meta['site_id'] = $site->ID; - - // This filter can be used to exclude or alter posts during a pull import /** * Filter the post used by the RSS pull client when pulling an update. * @@ -119,13 +116,16 @@ function( $args ) { * @param int $post_ID The id of the post originating this request. */ $new_post = apply_filters( 'syn_rss_pull_filter_post', $new_post, array(), $item ); + if ( false === $new_post ) { continue; } + $posts[] = $new_post; } + /* This action is documented in includes/clients/rss-pull/class-pull-client.php */ - do_action( 'push_syndication_event', 'pull_success', $site->ID ); + do_action( 'push_syndication_event', 'pull_success', $site_id ); return $posts; } @@ -133,12 +133,12 @@ function( $args ) { public function set_taxonomy( $item ) { $cats = $item->get_categories(); $ids = array( - 'cats' => array(), - 'tags' => array() + 'cats' => array(), + 'tags' => array(), ); foreach ( $cats as $cat ) { - // checks if term exists + // Checks if term exists. if ( $result = get_term_by( 'name', $cat->term, 'category' ) ) { if ( isset( $result->term_id ) ) { $ids['cats'][] = $result->term_id; @@ -148,7 +148,7 @@ public function set_taxonomy( $item ) { $ids['tags'][] = $result->term_id; } } else { - // creates if not + // Creates if not. $result = wp_insert_term( $cat->term, 'category' ); if ( isset( $result->term_id ) ) { $ids['cats'][] = $result->term_id; @@ -156,7 +156,7 @@ public function set_taxonomy( $item ) { } } - // returns array ready for post creation + // Returns array ready for post creation. return $ids; } diff --git a/includes/clients/xml-push/class-push-client.php b/includes/clients/xml-push/class-push-client.php index ed1a7f8..622c2d2 100644 --- a/includes/clients/xml-push/class-push-client.php +++ b/includes/clients/xml-push/class-push-client.php @@ -511,14 +511,14 @@ public function is_post_exists( $remote_post_id ) { protected function convert_date_gmt( $date_gmt, $date ) { if ( $date !== '0000-00-00 00:00:00' && $date_gmt === '0000-00-00 00:00:00' ) { - return new IXR_Date( get_gmt_from_date( mysql2date( 'Y-m-d H:i:s', $date, false ), 'Ymd\TH:i:s' ) ); + return new \IXR_Date( get_gmt_from_date( mysql2date( 'Y-m-d H:i:s', $date, false ), 'Ymd\TH:i:s' ) ); } return $this->convert_date( $date_gmt ); } protected function convert_date( $date ) { if ( $date === '0000-00-00 00:00:00' ) { - return new IXR_Date( '00000000T00:00:00Z' ); + return new \IXR_Date( '00000000T00:00:00Z' ); } return new \IXR_Date( mysql2date( 'Ymd\TH:i:s', $date, false ) ); } diff --git a/phpunit.xml b/phpunit.xml index 44f0fdb..4fc2a33 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -6,6 +6,9 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" > + + + ./tests/ diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..44f0fdb --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,14 @@ + + + + ./tests/ + + + diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..cf04448 --- /dev/null +++ b/readme.md @@ -0,0 +1,8 @@ +## Contributing ## + +If you are interested in contributing, we need help with two main areas: + +1. Fixing bugs in v1 +2. Feature development in v2 + +Issues have been created for each of the planned feature developments. Help with documentation for both versions is also greatly appreciated. diff --git a/tests/bootstrap.php b/tests/bootstrap.php index a1ae6d6..b91b3bb 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,14 +1,25 @@ factory->term->create_and_get( array( + 'taxonomy' => 'syn_sitegroup', + 'name' => 'Test Syndication Endpoint Group', + ) ); + + // Create a site. + $this->site = $this->factory->post->create_and_get( array( + 'post_title' => 'RSS Pull Syndication Endpoint', + 'post_type' => 'syn_site', + ) ); + + /* + * RSS Pull is the "WP_RSS" transport type. + * + * Use the VIP feed which isn't ideal, but mocking SimplePie is a challenge. + * In the future we should use dependency injection and a wrapper for + * fetching a feed. + */ + add_post_meta( $this->site->ID, 'syn_transport_type', 'WP_RSS' ); + add_post_meta( $this->site->ID, 'syn_feed_url', 'https://vip.wordpress.com/feed' ); + add_post_meta( $this->site->ID, 'syn_default_post_type', 'post' ); + add_post_meta( $this->site->ID, 'syn_default_post_status', 'publish' ); + add_post_meta( $this->site->ID, 'syn_default_comment_status', 'open' ); + add_post_meta( $this->site->ID, 'syn_default_ping_status', 'open' ); + add_post_meta( $this->site->ID, 'syn_default_cat_status', 'yes' ); + add_post_meta( $this->site->ID, 'syn_site_enabled', 'on' ); + + // Add the site to the sitegroup. + wp_set_object_terms( $this->site->ID, $sitegroup->term_id, 'syn_sitegroup' ); + + $this->client = new Pull_Client(); + $this->client->init( $this->site->ID ); + } + + /** + * Clean up after yourself. + * + * @since 2.1 + */ + public function tearDown() { + parent::tearDown(); + + $q = new \WP_Query( array( + 'post_type' => array( 'syn_site', 'syn_sitegroup', 'post' ), + 'posts_per_page' => -1, + ) ); + + $post_ids = wp_list_pluck( $q->posts, 'ID' ); + + foreach ( $post_ids as $post_id ) { + wp_delete_post( $post_id ); + } + } + + /** + * Test that processing a site pulls new posts. + * + * @since 2.1 + * @covers Puller::process_site() + */ + public function test_processing_site_pulls_new_posts() { + $processed_posts = $this->client->process_site( $this->site->ID, $this->client ); + + // Check new posts fetched. + $this->assertEquals( 10, count( $processed_posts ) ); + + // Check new posts were added. + $posts = new \WP_Query( array( + 'post_type' => 'post', + 'posts_per_page' => 10, + ) ); + + $this->assertEquals( 10, $posts->post_count ); + + // Test log was added. + $logs = Syndication_Logger::get_messages(); + + $this->assertEquals( 'success', $logs[ $this->site->ID ][ $posts->posts[1]->ID ]['msg_type'] ); + $this->assertEquals( 'new', $logs[ $this->site->ID ][ $posts->posts[1]->ID ]['status'] ); + } + + public function test_processing_invalid_feed_returns_false() { + // Setup invalid feed. + update_post_meta( $this->site->ID, 'syn_feed_url', 'https://localhost/invalidfeed' ); + + $processed_posts = $this->client->process_site( $this->site->ID, $this->client ); + + $this->assertFalse( $processed_posts ); + + // Reset to valid feed. + update_post_meta( $this->site->ID, 'syn_feed_url', 'https://vip.wordpress.com/feed' ); + } + + /** + * Test that processing a site doesn't create duplicates. + * + * @since 2.1 + * @covers Puller::process_site() + */ + public function test_process_site_site_no_duplicates() { + /* + * We should process the feed twice to make sure posts only get added + * once. The likely hood of the feed getting updated between the two + * process runs is slim to none. + */ + $this->client->process_site( $this->site->ID, $this->client ); + $this->client->process_site( $this->site->ID, $this->client ); + + // Check posts we updated and not added i.e. we should still have 10 posts in the DB. + $q = new \WP_Query( array( + 'post_type' => 'post', + 'posts_per_page' => 100, + ) ); + + $this->assertEquals( 10, $q->post_count ); + } + + public function test_process_site_updates_content() { + /* + * To pull this off, we need to use a mock class so that we can control + * the data that gets returned from `get_posts`. Usually that method + * fetches a feed, in our case we're returning dummy data so that we can + * test an update (instead of waiting for an update to happen to a real + * feed). + */ + $this->client = new Mock_RSS_Pull_Client_New(); + $this->client->init( $this->site->ID ); + + $processed_posts = $this->client->process_site( $this->site->ID, $this->client ); + + // Check new posts fetched. + $this->assertEquals( 1, count( $processed_posts ) ); + + // Check new posts were added. + $posts = new \WP_Query( array( + 'post_type' => 'post', + 'posts_per_page' => 10, + ) ); + + $this->assertEquals( 1, $posts->post_count ); + + // Check that new post just added gets updated + add_action( 'pre_option_push_syndicate_settings', function() { + return array( + 'selected_pull_sitegroups' => array(), + 'selected_post_types' => array( 'post' ), + 'delete_pushed_posts' => 'off', + 'pull_time_interval' => '3600', + 'update_pulled_posts' => 'on', + ); + } ); + + global $settings_manager; + $settings_manager->init(); + + $this->client = new Mock_RSS_Pull_Client_Update(); + $this->client->init( $this->site->ID ); + $this->client->process_site( $this->site->ID, $this->client ); + + $posts = new \WP_Query( array( + 'post_type' => 'post', + 'posts_per_page' => 10, + ) ); + + $this->assertEquals( 1, $posts->post_count ); + $this->assertEquals( 'Test post title (updated)', $posts->posts[0]->post_title ); + $this->assertEquals( '2017-01-01 12:00:00', $posts->posts[0]->post_date ); + } +} + +/** + * Class Mock_RSS_Pull_Client_New + * + * Mock class for faking an RSS feed's results and creating a new post. + * + * @package Automattic\Syndication\Clients\RSS_Pull + */ +class Mock_RSS_Pull_Client_New extends Pull_Client { + /** + * Returns a list of posts that would be fetched from an RSS feed. + * + * @param int $site_id The ID of the site to get posts for. + * @return array Array of posts on success, false on failure. + */ + public function get_posts( $site_id = 0 ) { + $new_post = new Types\Post(); + $new_post->post_data['post_title'] = 'Test post title'; + $new_post->post_data['post_content'] = 'Content of the test post'; + $new_post->post_data['post_excerpt'] = 'Test excerpt'; + $new_post->post_data['post_type'] = 'post'; + $new_post->post_data['post_status'] = 'publish'; + $new_post->post_data['post_date'] = '2017-01-01 00:00:00'; + $new_post->post_data['comment_status'] = 'open'; + $new_post->post_data['ping_status'] = 'open'; + $new_post->post_data['post_guid'] = 'unique_id_1'; + $new_post->post_data['post_category'] = ''; + $new_post->post_data['tags_input'] = ''; + $new_post->post_meta['site_id'] = $site_id; + + return array( $new_post ); + } +} + +/** + * Class Mock_RSS_Pull_Client_Update + * + * Mock class for faking an RSS feed's results and updating the new post created + * above. + * + * @package Automattic\Syndication\Clients\RSS_Pull + */ +class Mock_RSS_Pull_Client_Update extends Pull_Client { + /** + * Returns a list of posts that would be fetched from an RSS feed. + * + * @param int $site_id The ID of the site to get posts for. + * @return array Array of posts on success, false on failure. + */ + public function get_posts( $site_id = 0 ) { + $new_post = new Types\Post(); + $new_post->post_data['post_title'] = 'Test post title (updated)'; + $new_post->post_data['post_content'] = 'Content of the test post'; + $new_post->post_data['post_excerpt'] = 'Test excerpt'; + $new_post->post_data['post_type'] = 'post'; + $new_post->post_data['post_status'] = 'publish'; + $new_post->post_data['post_date'] = '2017-01-01 12:00:00'; + $new_post->post_data['comment_status'] = 'open'; + $new_post->post_data['ping_status'] = 'open'; + $new_post->post_data['post_guid'] = 'unique_id_1'; + $new_post->post_data['post_category'] = ''; + $new_post->post_data['tags_input'] = ''; + $new_post->post_meta['site_id'] = $site_id; + + return array( $new_post ); + } +} diff --git a/tests/clients/test-xml-push-client.php b/tests/clients/test-xml-push-client.php new file mode 100644 index 0000000..a4e1d9a --- /dev/null +++ b/tests/clients/test-xml-push-client.php @@ -0,0 +1,266 @@ +setup_XMLRPC_inteceptor(); + + // Create a site group. + $sitegroup = $this->factory->term->create_and_get( array( + 'taxonomy' => 'syn_sitegroup', + 'name' => 'Test Syndication Endpoint Group', + ) ); + + // Create a site. + $this->site = $this->factory->post->create_and_get( array( + 'post_title' => 'RSS Pull Syndication Endpoint', + 'post_type' => 'syn_site', + ) ); + + /* + * Setup a fake XML RPC service, we actually intercept the XMLRPC + * call below and send the request to a multisite install in our tests. + */ + add_post_meta( $this->site->ID, 'syn_transport_type', 'WP_RSS' ); + add_post_meta( $this->site->ID, 'syn_site_url', 'http://localhost/xmlrpc/xmlrpc.php' ); + add_post_meta( $this->site->ID, 'syn_default_post_type', 'post' ); + add_post_meta( $this->site->ID, 'syn_default_post_status', 'publish' ); + add_post_meta( $this->site->ID, 'syn_default_comment_status', 'open' ); + add_post_meta( $this->site->ID, 'syn_default_ping_status', 'open' ); + add_post_meta( $this->site->ID, 'syn_default_cat_status', 'yes' ); + add_post_meta( $this->site->ID, 'syn_site_enabled', 'on' ); + + // Add the site to the sitegroup. + wp_set_object_terms( $this->site->ID, $sitegroup->term_id, 'syn_sitegroup' ); + + // Instance of the actual client. + $this->client = new Push_Client(); + $this->client->init( $this->site->ID ); + + // Create a new multisite blog to send the request to. + $this->blog_id = $this->factory->blog->create(); + } + + /** + * Clean up after yourself. + * + * @since 2.1 + */ + public function tearDown() { + parent::tearDown(); + + switch_to_blog( $this->blog_id ); + + $q = new \WP_Query( array( + 'post_type' => array( 'syn_site', 'syn_sitegroup', 'post' ), + 'posts_per_page' => -1, + ) ); + + $post_ids = wp_list_pluck( $q->posts, 'ID' ); + + foreach ( $post_ids as $post_id ) { + wp_delete_post( $post_id ); + } + + restore_current_blog(); + } + + /** + * Test that a new posts gets sent sent and created on the other side. + * + * @since 2.1 + * @covers Push_Client::new_post() + */ + public function test_new_post() { + // Create a new post. + $post_id = wp_insert_post( array( 'post_title' => 'Test Post', 'post_content' => 'Test post content', 'post_status' => 'publish' ) ); + + // Send the new post to the other side. + $this->client->new_post( $post_id ); + + // Switch to the other blog and fetch it's posts. + switch_to_blog( $this->blog_id ); + + $posts = new \WP_Query( array( + 'post_type' => 'post', + 'posts_per_page' => 1, + 'orderby' => 'ID', + 'order' => 'DESC', + ) ); + + restore_current_blog(); + + // Test that the post was created as expected. + $this->assertEquals( 'Test Post', $posts->post->post_title ); + $this->assertEquals( 'Test post content', $posts->post->post_content ); + } + + /** + * Test that a when updating a post it gets updated on the other side. + * + * @since 2.1 + * @covers Push_Client::edit_post() + */ + public function test_edit_post() { + // Create a new post. + $post_id = wp_insert_post( array( 'post_title' => 'Test Post', 'post_content' => 'Test post content', 'post_status' => 'publish' ) ); + + // Send the new post to the other side. + $remote_post_id = $this->client->new_post( $post_id ); + + // Update our post. + wp_update_post( array( 'ID' => $post_id, 'post_title' => 'Test Post (Updated)', 'post_content' => 'Test post content (updated)' ) ); + + // Send the update across the wire. + $this->client->edit_post( $post_id, $remote_post_id ); + + // Switch to the other blog and fetch it's posts. + switch_to_blog( $this->blog_id ); + + $posts = new \WP_Query( array( + 'post_type' => 'post', + 'posts_per_page' => 1, + 'orderby' => 'ID', + 'order' => 'DESC', + ) ); + + restore_current_blog(); + + // Test that the post was updated as expected. + $this->assertEquals( 'Test Post (Updated)', $posts->post->post_title ); + $this->assertEquals( 'Test post content (updated)', $posts->post->post_content ); + } + + /** + * Test that a post gets deleted on the other side. + * + * @since 2.1 + * @covers Push_Client::delete_post() + */ + public function test_delete_post() { + // Create a new post. + $post_id = wp_insert_post( array( 'post_title' => 'Test Post', 'post_content' => 'Test post content', 'post_status' => 'publish' ) ); + + // Send the new post to the other side. + $remote_post_id = $this->client->new_post( $post_id ); + + // Make sure it exists. + switch_to_blog( $this->blog_id ); + + $posts = new \WP_Query( array( + 'post_type' => 'post', + 'posts_per_page' => 1, + 'orderby' => 'ID', + 'order' => 'DESC', + ) ); + + restore_current_blog(); + + $this->assertEquals( 'Test Post', $posts->post->post_title ); + + // Delete it on the other site and test it was deleted. + $this->client->delete_post( $remote_post_id ); + + switch_to_blog( $this->blog_id ); + + $posts = new \WP_Query( array( + 'post_type' => 'post', + 'posts_per_page' => 1, + 'orderby' => 'ID', + 'order' => 'DESC', + ) ); + + restore_current_blog(); + + $this->assertNotEquals( 'Test Post', $posts->post->post_title ); + $this->assertNotEquals( 'Test post content', $posts->post->post_content ); + } + + /** + * This method intercepts our XML RPC calls and sends them to a multisite + * blog that was created above. It handles new posts, updating posts and + * deleting posts. Once the request is fulfilled, it retuns a valid HTTP + * response back to the caller. + * + * @since 2.1 + */ + public function setup_XMLRPC_inteceptor() { + // Mock remote HTTP calls made by XMLRPC + add_action( 'pre_http_request', function( $short_circuit, $args, $url ) { + if ( 'http://localhost/xmlrpc/xmlrpc.php' === $url ) { + switch_to_blog( $this->blog_id ); + + // Create user. + $this->make_user_by_role( 'author' ); + + // Parse message. + $message = new \IXR_Message( $args['body'] ); + $message->parse(); + + // Set username and password. + $message->params[1] = 'author'; + $message->params[2] = 'author'; + + // Add method callback. + $this->myxmlrpcserver->callbacks['wp.getPost'] = 'this:wp_getPost'; + $this->myxmlrpcserver->callbacks['wp.newPost'] = 'this:wp_newPost'; + $this->myxmlrpcserver->callbacks['wp.editPost'] = 'this:wp_editPost'; + $this->myxmlrpcserver->callbacks['wp.deletePost'] = 'this:wp_deletePost'; + + // Post. + $result = $this->myxmlrpcserver->call( $message->methodName, $message->params ); + + // Encode the result + $r = new \IXR_Value( $result ); + $resultxml = $r->getXml(); + + $xml = << + + + + + $resultxml + + + + + +EOD; + + restore_current_blog(); + + return array( + 'headers' => array(), + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'body' => $xml, + ); + } + + return $short_circuit; + }, 10, 3 ); + } +} diff --git a/tests/test-bootstrap.php b/tests/test-bootstrap.php deleted file mode 100644 index 622f391..0000000 --- a/tests/test-bootstrap.php +++ /dev/null @@ -1,28 +0,0 @@ -assertTrue( post_type_exists( 'syn_site' ) ); - $this->assertTrue( taxonomy_exists( 'syn_sitegroup' ) ); - - $post_type = get_post_type_object( 'syn_site' ); - $this->assertEquals( 'Syndication Endpoints', $post_type->labels->name ); - - $taxonomy = get_taxonomy( 'syn_sitegroup' ); - $this->assertEquals( 'Syndication Endpoint Groups', $taxonomy->labels->name ); - } -} \ No newline at end of file diff --git a/tests/test-class-bootstrap.php b/tests/test-class-bootstrap.php new file mode 100644 index 0000000..4792e6a --- /dev/null +++ b/tests/test-class-bootstrap.php @@ -0,0 +1,139 @@ +assertTrue( post_type_exists( 'syn_site' ) ); + $this->assertTrue( taxonomy_exists( 'syn_sitegroup' ) ); + + $post_type = get_post_type_object( 'syn_site' ); + $this->assertEquals( 'Syndication Endpoints', $post_type->labels->name ); + + $taxonomy = get_taxonomy( 'syn_sitegroup' ); + $this->assertEquals( 'Syndication Endpoint Groups', $taxonomy->labels->name ); + } + + /** + * Test to see if we can create a site (Endpoint). + * + * @since 2.1 + * @covers Bootstrap::register_post_type() + */ + public function test_add_new_site() { + $site = $this->factory->post->create_and_get( array( + 'post_type' => 'syn_site', + ) ); + + $this->assertInstanceOf( 'WP_Post', $site ); + } + + /** + * Test to see if we can delete a site (Endpoint). + * + * @since 2.1 + * @covers Bootstrap::register_post_type() + */ + public function test_delete_site() { + $site = $this->factory->post->create_and_get( array( + 'post_type' => 'syn_site', + ) ); + + $deleted = wp_delete_post( $site->ID ); + + $this->assertNotFalse( $deleted ); + } + + /** + * Test to see if we can edit a site (Endpoint). + * + * @since 2.1 + * @covers Bootstrap::register_post_type() + */ + public function test_edit_site() { + $site = $this->factory->post->create_and_get( array( + 'post_type' => 'syn_site', + ) ); + + $site->post_title = 'New Site ID'; + + $site_updated = wp_update_post( $site ); + + $this->assertEquals( $site->ID, $site_updated ); + } + + /** + * Test to see if we can create a sitegroup. + * + * @since 2.1 + * @covers Bootstrap::register_post_type() + */ + public function test_add_new_sitegroup() { + $sitegroup_taxonomy = 'syn_sitegroup'; + $sitegroup_name = 'Site Group A'; + + $term = $this->factory->term->create_and_get( array( + 'taxonomy' => $sitegroup_taxonomy, + 'name' => $sitegroup_name, + ) ); + + $this->assertInstanceOf( 'WP_Term', $term ); + } + + /** + * Test to see if we can delete a sitegroup. + * + * @since 2.1 + * @covers Bootstrap::register_post_type() + */ + public function test_delete_sitegroup() { + $sitegroup_taxonomy = 'syn_sitegroup'; + $sitegroup_name = 'Site Group B'; + + $term = $this->factory->term->create_and_get( array( + 'taxonomy' => $sitegroup_taxonomy, + 'name' => $sitegroup_name, + ) ); + + $deleted = wp_delete_term( $term->term_id, $term->taxonomy ); + + $this->assertTrue( $deleted ); + } + + /** + * Test to see if we can edit a sitegroup. + * + * @since 2.1 + * @covers Bootstrap::register_post_type() + */ + public function test_edit_sitegroup() { + $sitegroup_taxonomy = 'syn_sitegroup'; + $sitegroup_name = 'Site Group C'; + + $term = $this->factory->term->create_and_get( array( + 'taxonomy' => $sitegroup_taxonomy, + 'name' => $sitegroup_name, + ) ); + + $deleted = wp_update_term( $term->term_id, $term->taxonomy, array( + 'name' => 'Site Group C1', + 'slug' => 'site-group-c1', + ) ); + + $modified_term = get_term_by( 'name', 'Site Group C1', $sitegroup_taxonomy ); + + $this->assertInstanceOf( 'WP_Term', $modified_term ); + } +} \ No newline at end of file