diff --git a/.gitignore b/.gitignore index ed74ecd..99e6bbc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,9 @@ * !/**/ !*.* + +.DS_Store + +tmp/* + +!*.gitkeep diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist new file mode 100644 index 0000000..2a45742 --- /dev/null +++ b/.phpcs.xml.dist @@ -0,0 +1,48 @@ + + + Generally-applicable sniffs for WordPress plugins. + + + . + /vendor/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ec9429e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,65 @@ +sudo: false +dist: trusty + +language: php + +notifications: + email: + on_success: never + on_failure: change + +branches: + only: + - master + +cache: + directories: + - $HOME/.composer/cache + +matrix: + include: + - php: 7.2 + env: WP_VERSION=latest + - php: 7.1 + env: WP_VERSION=latest + - php: 7.0 + env: WP_VERSION=latest + - php: 5.6 + env: WP_VERSION=latest + - php: 5.6 + env: WP_VERSION=trunk + - php: 5.6 + env: WP_TRAVISCI=phpcs + - php: 5.3 + env: WP_VERSION=latest + dist: precise + +before_script: + - export PATH="$HOME/.composer/vendor/bin:$PATH" + - | + if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then + phpenv config-rm xdebug.ini + else + echo "xdebug.ini does not exist" + fi + - | + if [[ ! -z "$WP_VERSION" ]] ; then + bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION + composer global require "phpunit/phpunit=4.8.*|5.7.*" + fi + - | + if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then + composer global require wp-coding-standards/wpcs + phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs + fi + +script: + - | + if [[ ! -z "$WP_VERSION" ]] ; then + phpunit + WP_MULTISITE=1 phpunit + fi + - | + if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then + phpcs + fi diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh new file mode 100755 index 0000000..364f839 --- /dev/null +++ b/bin/install-wp-tests.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash + +if [ $# -lt 3 ]; then + echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" + exit 1 +fi + +DB_NAME=$1 +DB_USER=$2 +DB_PASS=$3 +DB_HOST=${4-localhost} +WP_VERSION=${5-latest} +SKIP_DB_CREATE=${6-false} + +TMPDIR=${TMPDIR-/tmp} +TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") +WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} +WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/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]+$ ]]; then + WP_TESTS_TAG="branches/$WP_VERSION" +elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + WP_TESTS_TAG="tags/${WP_VERSION%??}" + else + WP_TESTS_TAG="tags/$WP_VERSION" + fi +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 == 'nightly' || $WP_VERSION == 'trunk' ]]; then + mkdir -p $TMPDIR/wordpress-nightly + download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip + unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ + mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR + else + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then + # https serves multiple offers, whereas http serves single. + download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + LATEST_VERSION=${WP_VERSION%??} + else + # otherwise, scan the releases and get the most up to date minor version of the major release + local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` + LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) + fi + if [[ -z "$LATEST_VERSION" ]]; then + local ARCHIVE_NAME="wordpress-$WP_VERSION" + else + local ARCHIVE_NAME="wordpress-$LATEST_VERSION" + fi + else + local ARCHIVE_NAME="wordpress-$WP_VERSION" + fi + download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz + tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR + fi + + download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php +} + +install_test_suite() { + # portable in-place argument for both GNU sed and Mac OSX sed + if [[ $(uname -s) == 'Darwin' ]]; then + local ioption='-i.bak' + else + local ioption='-i' + fi + + # 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 + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data + 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]}; + local DB_SOCK_OR_PORT=${PARTS[1]}; + local EXTRA="" + + if ! [ -z $DB_HOSTNAME ] ; 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" + elif ! [ -z $DB_HOSTNAME ] ; then + EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" + fi + fi + + # create database + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA +} + +install_wp +install_test_suite +install_db diff --git a/classes/LBRY_Daemon.php b/classes/LBRY_Daemon.php index 13171ca..9abcdd2 100644 --- a/classes/LBRY_Daemon.php +++ b/classes/LBRY_Daemon.php @@ -80,6 +80,42 @@ class LBRY_Daemon return $result->result; } + /** + * Publishes a post to the LBRY Network + * @param string $name The slug for the post + * @param float $bid The amount of LBC to bid + * @param string $filepath The path of the temporary content file + * @param string $title The Title of the post + * @param string $description The Description of the Post + * @param string $language Two letter ISO Code of the language + * @return string $channel The Claim ID of the Channel + */ + public function publish($name, $bid, $filepath, $title, $description, $language, $channel) + { + $args = array( + 'name' => $name, + 'bid' => $bid, + 'file_path' => $filepath, + 'title' => $title, + 'description' => $description, + 'language' => $language, + ); + + // Make sure we aren't publishing to unattributed + if ($channel != 'null') { + $args['channel_id'] = $channel; + } + + // TODO: Bring thumbnails into the mix + $result = $this->request( + 'publish', + $args + ); + + $this->check_for_errors($result); + return $result; + } + /** * Sends a cURL request to the LBRY Daemon * @param string $method The method to call on the LBRY API diff --git a/classes/LBRY_Network.php b/classes/LBRY_Network.php index e571557..3ad4b8e 100644 --- a/classes/LBRY_Network.php +++ b/classes/LBRY_Network.php @@ -7,10 +7,103 @@ class LBRY_Network { + + /** + * The Publishing Object + * @var LBRY_Network_Publisher + */ + public $publisher = null; + + /** + * The Parsing Object + * @var LBRY_Network_Parser + */ + public $parser = null; + /** * [__construct description] */ public function __construct() { + $this->publisher = new LBRY_Network_Publisher(); + $this->parser = new LBRY_Network_Parser(); + + $this->post_meta_setup(); + } + + /** + * Sets up everything for the post meta boxes + */ + private function post_meta_setup() + { + // Add the meta boxes + add_action('add_meta_boxes', array($this, 'add_meta_boxes')); + + // Save the post meta on 'save_post' hook + add_action('save_post', array($this, 'save_post_meta'), 10, 2); + } + + /** + * Adds the meta boxes to the post editing backend + */ + public function add_meta_boxes() + { + // TODO: Support post types based on user selection + add_meta_box( + 'lbry-network-publishing', // Unique ID + 'LBRY Network', // Title + array($this, 'meta_box_html'), // Callback function + 'post', // Screen Options (or post type) + 'side', // Context + 'high' // Priority + ); + } + + /** + * Handles saving the post meta that is relative to publishing to the LBRY Network + * @param int $post_id The ID of the post we are saving + * @param WP_Post $post The Post Object we are saving + * @return int Returns post_id if user cannot edit post + */ + public function save_post_meta($post_id, $post) + { + // Verify the nonce before proceeding. + if (!isset($_POST['_lbrynonce']) || !wp_verify_nonce($_POST['_lbrynonce'], 'lbry_publish_channels')) { + return $post_id; + } + + // Check if the current user has permission to edit the post. + $post_type = get_post_type_object($post->post_type); + if (!current_user_can($post_type->cap->edit_post, $post_id)) { + return $post_id; + } + + $will_publish = (isset($_POST[LBRY_WILL_PUBLISH]) ? $_POST[LBRY_WILL_PUBLISH] : false); + $new_channel = (isset($_POST[LBRY_POST_CHANNEL]) ? $_POST[LBRY_POST_CHANNEL] : null); + $cur_channel = get_post_meta($post_id, LBRY_POST_CHANNEL, true); + + // Update meta acordingly + if (!$will_publish) { + update_post_meta($post_id, LBRY_WILL_PUBLISH, 'false'); + } else { + update_post_meta($post_id, LBRY_WILL_PUBLISH, 'true'); + } + if ($new_channel !== $cur_channel) { + update_post_meta($post_id, LBRY_POST_CHANNEL, $new_channel); + } + + if ($will_publish) { + // Publish the post on the LBRY Network + $this->publisher->publish($post, get_post_meta($post_id, LBRY_POST_CHANNEL, true)); + } + } + + /** + * Returns the HTML for the LBRY Meta Box + * @param [type] $post [description] + */ + public function meta_box_html($post) + { + require_once(LBRY_ABSPATH . 'templates/meta_box.php'); } } diff --git a/classes/LBRY_Network_Parser.php b/classes/LBRY_Network_Parser.php index cc06b84..6c7a0d7 100644 --- a/classes/LBRY_Network_Parser.php +++ b/classes/LBRY_Network_Parser.php @@ -1,16 +1,45 @@ converter = new HtmlConverter(array( + 'strip_tags' => true + )); + } + + /** + * Converts a post into markdown. + * @param WP_Post $post The post to be converted + * @return string + */ + public function convert_to_markdown($post) + { + // $title = '

' . $post->post_title . '

'; + // + // $featured_image = get_the_post_thumbnail($post); + // + // $content = $title; + // if ($featured_image) { + // $content .= $featured_image . '
'; + // } + $content = apply_filters('the_content', $post->post_content); + $converted = $this->converter->convert($content); + + return $converted; } } diff --git a/classes/LBRY_Network_Publisher.php b/classes/LBRY_Network_Publisher.php index 76b9370..2648401 100644 --- a/classes/LBRY_Network_Publisher.php +++ b/classes/LBRY_Network_Publisher.php @@ -13,4 +13,47 @@ class LBRY_Network_Publisher public function __construct() { } + + /** + * Publish the post to the LBRY Network + * @param int $post_id The ID of the post we are publishing + * @param string $channel The Claim ID of the channel we are posting to + */ + public function publish($post, $channel) + { + // Leave if nothing to publish to + if (!$channel) { + return; + } + + // Get converted markdown into a file + $filepath = LBRY_ABSPATH . 'tmp/' . $post->post_name . time() . '.md'; + $file = fopen($filepath, 'w'); + $converted = LBRY()->network->parser->convert_to_markdown($post); + $write_status = $file && fwrite($file, $converted); + fclose($file); + + // TODO: Catch relative exceptions if necessary + try { + // If everything went well with the conversion, carry on + if ($write_status) { + $featured_image = get_the_post_thumbnail($post); + + $name = $post->post_name; + $bid = floatval(get_option(LBRY_SETTINGS)[LBRY_LBC_PUBLISH]); + $title = $post->post_title; + $language = substr(get_locale(), 0, 2); + $license = get_option(LBRY_SETTINGS)[LBRY_LICENSE]; + // TODO: See if we can grab from yoast or a default? + $description = $post->post_title; + // TODO: Bring thumbnails into the mix + // $thumbnail = $featured_image ? $featured_image : null; + + LBRY()->daemon->publish($name, $bid, $filepath, $title, $description, $language, $channel); + } + } finally { + // Delete the temporary markdown file + unlink($filepath); + } + } } diff --git a/classes/lbrypress.php b/classes/lbrypress.php index d8abbb3..c228699 100644 --- a/classes/lbrypress.php +++ b/classes/lbrypress.php @@ -38,6 +38,11 @@ class LBRYPress */ public $notice = null; + /** + * The Library Network Object + */ + public $network = null; + /** * Main LBRYPress Instance. * @@ -96,6 +101,8 @@ class LBRYPress $this->define('LBRY_SPEECH', 'lbry_speech'); // the spee.ch address $this->define('LBRY_LICENSE', 'lbry_license'); // the license to publish with to the LBRY network $this->define('LBRY_LBC_PUBLISH', 'lbry_lbc_publish'); // amount of lbc to use per publish + $this->define('LBRY_WILL_PUBLISH', 'lbry_will_publish'); // The meta key for if to publish to LBRY Network or not + $this->define('LBRY_POST_CHANNEL', 'lbry_channel'); // The meta key for which channel to publish $this->define('LBRY_AVAILABLE_LICENSES', array( 'mit' => 'MIT', 'license2' => 'License 2', @@ -123,7 +130,6 @@ class LBRYPress { $this->daemon = new LBRY_Daemon(); $this->speech = new LBRY_Speech(); - $this->notice = new LBRY_Admin_Notice(); } /** @@ -137,6 +143,8 @@ class LBRYPress // Admin request if (is_admin()) { $this->admin = new LBRY_Admin(); + $this->notice = new LBRY_Admin_Notice(); + $this->network = new LBRY_Network(); } else { $this->speech->maybe_rewrite_urls(); } @@ -176,8 +184,6 @@ class LBRYPress // } // } // update_option(LBRY_SETTINGS, $new_settings); - - error_log('Activated'); } /** @@ -187,4 +193,19 @@ class LBRYPress { error_log('Deactivated'); } + + /* + * Utility Functions + */ + public static function channel_name_comp($a, $b) + { + if ($a->name === $b->name) { + return 0; + } + + if ($b->name == '(none / unattributed)') { + return -1; + } + return strnatcasecmp($a->name, $b->name); + } } diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..0a97f8e --- /dev/null +++ b/composer.json @@ -0,0 +1,13 @@ +{ + "name": "undergroundweblab/lbrypress", + "description": "A Wordpress plugin to connect to the LBRY Network", + "require": { + "league/html-to-markdown": "^4.8" + }, + "authors": [ + { + "name": "Underground Web Lab", + "email": "paul@undergroundweblab.com" + } + ] +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..e4114f9 --- /dev/null +++ b/composer.lock @@ -0,0 +1,82 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "cb08bd734b8d1da7ed57c572fa75e2d9", + "packages": [ + { + "name": "league/html-to-markdown", + "version": "4.8.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/html-to-markdown.git", + "reference": "f9a879a068c68ff47b722de63f58bec79e448f9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/f9a879a068c68ff47b722de63f58bec79e448f9d", + "reference": "f9a879a068c68ff47b722de63f58bec79e448f9d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xml": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "mikehaertl/php-shellcommand": "~1.1.0", + "phpunit/phpunit": "4.*", + "scrutinizer/ocular": "~1.1" + }, + "bin": [ + "bin/html-to-markdown" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\HTMLToMarkdown\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nick Cernis", + "email": "nick@cern.is", + "homepage": "http://modernnerd.net", + "role": "Original Author" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "An HTML-to-markdown conversion helper for PHP", + "homepage": "https://github.com/thephpleague/html-to-markdown", + "keywords": [ + "html", + "markdown" + ], + "time": "2018-09-18T12:18:08+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..d9af975 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,15 @@ + + + + + ./tests/ + + + diff --git a/templates/meta_box.php b/templates/meta_box.php new file mode 100644 index 0000000..ed05828 --- /dev/null +++ b/templates/meta_box.php @@ -0,0 +1,43 @@ + '(none / unattributed)', + 'claim_id' => 'null' +); +// TODO: Test what happens with empty channel list, can't remember the return value +$channels = LBRY()->daemon->channel_list(); +$channels[] = $unnatributed; +// Sort the channels in a natural way +usort($channels, array('LBRYPress', 'channel_name_comp')); +$cur_channel = get_post_meta($post->ID, LBRY_POST_CHANNEL, true); +$will_publish = get_post_meta($post->ID, LBRY_WILL_PUBLISH, true); +?> + +
+ +
+ diff --git a/templates/options_page.php b/templates/options_page.php index 5a2932f..320f2ea 100644 --- a/templates/options_page.php +++ b/templates/options_page.php @@ -2,14 +2,12 @@ $LBRY = LBRY(); $wallet_balance = $LBRY->daemon->wallet_balance(); $channel_list = $LBRY->daemon->channel_list(); +// TODO: Make this page look cleaner ?>
-

-

Your wallet amount:

-
daemon->channel_list(); submit_button('Save Settings'); ?>
-

Your Publishable Channels