2
0
Fork 0
mirror of https://github.com/discourse/wp-discourse.git synced 2025-08-17 18:11:19 +08:00

Merging changes from Got for 0.7 release

git-svn-id: http://plugins.svn.wordpress.org/wp-discourse/trunk@1449569 b8457f37-d9ea-0310-8a92-e5e31aec5664
This commit is contained in:
scossar 2016-07-05 22:05:34 +00:00
parent 4d91d3004a
commit 2d8feca9ce
41 changed files with 5581 additions and 385 deletions

5
.gitignore vendored
View file

@ -1 +1,6 @@
*.swp
# JetBrains IDEs
.idea/*
vendor/*

4
.jscsrc Normal file
View file

@ -0,0 +1,4 @@
{
"preset": "wordpress",
"fileExtensions": [ ".js" ]
}

13
.mention-bot Normal file
View file

@ -0,0 +1,13 @@
{
"maxReviewers": 2,
"userBlacklist": [
"tnorthcutt",
"retlehs"
],
"userBlacklistForPR": [
"scossar",
"retlehs",
"techAPJ",
"SamSaffron"
]
}

90
.travis.yml Normal file
View file

@ -0,0 +1,90 @@
# Travis CI (MIT License) configuration file for the WP Discourse WordPress plugin.
# @link https://travis-ci.org/
# For use with the WP Discourse WordPress plugin.
# @link https://github.com/discourse/wp-discourse
# 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
# Declare versions of PHP to use. Use one decimal max.
# @link http://docs.travis-ci.com/user/build-configuration/
matrix:
fast_finish: true
include:
# aliased to a recent 5.3.x version
- php: '5.3'
# 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:
- 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
# Install JSCS: JavaScript Code Style checker.
# @link http://jscs.info/
- if [[ "$SNIFF" == "1" ]]; then npm install -g jscs; fi
# Install JSHint, a JavaScript Code Quality Tool.
# @link http://jshint.com/docs/
- if [[ "$SNIFF" == "1" ]]; then npm install -g jshint; fi
# Pull in the WP Core jshint rules.
- if [[ "$SNIFF" == "1" ]]; then wget https://develop.svn.wordpress.org/trunk/.jshintrc; 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' -and ! -name 'test*.php' -print0 | xargs -0 -n 1 -P 4 php -l
# Run the theme through JSHint.
- if [[ "$SNIFF" == "1" ]]; then jshint .; fi
# Run the theme through JavaScript Code Style checker.
- if [[ "$SNIFF" == "1" ]]; then jscs .; fi
# 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=./codesniffer.ruleset.xml --ignore=*/tests/*,*/tests/lib/* --extensions=php; fi
# Receive notifications for build results.
# @link http://docs.travis-ci.com/user/notifications/#Email-notifications
notifications:
email: false

45
CHANGELOG.md Normal file
View file

@ -0,0 +1,45 @@
### 0.7.0: May 18th, 2016
* Move templates out of options ([#194](https://github.com/discourse/wp-discourse/pull/194))
* Validate settings ([#189](https://github.com/discourse/wp-discourse/pull/189))
* Add notices to indicate connection status ([#193](https://github.com/discourse/wp-discourse/pull/193))
* Sanitize admin options page ([#196](https://github.com/discourse/wp-discourse/pull/196))
* Sanitize comment template output ([#195](https://github.com/discourse/wp-discourse/pull/195))
* Add type argument to text input method ([#192](https://github.com/discourse/wp-discourse/pull/192))
* Use cached categories when there is a configuration error ([#191](https://github.com/discourse/wp-discourse/pull/191))
* Fix name property not available in participants array ([#187](https://github.com/discourse/wp-discourse/pull/187))
* Use `wp_get_current_user` ([#185](https://github.com/discourse/wp-discourse/pull/185))
* Fix `add_query_arg` undefined offset notice ([#184](https://github.com/discourse/wp-discourse/pull/184))
* Update Discourse post on WP post update ([#176](https://github.com/discourse/wp-discourse/pull/176))
* Better method for including comments script and other small tweaks ([#181](https://github.com/discourse/wp-discourse/pull/181))
* Allow choosing Discourse category per post ([#177](https://github.com/discourse/wp-discourse/pull/177))
* Replace avatar URL function ([#172](https://github.com/discourse/wp-discourse/pull/172))
* Fix timezone for custom timestamp ([#162](https://github.com/discourse/wp-discourse/pull/162))
### 0.6.6: July 30th, 2015
* Add custom datetime format string to admin settings ([#160](https://github.com/discourse/wp-discourse/pull/160))
* Add a log entry when HTTP request fails ([#159](https://github.com/discourse/wp-discourse/pull/159))
* Log out of WordPress when logging out of Discourse ([#158](https://github.com/discourse/wp-discourse/pull/158))
* Fix security issue, add missing `esc_url_raw()` ([#157](https://github.com/discourse/wp-discourse/pull/157))
* Fix SSO login ([#156](https://github.com/discourse/wp-discourse/pull/156))
* Use `wp_remote_get` instead of `file_get_contents` ([#155](https://github.com/discourse/wp-discourse/pull/155))
* Fix user mention links ([8b6fe46](https://github.com/discourse/wp-discourse/commit/8b6fe46bdbeaa6f4be490723f1e9d6b5a6f48d41))
* Allow showing existing WP comments under Discourse ([#137](https://github.com/discourse/wp-discourse/pull/137))
* Add `<time>` to allowed tags ([#135](https://github.com/discourse/wp-discourse/pull/135))
* Don't do a replace if already an absolute URL ([#131](https://github.com/discourse/wp-discourse/pull/131))
### 0.6.5: February 28th, 2015
* Whitespaces should be stripped only on strings
### 0.6.4: February 24th, 2015
* Minor re-organization of the settings page
* Fetch categories from remote
* Add fixes for allowed post types
* Fix asset URLs on synced Discourse comments
### 0.6.3: January 31st, 2015
* Add CHANGELOG
* Move comments template into new folder
* Move SSO and admin functions into separate files
* Switch from `register_uninstall_hook` to `uninstall.php`
* Move JS from separate file to inline
* Remove unnecessary stylesheet

View file

@ -1,8 +1,47 @@
wp-discourse
============
# wp-discourse
This plugin allows you to *optionally* publish your WordPress blog posts on a Discourse instance.
This WordPress plugin allows you to **use Discourse as a community engine for your WordPress blog**.
Once published, WordPress will fetch the best 5 comments from the Discourse topic and surplant the existing comments section.
## Features
Comments and post metadata is synchronized every 10 minutes.
* Optionally publish all new posts to Discourse automatically
* Use Discourse to comment on blog posts with associated Discourse topics
* Periodically sync the "best" posts in Discourse topics back to the associated WordPress blog entry as WordPress comments
* Enable SSO to Discourse
* Define format of post on Discourse
* Set username of post on Discourse
* Set published category on Discourse
* Allow author to automatically track published Discourse topics
* Show comments on Discourse based on post score and commenter trust level
## Installation
### Plugin manager from your `wp-admin`
Download the [latest release](https://github.com/discourse/wp-discourse/releases) and upload it in the WordPress admin from Plugins > Add New > Upload Plugin.
### Composer
If you're using Composer to manage WordPress, add WP-Discourse to your project's dependencies. Run:
```sh
composer require discourse/wp-discourse 0.7.0
```
Or manually add it to your `composer.json`:
```json
"require": {
"php": ">=5.3.0",
"wordpress": "4.1.1",
"discourse/wp-discourse": "0.7.0"
}
```
## Contributing
1. Fork this repo
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new pull request

BIN
assets/screenshot-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
assets/screenshot-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
assets/screenshot-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
assets/screenshot-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

118
bin/install-wp-tests.sh Executable file
View file

@ -0,0 +1,118 @@
#!/usr/bin/env bash
if [ $# -lt 3 ]; then
echo "usage: $0 <db-name> <db-user> <db-pass> [db-host] [wp-version]"
exit 1
fi
DB_NAME=$1
DB_USER=$2
DB_PASS=$3
DB_HOST=${4-localhost}
WP_VERSION=${5-latest}
WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib}
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 == '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
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
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
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
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() {
# 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

18
codesniffer.ruleset.xml Normal file
View file

@ -0,0 +1,18 @@
<?xml version="1.0"?>
<ruleset name="WP Discourse Coding Standards">
<!-- See https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-ruleset.xml -->
<!-- See https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/blob/develop/WordPress-Core/ruleset.xml -->
<!-- Set a description for this ruleset. -->
<description>A custom set of code standard rules for the WP Discourse plugin.</description>
<!-- Include the WordPress ruleset, with exclusions. -->
<rule ref="WordPress">
<exclude name="Generic.WhiteSpace.ScopeIndent.IncorrectExact" />
<exclude name="Generic.WhiteSpace.ScopeIndent.Incorrect" />
<exclude name="PEAR.Functions.FunctionCallSignature.Indent" />
</rule>
<!-- Include sniffs for PHP cross-version compatibility. -->
<rule ref="PHPCompatibility"/>
</ruleset>

28
composer.json Normal file
View file

@ -0,0 +1,28 @@
{
"name": "discourse/wp-discourse",
"type": "wordpress-plugin",
"license": "GPLv3",
"description": "WordPress plugin that allows you to use Discourse as a community engine for your WordPress blog.",
"homepage": "https://github.com/discourse/wp-discourse",
"authors": [
{
"name": "Sam Saffron",
"homepage": "https://github.com/SamSaffron"
},
{
"name": "Robin Ward",
"homepage": "https://github.com/eviltrout"
}
],
"keywords": ["wordpress", "discourse"],
"support": {
"issues": "https://github.com/discourse/wp-discourse/issues"
},
"require": {
"php": ">=5.3",
"composer/installers": "1.0.*"
},
"require-dev": {
"php-mock/php-mock-phpunit": "^0.3"
}
}

1394
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

24
css/admin-styles.css Normal file
View file

@ -0,0 +1,24 @@
/* Highlights errors in settings form inputs */
#setting-error-discourse_url ~ form #discourse_url,
#setting-error-api_key ~ form #discourse_api-key,
#setting-error-publish_username ~ form #discourse_publish-username,
#setting-error-max_comments ~ form #discourse_max-comments,
#setting-error-min_replies ~ form #discourse_min-replies,
#setting-error-min_score ~ form #discourse_min-score,
#setting-error-min_trust_level ~ form #discourse_min-trust-level,
#setting-error-bypass_trust_level ~ form #discourse_bypass-trust-level-score,
#setting-error-excerpt_length ~ form #discourse_custom-excerpt-length,
#setting-error-sso_secret ~ form #discourse_sso-secret,
#setting-error-login_path ~ form #discourse_login-path {
border-color: #dc3232;
border-width: 2px;
}
.settings_page_discourse .discourse-allowed-types {
min-width: 160px;
}
#discourse-publish-meta-box .warning {
padding: 0 8px;
}

18
js/comments.js Normal file
View file

@ -0,0 +1,18 @@
/* globals discourse */
/**
* Fixes Discourse oneboxes and mention links for display on WordPress.
*
* @package WPDiscourse
*/
jQuery( document ).ready(function() {
jQuery( '.lazyYT' ).each(function() {
var id = jQuery( this ).data( 'youtube-id' ),
url = 'https://www.youtube.com/watch?v=' + id;
jQuery( this ).replaceWith( '<a href="' + url + '">' + url + '</a>' );
});
jQuery( 'a.mention' ).each(function() {
jQuery( this ).attr( 'href', discourse.url + jQuery( this ).attr( 'href' ) );
});
});

655
lib/admin.php Normal file
View file

@ -0,0 +1,655 @@
<?php
/**
* WP-Discourse admin settings
*
* @link https://github.com/discourse/wp-discourse/blob/master/lib/admin.php
* @package WPDiscourse
*/
namespace WPDiscourse\DiscourseAdmin;
use WPDiscourse\Utilities\Utilities as DiscourseUtilities;
/**
* Class DiscourseAdmin
*/
class DiscourseAdmin {
/**
* Gives access to the plugin options.
*
* @access protected
* @var mixed|void
*/
protected $options;
/**
* Discourse constructor.
*/
public function __construct() {
$this->options = get_option( 'discourse' );
add_action( 'admin_init', array( $this, 'admin_init' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'admin_styles' ) );
add_action( 'admin_menu', array( $this, 'discourse_admin_menu' ) );
add_action( 'load-settings_page_discourse', array( $this, 'connection_status_notice' ) );
}
/**
* Enqueues the admin stylesheet.
*/
public function admin_styles() {
wp_register_style( 'wp_discourse_admin', WPDISCOURSE_URL . '/css/admin-styles.css' );
wp_enqueue_style( 'wp_discourse_admin' );
}
/**
* Settings
*/
public function admin_init() {
register_setting( 'discourse', 'discourse', array( $this, 'discourse_validate_options' ) );
add_settings_section( 'discourse_wp_api', __( 'Common Settings', 'wp-discourse' ), array(
$this,
'init_default_settings',
), 'discourse' );
add_settings_section( 'discourse_wp_publish', __( 'Publishing Settings', 'wp-discourse' ), array(
$this,
'init_default_settings',
), 'discourse' );
add_settings_section( 'discourse_comments', __( 'Comments Settings', 'wp-discourse' ), array(
$this,
'init_comment_settings',
), 'discourse' );
add_settings_section( 'discourse_wp_sso', __( 'SSO Settings', 'wp-discourse' ), array(
$this,
'init_default_settings',
), 'discourse' );
add_settings_field( 'discourse_url', __( 'Discourse URL', 'wp-discourse' ), array(
$this,
'url_input',
), 'discourse', 'discourse_wp_api' );
add_settings_field( 'discourse_api_key', __( 'API Key', 'wp-discourse' ), array(
$this,
'api_key_input',
), 'discourse', 'discourse_wp_api' );
add_settings_field( 'discourse_publish_username', __( 'Publishing username', 'wp-discourse' ), array(
$this,
'publish_username_input',
), 'discourse', 'discourse_wp_api' );
add_settings_field( 'discourse_enable_sso', __( 'Enable SSO', 'wp-discourse' ), array(
$this,
'enable_sso_checkbox',
), 'discourse', 'discourse_wp_sso' );
add_settings_field( 'discourse_wp_login_path', __( 'Path to your login page', 'wp-discourse' ), array(
$this,
'wordpress_login_path',
), 'discourse', 'discourse_wp_sso' );
add_settings_field( 'discourse_sso_secret', __( 'SSO Secret Key', 'wp-discourse' ), array(
$this,
'sso_secret_input',
), 'discourse', 'discourse_wp_sso' );
add_settings_field( 'discourse_display_subcategories', __( 'Display subcategories', 'wp-discourse' ), array(
$this,
'display_subcategories',
), 'discourse', 'discourse_wp_publish' );
add_settings_field( 'discourse_publish_category', __( 'Published category', 'wp-discourse' ), array(
$this,
'publish_category_input',
), 'discourse', 'discourse_wp_publish' );
add_settings_field( 'discourse_publish_category_update', __( 'Force category update', 'wp-discourse' ), array(
$this,
'publish_category_input_update',
), 'discourse', 'discourse_wp_publish' );
add_settings_field( 'discourse_full_post_content', __( 'Use full post content', 'wp-discourse' ), array(
$this,
'full_post_checkbox',
), 'discourse', 'discourse_wp_publish' );
add_settings_field( 'discourse_auto_publish', __( 'Auto Publish', 'wp-discourse' ), array(
$this,
'auto_publish_checkbox',
), 'discourse', 'discourse_wp_publish' );
add_settings_field( 'discourse_auto_track', __( 'Auto Track Published Topics', 'wp-discourse' ), array(
$this,
'auto_track_checkbox',
), 'discourse', 'discourse_wp_publish' );
add_settings_field( 'discourse_allowed_post_types', __( 'Post Types to publish to Discourse', 'wp-discourse' ), array(
$this,
'post_types_select',
), 'discourse', 'discourse_wp_publish' );
add_settings_field( 'discourse_use_discourse_comments', __( 'Use Discourse Comments', 'wp-discourse' ), array(
$this,
'use_discourse_comments_checkbox',
), 'discourse', 'discourse_comments' );
add_settings_field( 'discourse_show_existing_comments', __( 'Show Existing WP Comments', 'wp-discourse' ), array(
$this,
'show_existing_comments_checkbox',
), 'discourse', 'discourse_comments' );
add_settings_field( 'discourse_existing_comments_heading', __( 'Existing Comments Heading', 'wp-discourse' ), array(
$this,
'existing_comments_heading_input',
), 'discourse', 'discourse_comments' );
add_settings_field( 'discourse_max_comments', __( 'Max visible comments', 'wp-discourse' ), array(
$this,
'max_comments_input',
), 'discourse', 'discourse_comments' );
add_settings_field( 'discourse_min_replies', __( 'Min number of replies', 'wp-discourse' ), array(
$this,
'min_replies_input',
), 'discourse', 'discourse_comments' );
add_settings_field( 'discourse_min_score', __( 'Min score of posts', 'wp-discourse' ), array(
$this,
'min_score_input',
), 'discourse', 'discourse_comments' );
add_settings_field( 'discourse_min_trust_level', __( 'Min trust level', 'wp-discourse' ), array(
$this,
'min_trust_level_input',
), 'discourse', 'discourse_comments' );
add_settings_field( 'discourse_bypass_trust_level_score', __( 'Bypass trust level score', 'wp-discourse' ), array(
$this,
'bypass_trust_level_input',
), 'discourse', 'discourse_comments' );
add_settings_field( 'discourse_custom_excerpt_length', __( 'Custom excerpt length', 'wp-discourse' ), array(
$this,
'custom_excerpt_length',
), 'discourse', 'discourse_comments' );
add_settings_field( 'discourse_custom_datetime_format', __( 'Custom Datetime Format', 'wp-discourse' ), array(
$this,
'custom_datetime_format',
), 'discourse', 'discourse_comments' );
add_settings_field( 'discourse_only_show_moderator_liked', __( 'Only import comments liked by a moderator', 'wp-discourse' ), array(
$this,
'only_show_moderator_liked_checkbox',
), 'discourse', 'discourse_comments' );
add_settings_field( 'discourse_debug_mode', __( 'Debug mode', 'wp-discourse' ), array(
$this,
'debug_mode_checkbox',
), 'discourse', 'discourse_comments' );
}
/**
* Adds Discourse username to the user contact methods.
*
* @param array $fields Contact information fields available to users.
*
* @return mixed
*/
function extend_user_profile( $fields ) {
$fields['discourse_username'] = 'Discourse Username';
return $fields;
}
/**
* Adds content to the top of the settings section.
*/
function init_default_settings() {
}
/**
* Adds content to the top of the comment section.
*/
function init_comment_settings() {
?>
<p class="documentation-link">
<em><?php esc_html_e( 'For documentation on customizing the plugin\'s html, visit ', 'wp-discourse' ); ?></em>
<a href="https://github.com/discourse/wp-discourse/wiki/Template-Customization">https://github.com/discourse/wp-discourse/wiki/Template-Customization</a>
</p>
<?php
}
/**
* Outputs markup for the Discourse-url input.
*/
function url_input() {
self::text_input( 'url', __( 'e.g. http://discourse.example.com', 'wp-discourse' ), 'url' );
}
/**
* Outputs markup for the login-path input.
*/
function wordpress_login_path() {
self::text_input( 'login-path', __( '(Optional) The path to your login page. It should start with \'/\'. Leave blank to use the default WordPress login page.', 'wp-discourse' ) );
}
/**
* Outputs markup for the api-key input.
*/
function api_key_input() {
$discourse_options = $this->options;
if ( isset( $discourse_options['url'] ) && ! empty( $discourse_options['url'] ) ) {
self::text_input( 'api-key', __( 'Found at ', 'wp-discourse' ) . '<a href="' . esc_url( $discourse_options['url'] ) . '/admin/api" target="_blank">' . esc_url( $discourse_options['url'] ) . '/admin/api</a>' );
} else {
self::text_input( 'api-key', __( 'Found at http://discourse.example.com/admin/api', 'wp-discourse' ) );
}
}
/**
* Outputs markup for the enable-sso checkbox.
*/
function enable_sso_checkbox() {
self::checkbox_input( 'enable-sso', __( 'Enable SSO to Discourse', 'wp-discourse' ) );
}
/**
* Outputs markup for the sso-secret input.
*/
function sso_secret_input() {
self::text_input( 'sso-secret', '' );
}
/**
* Outputs markup for the publish-username input.
*/
function publish_username_input() {
self::text_input( 'publish-username', __( 'Discourse username of publisher (will be overriden if Discourse Username is specified on user)', 'wp-discourse' ) );
}
/**
* Outputs markup for the display-subcategories checkbox.
*/
function display_subcategories() {
self::checkbox_input( 'display-subcategories', __( 'Include subcategories in the list of available categories.', 'wp-discourse' ) );
}
/**
* Outputs markup for the publish-category input.
*/
function publish_category_input() {
self::category_select( 'publish-category', __( 'Default category used to published in Discourse (optional)', 'wp-discourse' ) );
}
/**
* Outputs markup for the publish-category-update input.
*/
function publish_category_input_update() {
self::checkbox_input( 'publish-category-update', __( 'Update the discourse publish category list, (normally set to refresh every hour)', 'wp-discourse' ) );
}
/**
* Outputs markup for the max-comments input.
*/
function max_comments_input() {
self::text_input( 'max-comments', __( 'Maximum number of comments to display', 'wp-discourse' ), 'number' );
}
/**
* Outputs markup for the aoto-publish checkbox.
*/
function auto_publish_checkbox() {
self::checkbox_input( 'auto-publish', __( 'Publish all new posts to Discourse', 'wp-discourse' ) );
}
/**
* Outputs markup for the auto-track checkbox.
*/
function auto_track_checkbox() {
self::checkbox_input( 'auto-track', __( 'Author automatically tracks published Discourse topics', 'wp-discourse' ) );
}
/**
* Outputs markup for the post-types select input.
*/
function post_types_select() {
self::post_type_select_input( 'allowed_post_types',
$this->post_types_to_publish( array( 'attachment' ) ),
__( 'Hold the <strong>control</strong> button (Windows) or the <strong>command</strong> button (Mac) to select multiple options.', 'wp-discourse' ) );
}
/**
* Outputs markup for the use-discourse-comments checkbox.
*/
function use_discourse_comments_checkbox() {
self::checkbox_input( 'use-discourse-comments', __( 'Use Discourse to comment on Discourse published posts', 'wp-discourse' ) );
}
/**
* Outputs markup for the show-existing-comments checkbox.
*/
function show_existing_comments_checkbox() {
self::checkbox_input( 'show-existing-comments', __( 'Display existing WordPress comments beneath Discourse comments', 'wp-discourse' ) );
}
/**
* Outputs markup for the existing-comments-heading input.
*/
function existing_comments_heading_input() {
self::text_input( 'existing-comments-heading', __( 'Heading for existing WordPress comments (e.g. "Historical Comment Archive")', 'wp-discourse' ) );
}
/**
* Outputs markup for the min-replies input.
*/
function min_replies_input() {
self::text_input( 'min-replies', __( 'Minimum replies required prior to pulling comments across', 'wp-discourse' ), 'number', 0 );
}
/**
* Outputs markup for the min-trust-level input.
*/
function min_trust_level_input() {
self::text_input( 'min-trust-level', __( 'Minimum trust level required prior to pulling comments across (0-5)', 'wp-discourse' ), 'number', 0 );
}
/**
* Outputs markup for the min-score input.
*/
function min_score_input() {
self::text_input( 'min-score', __( 'Minimum score required prior to pulling comments across (score = 15 points per like, 5 per reply, 5 per incoming link, 0.2 per read)', 'wp-discourse' ), 'number', 0 );
}
/**
* Outputs markup for the custom-excerpt-length input.
*/
function custom_excerpt_length() {
self::text_input( 'custom-excerpt-length', __( 'Custom excerpt length in words (default: 55)', 'wp-discourse' ), 'number', 0 );
}
/**
* Outputs markup for the custom-datetime input.
*/
function custom_datetime_format() {
self::text_input( 'custom-datetime-format', __( 'Custom comment meta datetime string format (default: "', 'wp-discourse' ) .
get_option( 'date_format' ) . '").' .
__( ' See ', 'wp-discourse' ) . '<a href="https://codex.wordpress.org/Formatting_Date_and_Time" target="_blank">' .
__( 'this', 'wp-discourse' ) . '</a>' . __( ' for more info.', 'wp-discourse' ) );
}
/**
* Outputs markup for the bypass-trust-level input.
*/
function bypass_trust_level_input() {
self::text_input( 'bypass-trust-level-score', __( 'Bypass trust level check on posts with this score', 'wp-discourse' ), 'number', 0 );
}
/**
* Outputs markup for the debug-mode checkbox.
*/
function debug_mode_checkbox() {
self::checkbox_input( 'debug-mode', __( '(always refresh comments)', 'wp-discourse' ) );
}
/**
* Outputs markup for the use-full-post checkbox.
*/
function full_post_checkbox() {
self::checkbox_input( 'full-post-content', __( 'Use the full post for content rather than an excerpt.', 'wp-discourse' ) );
}
/**
* Outputs markup for the only-show-moderator-liked checkbox.
*/
function only_show_moderator_liked_checkbox() {
self::checkbox_input( 'only-show-moderator-liked', __( 'Yes', 'wp-discourse' ) );
}
/**
* Outputs the markup for a checkbox input.
*
* @param string $option The option name.
* @param string $label The text for the label.
* @param string $description The description of the settings field.
*/
function checkbox_input( $option, $label, $description = '' ) {
$options = $this->options;
if ( array_key_exists( $option, $options ) and 1 === intval( $options[ $option ] ) ) {
$checked = 'checked="checked"';
} else {
$checked = '';
}
?>
<label>
<input id='discourse_<?php echo esc_attr( $option ); ?>'
name='discourse[<?php echo esc_attr( $option ); ?>]' type='checkbox'
value='1' <?php echo esc_attr( $checked ); ?> />
<?php echo esc_html( $label ); ?>
</label>
<p class="description"><?php echo esc_html( $description ); ?></p>
<?php
}
/**
* Outputs the post-type select input.
*
* @param string $option Used to set the selected option.
* @param array $post_types An array of available post types.
* @param string $description The description of the settings field.
*/
function post_type_select_input( $option, $post_types, $description = '' ) {
$options = $this->options;
$allowed = array(
'strong' => array(),
);
echo "<select multiple id='discourse_allowed_post_types' class='discourse-allowed-types' name='discourse[allowed_post_types][]'>";
foreach ( $post_types as $post_type ) {
if ( array_key_exists( $option, $options ) and in_array( $post_type, $options[ $option ], true ) ) {
$value = 'selected';
} else {
$value = '';
}
echo '<option ' . esc_attr( $value ) . " value='" . esc_attr( $post_type ) . "'>" . esc_html( $post_type ) . '</option>';
}
echo '</select>';
echo '<p class="description">' . wp_kses( $description, $allowed ) . '</p>';
}
/**
* Outputs the markup for the categories select input.
*
* @param string $option The name of the option.
* @param string $description The description of the settings field.
*/
function category_select( $option, $description ) {
$options = get_option( 'discourse' );
$categories = DiscourseUtilities::get_discourse_categories();
if ( is_wp_error( $categories ) ) {
esc_html_e( 'The category list will be synced with Discourse when you establish a connection.' , 'wp-discourse' );
return;
}
$selected = isset( $options['publish-category'] ) ? $options['publish-category'] : '';
$name = "discourse[$option]";
self::option_input( $name, $categories, $selected );
}
/**
* Outputs the markup for an option input.
*
* @param string $name Suppies the 'name' value for the select input.
* @param array $group The array of items to be selected.
* @param int $selected The value of the selected option.
*/
function option_input( $name, $group, $selected ) {
echo '<select id="' . esc_attr( $name ) . '" name="' . esc_attr( $name ) . '">';
foreach ( $group as $item ) {
printf( '<option value="%s"%s>%s</option>',
esc_attr( $item['id'] ),
selected( $selected, $item['id'], false ),
esc_html( $item['name'] )
);
}
echo '</select>';
}
/**
* Outputs the markup for an input box, defaults to outputting a text input, but
* can be used for other types.
*
* @param string $option The name of the option.
* @param string $description The description of the settings field.
* @param null $type The type of input ('number', 'url', etc).
* @param null $min The min value (applied to number inputs).
*/
function text_input( $option, $description, $type = null, $min = null ) {
$options = $this->options;
$allowed = array(
'a' => array(
'href' => array(),
'target' => array(),
),
);
if ( array_key_exists( $option, $options ) ) {
$value = $options[ $option ];
} else {
$value = '';
}
?>
<input id='discourse_<?php echo esc_attr( $option ); ?>' name='discourse[<?php echo esc_attr( $option ); ?>]'
type="<?php echo isset( $type ) ? esc_attr( $type ) : 'text'; ?>"
<?php if ( isset( $min ) ) {
echo 'min="' . esc_attr( $min ) . '"';
} ?>
value='<?php echo esc_attr( $value ); ?>' class="regular-text ltr"/>
<p class="description"><?php echo wp_kses( $description, $allowed ); ?></p>
<?php
}
/**
* Outputs the markup for a text area.
*
* @param string $option The name of the option.
* @param string $description The description of the settings field.
*/
function text_area( $option, $description ) {
$options = $this->options;
if ( array_key_exists( $option, $options ) ) {
$value = $options[ $option ];
} else {
$value = '';
}
?>
<textarea cols=100 rows=6 id='discourse_<?php echo esc_attr( $option ); ?>'
name='discourse[<?php echo esc_attr( $option ); ?>]'><?php echo esc_textarea( $value ); ?></textarea>
<p class="description"><?php echo esc_html( $description ); ?></p>
<?php
}
/**
* The callback for validating the 'discourse' options.
*
* @param array $inputs The inputs to be validated.
*
* @return array
*/
function discourse_validate_options( $inputs ) {
$output = array();
foreach ( $inputs as $key => $input ) {
$filter = 'validate_' . str_replace( '-', '_', $key );
$output[ $key ] = apply_filters( $filter, $input );
}
return $output;
}
/**
* Adds the Discourse options page to the admin menu.
*
* Hooks into the 'admin_menu' action.
*/
function discourse_admin_menu() {
add_options_page( __( 'Discourse', 'wp-discourse' ), __( 'Discourse', 'wp-discourse' ), 'manage_options', 'discourse', array(
$this,
'discourse_options_page',
) );
}
/**
* The callback for creating the Discourse options page.
*/
function discourse_options_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wp-discourse' ) );
}
?>
<div class="wrap">
<h2>Discourse Options</h2>
<p class="documentation-link">
<em><?php esc_html_e( 'The WP Discourse plugin documentation can be found at ', 'wp-discourse' ); ?></em>
<a href="https://github.com/discourse/wp-discourse/wiki">https://github.com/discourse/wp-discourse/wiki</a>
</p>
<form action="options.php" method="POST">
<?php settings_fields( 'discourse' ); ?>
<?php do_settings_sections( 'discourse' ); ?>
<?php submit_button(); ?>
</form>
</div>
<?php
}
/**
* Outputs the markup for the 'connected' notice.
*/
function connection_status_notice() {
if ( ! DiscourseUtilities::check_connection_status() ) {
add_action( 'admin_notices', array( $this, 'disconnected' ) );
} else {
add_action( 'admin_notices', array( $this, 'connected' ) );
}
}
/**
* Outputs the markup for the 'disconnected' notice.
*/
function disconnected() {
?>
<div class="notice notice-warning is-dismissible">
<p>
<strong><?php esc_html_e( 'You are not currently connected to a Discourse forum. ' .
"To establish a connection, check your settings for 'Discourse URL', 'API Key', and 'Publishing username'. " .
'Also, make sure that your Discourse forum is online.', 'wp-discourse' ); ?></strong>
</p>
</div>
<?php
}
/**
* Outputs the markup for the 'connected' notice.
*/
function connected() {
?>
<div class="notice notice-success is-dismissible">
<p>
<strong><?php esc_html_e( 'You are connected to Discourse!', 'wp-discourse' ); ?></strong>
</p>
</div>
<?php
}
/**
* Returns the 'public' post-types minus the post-types in the 'excluded' array.
*
* @param array $excluded_types An array of post-types to exclude from publishing to Discourse.
*
* @return mixed|void
*/
protected function post_types_to_publish( $excluded_types = array() ) {
$post_types = get_post_types( array( 'public' => true ) );
foreach ( $excluded_types as $excluded ) {
unset( $post_types[ $excluded ] );
}
return apply_filters( 'discourse_post_types_to_publish', $post_types );
}
}

189
lib/discourse-comment.php Normal file
View file

@ -0,0 +1,189 @@
<?php
/**
* Syncs Discourse comments with WordPress posts.
*
* @package WPDiscourse
*/
namespace WPDiscourse\DiscourseComment;
use WPDiscourse\Utilities\Utilities as DiscourseUtilities;
/**
* Class DiscourseComment
*/
class DiscourseComment {
/**
* Gives access to the plugin options.
*
* @access protected
* @var mixed|void
*/
protected $options;
/**
* DiscourseComment constructor.
*/
public function __construct() {
$this->options = get_option( 'discourse' );
add_filter( 'comments_number', array( $this, 'comments_number' ) );
add_filter( 'comments_template', array( $this, 'comments_template' ), 20, 1 );
add_action( 'wp_enqueue_scripts', array( $this, 'discourse_comments_js' ) );
}
/**
* Enqueues the `comments.js` script.
*
* Hooks into 'wp_enqueue_scripts'.
*/
function discourse_comments_js() {
if ( is_singular( $this->options['allowed_post_types'] ) ) {
if ( $this->use_discourse_comments( get_the_ID() ) ) {
wp_enqueue_script(
'discourse-comments-js',
WPDISCOURSE_URL . '/js/comments.js',
array( 'jquery' ),
get_option( 'discourse_version' ),
true
);
// Localize script.
$data = array(
'url' => $this->options['url'],
);
wp_localize_script( 'discourse-comments-js', 'discourse', $data );
}
}
}
/**
* Checks if a post is using Discourse comments.
*
* @param int $postid The ID of the post.
*
* @return bool|int
*/
protected function use_discourse_comments( $postid ) {
if ( ( ! isset( $this->options['use-discourse-comments'] ) ) || ! $this->options['use-discourse-comments'] ) {
return 0;
}
$setting = get_post_meta( $postid, 'publish_to_discourse', true );
return 1 === intval( $setting );
}
/**
* Syncs Discourse comments to WordPress.
*
* @param int $postid The WordPress post id.
*/
function sync_comments( $postid ) {
$discourse_options = $this->options;
// Every 10 minutes do a json call to sync comment count and top comments.
$last_sync = (int) get_post_meta( $postid, 'discourse_last_sync', true );
$time = date_create()->format( 'U' );
$debug = isset( $discourse_options['debug-mode'] ) && 1 === intval( $discourse_options['debug-mode'] );
if ( $debug || $last_sync + 60 * 10 < $time ) {
$lock = 'comments_locked_for_' . $postid;
if ( ! 'locked' === get_transient( $lock ) ) {
set_transient( $lock, 'locked' );
if ( 'publish' === get_post_status( $postid ) ) {
$comment_count = intval( $discourse_options['max-comments'] );
$min_trust_level = intval( $discourse_options['min-trust-level'] );
$min_score = intval( $discourse_options['min-score'] );
$min_replies = intval( $discourse_options['min-replies'] );
$bypass_trust_level_score = intval( $discourse_options['bypass-trust-level-score'] );
$options = 'best=' . $comment_count . '&min_trust_level=' . $min_trust_level . '&min_score=' . $min_score;
$options = $options . '&min_replies=' . $min_replies . '&bypass_trust_level_score=' . $bypass_trust_level_score;
if ( isset( $discourse_options['only-show-moderator-liked'] ) && 1 === intval( $discourse_options['only-show-moderator-liked'] ) ) {
$options = $options . '&only_moderator_liked=true';
}
$options = $options . '&api_key=' . $discourse_options['api-key'] . '&api_username=' . $discourse_options['publish-username'];
$permalink = esc_url_raw( get_post_meta( $postid, 'discourse_permalink', true ) ) . '/wordpress.json?' . $options;
$result = wp_remote_get( $permalink );
if ( DiscourseUtilities::validate( $result ) ) {
$json = json_decode( $result['body'] );
if ( isset( $json->posts_count ) ) {
$posts_count = $json->posts_count - 1;
if ( $posts_count < 0 ) {
$posts_count = 0;
}
update_post_meta( $postid, 'discourse_comments_count', $posts_count );
update_post_meta( $postid, 'discourse_comments_raw', esc_sql( $result['body'] ) );
update_post_meta( $postid, 'discourse_last_sync', $time );
}
}
}
delete_transient( $lock );
}
}
}
/**
* Loads the comments template.
*
* Hooks into 'comments_template'.
*
* @param string $old The comments template returned by WordPress.
*
* @return string
*/
function comments_template( $old ) {
global $post;
if ( $this->use_discourse_comments( $post->ID ) ) {
$this->sync_comments( $post->ID );
$options = $this->options;
$num_wp_comments = get_comments_number();
if ( ( isset( $options['show-existing-comments'] ) && ( 0 === intval( $options['show-existing-comments'] ) ) ) ||
0 === intval( $num_wp_comments ) ) {
// Only show the Discourse comments.
return WPDISCOURSE_PATH . 'templates/comments.php';
} else {
// Show the Discourse comments then show the existing WP comments (in $old).
include WPDISCOURSE_PATH . 'templates/comments.php';
echo '<div class="discourse-existing-comments-heading">' . wp_kses_post( $options['existing-comments-heading'] ) . '</div>';
return $old;
}
}
// Show the existing WP comments.
return $old;
}
/**
* Returns the comments number.
*
* If Discourse comments are enabled, returns the 'discourse_comments_count', otherwise
* returns the $count value. Hooks into 'comments_number'.
*
* @param int $count The comment count supplied by WordPress.
*
* @return mixed|string
*/
function comments_number( $count ) {
global $post;
if ( $this->use_discourse_comments( $post->ID ) ) {
$this->sync_comments( $post->ID );
$count = get_post_meta( $post->ID, 'discourse_comments_count', true );
if ( ! $count ) {
$count = 'Leave a reply';
} else {
$count = ( 1 === intval( $count ) ) ? '1 Reply' : $count . ' Replies';
}
}
return $count;
}
}

242
lib/discourse-publish.php Normal file
View file

@ -0,0 +1,242 @@
<?php
/**
* Publishes a post to Discourse.
*
* @package WPDicourse
*/
namespace WPDiscourse\DiscoursePublish;
use WPDiscourse\Templates\HTMLTemplates as Templates;
use WPDiscourse\Utilities\Utilities as DiscourseUtilities;
/**
* Class DiscoursePublish
*/
class DiscoursePublish {
/**
* Gives access to the plugin options.
*
* @access protected
* @var mixed|void
*/
protected $options;
/**
* DiscoursePublish constructor.
*/
public function __construct() {
$this->options = get_option( 'discourse' );
// Priority is set to 13 so that 'publish_post_after_save' is called after the meta-box is saved.
add_action( 'save_post', array( $this, 'publish_post_after_save' ), 13, 2 );
add_action( 'transition_post_status', array( $this, 'publish_post_after_transition' ), 10, 3 );
add_action( 'xmlrpc_publish_post', array( $this, 'xmlrpc_publish_post_to_discourse' ) );
}
/**
* Publishes a post to Discourse after its status has transitioned.
*
* This function is called when post status changes. Hooks into 'transition_post_status'.
*
* @param string $new_status New post status after an update.
* @param string $old_status The old post status.
* @param object $post The post object.
*/
function publish_post_after_transition( $new_status, $old_status, $post ) {
$publish_to_discourse = get_post_meta( $post->ID, 'publish_to_discourse', true );
if ( $publish_to_discourse && 'publish' === $new_status && $this->is_valid_sync_post_type( $post->ID ) ) {
$this->sync_to_discourse( $post->ID, $post->post_title, $post->post_content );
}
}
/**
* Published a post to Discourse after it has been saved.
*
* @param int $post_id The id of the post that has been saved.
* @param object $post The Post object.
*/
public function publish_post_after_save( $post_id, $post ) {
if ( wp_is_post_revision( $post_id ) ) {
return;
}
$post_is_published = 'publish' === get_post_status( $post_id );
$publish_to_discourse = get_post_meta( $post_id, 'publish_to_discourse', true );
if ( $publish_to_discourse && $post_is_published && $this->is_valid_sync_post_type( $post_id ) ) {
$this->sync_to_discourse( $post_id, $post->post_title, $post->post_content );
}
}
/**
* For publishing by xmlrpc.
*
* Hooks into 'xmlrpc_publish_post'.
*
* @param int $postid The post id.
*/
public function xmlrpc_publish_post_to_discourse( $postid ) {
$post = get_post( $postid );
if ( 'publish' === get_post_status( $postid ) && $this->is_valid_sync_post_type( $postid ) ) {
update_post_meta( $postid, 'publish_to_discourse', 1 );
$this->sync_to_discourse( $postid, $post->post_title, $post->post_content );
}
}
/**
* Calls `sync_do_discourse_work` after getting the lock.
*
* @param int $postid The post id.
* @param string $title The title.
* @param string $raw The raw content of the post.
*/
public function sync_to_discourse( $postid, $title, $raw ) {
$lock = 'publishing_locked_for_post_' . $postid;
// This avoids a double sync, just 1 is allowed to go through at a time.
if ( ! 'locked' === get_transient( $lock ) ) {
set_transient( $lock, 'locked' );
$this->sync_to_discourse_work( $postid, $title, $raw );
delete_transient( $lock );
}
}
/**
* Syncs a post to Discourse.
*
* @param int $postid The post id.
* @param string $title The post title.
* @param string $raw The content of the post.
*/
protected function sync_to_discourse_work( $postid, $title, $raw ) {
$discourse_id = get_post_meta( $postid, 'discourse_post_id', true );
$options = $this->options;
$discourse_post = get_post( $postid );
$use_full_post = isset( $options['full-post-content'] ) && 1 === intval( $options['full-post-content'] );
if ( $use_full_post ) {
$excerpt = apply_filters( 'wp_discourse_excerpt', $raw );
} else {
if ( has_excerpt( $postid ) ) {
$wp_excerpt = apply_filters( 'get_the_excerpt', $discourse_post->post_excerpt );
$excerpt = apply_filters( 'wp_discourse_excerpt', $wp_excerpt );
} else {
$excerpt = apply_filters( 'the_content', $raw );
$excerpt = apply_filters( 'wp_discourse_excerpt', wp_trim_words( $excerpt, $options['custom-excerpt-length'] ) );
}
}
// Trim to keep the Discourse markdown parser from treating this as code.
$baked = trim( Templates::publish_format_html() );
$baked = str_replace( '{excerpt}', $excerpt, $baked );
$baked = str_replace( '{blogurl}', get_permalink( $postid ), $baked );
$author_id = $discourse_post->post_author;
$author = get_the_author_meta( 'display_name', $author_id );
$baked = str_replace( '{author}', $author, $baked );
$thumb = wp_get_attachment_image_src( get_post_thumbnail_id( $postid ), 'thumbnail' );
$baked = str_replace( '{thumbnail}', '![image](' . $thumb['0'] . ')', $baked );
$featured = wp_get_attachment_image_src( get_post_thumbnail_id( $postid ), 'full' );
$baked = str_replace( '{featuredimage}', '![image](' . $featured['0'] . ')', $baked );
$username = get_the_author_meta( 'discourse_username', $discourse_post->post_author );
if ( ! $username || strlen( $username ) < 2 ) {
$username = $options['publish-username'];
}
// Get publish category of a post.
$publish_post_category = get_post_meta( $discourse_post->ID, 'publish_post_category', true );
$default_category = isset( $options['publish-category'] ) ? $options['publish-category'] : '';
$category = isset( $publish_post_category ) ? $publish_post_category : $default_category;
if ( ! $discourse_id > 0 ) {
$data = array(
'wp-id' => $postid,
'embed_url' => get_permalink( $postid ),
'api_key' => $options['api-key'],
'api_username' => $username,
'title' => $title,
'raw' => $baked,
'category' => $category,
'skip_validations' => 'true',
'auto_track' => ( isset( $options['auto-track'] ) && 1 === intval( $options['auto-track'] ) ? 'true' : 'false' ),
);
$url = $options['url'] . '/posts';
// Use key 'http' even if you send the request to https://.
$post_options = array(
'timeout' => 30,
'method' => 'POST',
'body' => http_build_query( $data ),
);
$result = wp_remote_post( $url, $post_options );
if ( DiscourseUtilities::validate( $result ) ) {
$json = json_decode( $result['body'] );
if ( property_exists( $json, 'id' ) ) {
$discourse_id = (int) $json->id;
}
if ( isset( $discourse_id ) && $discourse_id > 0 ) {
add_post_meta( $postid, 'discourse_post_id', $discourse_id, true );
}
}
} else {
$data = array(
'api_key' => $options['api-key'],
'api_username' => $username,
'post[raw]' => $baked,
'skip_validations' => 'true',
);
$url = $options['url'] . '/posts/' . $discourse_id;
$post_options = array(
'timeout' => 30,
'method' => 'PUT',
'body' => http_build_query( $data ),
);
$result = wp_remote_post( $url, $post_options );
if ( DiscourseUtilities::validate( $result ) ) {
$json = json_decode( $result['body'] );
if ( property_exists( $json, 'id' ) ) {
$discourse_id = (int) $json->id;
}
if ( isset( $discourse_id ) && $discourse_id > 0 ) {
add_post_meta( $postid, 'discourse_post_id', $discourse_id, true );
}
}
}
if ( isset( $json->topic_slug ) ) {
delete_post_meta( $postid, 'discourse_permalink' );
add_post_meta( $postid, 'discourse_permalink', $options['url'] . '/t/' . $json->topic_slug . '/' . $json->topic_id, true );
}
}
/**
* Checks if a post_type can be synced.
*
* @param null $postid The ID of the post in question.
*
* @return bool
*/
protected function is_valid_sync_post_type( $postid = null ) {
$allowed_post_types = $this->get_allowed_post_types();
$current_post_type = get_post_type( $postid );
return in_array( $current_post_type, $allowed_post_types, true );
}
/**
* Returns the array of allowed post types.
*
* @return mixed
*/
protected function get_allowed_post_types() {
$selected_post_types = $this->options['allowed_post_types'];
return $selected_post_types;
}
}

174
lib/discourse-sso.php Normal file
View file

@ -0,0 +1,174 @@
<?php
/**
* Allows for Single Sign On between between WordPress and Discourse.
*
* @package WPDiscourse\DiscourseSSO
*/
namespace WPDiscourse\DiscourseSSO;
/**
* Class DiscourseSSO
*/
class DiscourseSSO {
/**
* Gives access to the plugin options.
*
* @access protected
* @var mixed|void
*/
protected $options;
/**
* DiscourseSSO constructor.
*/
public function __construct() {
$this->options = get_option( 'discourse' );
add_filter( 'query_vars', array( $this, 'sso_add_query_vars' ) );
add_filter( 'login_url', array( $this, 'set_login_url' ), 10, 2 );
add_action( 'parse_query', array( $this, 'sso_parse_request' ) );
}
/**
* Allows the login_url to be configured.
*
* Hooks into the 'login_url' filter. If the 'login-path' option has been set the supplied path
* is used instead of the default WordPress login path.
*
* @param string $login_url The WordPress login url.
* @param string $redirect The after-login redirect, supplied by WordPress.
*
* @return string
*/
public function set_login_url( $login_url, $redirect ) {
$options = get_option( 'discourse' );
if ( $options['login-path'] ) {
$login_url = $options['login-path'];
if ( ! empty( $redirect ) ) {
return add_query_arg( 'redirect_to', urlencode( $redirect ), $login_url );
} else {
return $login_url;
}
}
if ( ! empty( $redirect ) ) {
return add_query_arg( 'redirect_to', urlencode( $redirect ), $login_url );
} else {
return $login_url;
}
}
/**
* Adds the 'sso' and 'sig' keys to the query_vars array.
*
* Hooks into 'query_vars'.
*
* @param array $vars The array of query vars.
*
* @return array
*/
public function sso_add_query_vars( $vars ) {
$vars[] = 'sso';
$vars[] = 'sig';
return $vars;
}
/**
* SSO Request Processing from Adam Capirola : https://gist.github.com/adamcapriola/11300529.
*
* Enables single sign on between WordPress and Discourse.
* Hooks into the 'parse_query' filter.
*
* @param WP_Query $wp The query object that parsed the query.
*
* @throws Exception Throws an exception it SSO helper class is not included, or the payload can't be validated against the sig.
*/
function sso_parse_request( $wp ) {
/**
* Sync logout from Discourse to WordPress from Adam Capirola : https://meta.discourse.org/t/wordpress-integration-guide/27531.
* To make this work, enter a URL of the form "http://my-wp-blog.com/?request=logout" in the "logout redirect"
* field in your Discourse admin
*/
if ( isset( $this->options['enable-sso'] ) &&
1 === intval( $this->options['enable-sso'] ) &&
isset( $_GET['request'] ) && // Input var okay.
'logout' === $_GET['request'] // Input var okay.
) {
wp_logout();
wp_redirect( $this->options['url'] );
exit;
}
// End logout processing.
if ( isset( $this->options['enable-sso'] ) &&
1 === intval( $this->options['enable-sso'] ) &&
array_key_exists( 'sso', $wp->query_vars ) &&
array_key_exists( 'sig', $wp->query_vars )
) {
// Not logged in to WordPress, redirect to WordPress login page with redirect back to here.
if ( ! is_user_logged_in() ) {
// Preserve sso and sig parameters.
$redirect = add_query_arg( null, null );
// Change %0A to %0B so it's not stripped out in wp_sanitize_redirect.
$redirect = str_replace( '%0A', '%0B', $redirect );
// Build login URL.
$login = wp_login_url( esc_url_raw( $redirect ) );
// Redirect to login.
wp_redirect( $login );
exit;
} else {
// Check for helper class.
if ( ! class_exists( '\\WPDiscourse\\SSO\\Discourse_SSO' ) ) {
echo( 'Helper class is not properly included.' );
exit;
}
// Payload and signature.
$payload = $wp->query_vars['sso'];
$sig = $wp->query_vars['sig'];
// Change %0B back to %0A.
$payload = urldecode( str_replace( '%0B', '%0A', urlencode( $payload ) ) );
// Validate signature.
$sso_secret = $this->options['sso-secret'];
$sso = new \WPDiscourse\SSO\Discourse_SSO( $sso_secret );
if ( ! ( $sso->validate( $payload, $sig ) ) ) {
echo( 'Invalid request.' );
exit;
}
$nonce = $sso->get_nonce( $payload );
$current_user = wp_get_current_user();
$params = array(
'nonce' => $nonce,
'name' => $current_user->display_name,
'username' => $current_user->user_login,
'email' => $current_user->user_email,
'about_me' => $current_user->description,
'external_id' => $current_user->ID,
'avatar_url' => get_avatar_url( get_current_user_id() ),
);
$q = $sso->build_login_string( $params );
// Redirect back to Discourse.
wp_redirect( $this->options['url'] . '/session/sso_login?' . $q );
exit;
}
}
}
}

85
lib/discourse.php Normal file
View file

@ -0,0 +1,85 @@
<?php
/**
* Sets up the plugin.
*
* @package WPDiscourse
*/
namespace WPDiscourse\Discourse;
/**
* Class Discourse
*/
class Discourse {
/**
* Sets the plugin version.
*
* @var string
*/
public static $version = '0.7.0';
/**
* The default options.
*
* The options can be accessed in any file with `get_option( 'discourse' )`.
*
* @var array
*/
static $options = array(
'url' => '',
'api-key' => '',
'enable-sso' => 0,
'sso-secret' => '',
'publish-username' => 'system',
'display-subcategories' => 0,
'publish-category' => '',
'auto-publish' => 0,
'allowed_post_types' => array( 'post' ),
'auto-track' => 1,
'max-comments' => 5,
'use-discourse-comments' => 0,
'show-existing-comments' => 0,
'min-score' => 0,
'min-replies' => 1,
'min-trust-level' => 1,
'custom-excerpt-length' => 55,
'bypass-trust-level-score' => 50,
'debug-mode' => 0,
'full-post-content' => 0,
'only-show-moderator-liked' => 0,
'login-path' => '',
);
/**
* Discourse constructor.
*/
public function __construct() {
load_plugin_textdomain( 'wp-discourse', false, basename( dirname( __FILE__ ) ) . '/languages' );
add_filter( 'user_contactmethods', array( $this, 'extend_user_profile' ), 10, 1 );
}
/**
* Adds the options 'discourse' and 'discourse_version'.
*
* Called with `register_activation_hook` from `wp-discourse.php`.
*/
public static function install() {
update_option( 'discourse_version', self::$version );
add_option( 'discourse', self::$options );
}
/**
* Adds 'discourse_username' to the user_contactmethods array.
*
* @param array $fields The array of contact methods.
*
* @return mixed
*/
function extend_user_profile( $fields ) {
$fields['discourse_username'] = 'Discourse Username';
return $fields;
}
}

190
lib/html-templates.php Normal file
View file

@ -0,0 +1,190 @@
<?php
/**
* Returns HTML templates used for publishing to Discourse and for displaying comments on the WordPress site.
*
* Templates and implementation copied from @aliso's commit:
* https://github.com/10up/wp-discourse/commit/5c9d43c4333e136204d5a3b07192f4b368c3f518.
*
* @link https://github.com/discourse/wp-discourse/blob/master/lib/html-templates.php
* @package WPDiscourse
*/
namespace WPDiscourse\Templates;
/**
* Class HTMLTemplates
*/
class HTMLTemplates {
/**
* HTML template for replies.
*
* Can be customized from within a theme using the filter provided.
*
* Available tags:
* {comments}, {discourse_url}, {discourse_url_name},
* {topic_url}, {more_replies}, {participants}
*
* @static
*
* @return mixed|void
*/
public static function replies_html() {
ob_start();
?>
<div id="comments" class="comments-area">
<h2 class="comments-title"><?php esc_html_e( 'Notable Replies', 'wp-discourse' ); ?></h2>
<ol class="comment-list">{comments}</ol>
<div class="respond comment-respond">
<h3 id="reply-title" class="comment-reply-title">
<a href="{topic_url}"><?php esc_html_e( 'Continue the discussion', 'wp-discourse' ); ?>
</a><?php esc_html_e( ' at ', 'wp-discourse' ); ?>{discourse_url_name}
</h3>
<p class="more-replies">{more_replies}</p>
<p class="comment-reply-title">{participants}</p>
</div><!-- #respond -->
</div>
<?php
$output = ob_get_clean();
return apply_filters( 'discourse_replies_html', $output );
}
/**
* HTML template for no replies.
*
* Can be customized from within a theme using the filter provided.
*
* Available tags:
* {comments}, {discourse_url}, {discourse_url_name}, {topic_url}
*
* @static
* @return mixed|void
*/
public static function no_replies_html() {
ob_start();
?>
<div id="comments" class="comments-area">
<div class="respond comment-respond">
<h3 id="reply-title" class="comment-reply-title"><a href="{topic_url}">
<?php esc_html_e( 'Start the discussion', 'wp-discourse' ); ?>
</a><?php esc_html_e( ' at ', 'wp-discourse' ); ?>{discourse_url_name}</h3>
</div><!-- #respond -->
</div>
<?php
$output = ob_get_clean();
return apply_filters( 'discourse_no_replies_html', $output );
}
/**
* The template that is displayed in the comments section after a post is created
* with bad credentials.
* This template is displayed in the comments section when there is no `discourse_permalink`
* index in the response returned from `Discourse::sync_to_discourse_work`
*
* Can be customized in the theme using the filter provided.
*
* @return mixed|void
*/
public static function bad_response_html() {
ob_start();
?>
<div class="respond comment-respond">
<div class="comment-reply-title discourse-no-connection-notice">
<p><?php esc_html_e( 'Comments are not enabled for this post.', 'wp-discourse' ); ?></p>
</div>
</div>
<?php
$output = ob_get_clean();
return apply_filters( 'discourse_no_connection_html', $output );
}
/**
* HTML template for each comment
*
* Can be customized from within a theme using the filter provided.
*
* Available tags:
* {discourse_url}, {discourse_url_name}, {topic_url},
* {avatar_url}, {user_url}, {username}, {fullname},
* {comment_body}, {comment_created_at}, {comment_url}
*
* @static
* @return mixed|void
*/
public static function comment_html() {
ob_start();
?>
<li class="comment even thread-even depth-1">
<article class="comment-body">
<footer class="comment-meta">
<div class="comment-author vcard">
<img alt="" src="{avatar_url}" class="avatar avatar-64 photo avatar-default" height="64"
width="64">
<b class="fn"><a href="{topic_url}" rel="external" class="url">{fullname}</a></b>
<span class="says">says:</span>
</div>
<!-- .comment-author -->
<div class="comment-metadata">
<time pubdate="" datetime="{comment_created_at}">{comment_created_at}</time>
</div>
<!-- .comment-metadata -->
</footer>
<!-- .comment-meta -->
<div class="comment-content">{comment_body}</div>
<!-- .comment-content -->
</article>
<!-- .comment-body -->
</li>
<?php
$output = ob_get_clean();
return apply_filters( 'discourse_comment_html', $output );
}
/**
* HTML template for each participant
*
* Can be customized from within a theme using the filter provided.
*
* Available tags:
* {discourse_url}, {discourse_url_name}, {topic_url},
* {avatar_url}, {user_url}, {username}
*
* @static
* @return mixed|void
*/
public static function participant_html() {
ob_start();
?>
<img alt="" src="{avatar_url}" class="avatar avatar-25 photo avatar-default" height="25"
width="25">
<?php
$output = ob_get_clean();
return apply_filters( 'discourse_participant_html', $output );
}
/**
* HTML template for published byline
*
* Can be customized from within a theme using the filter provided.
*
* Available tags:
* {excerpt}, {blogurl}, {author}, {thumbnail}, {featuredimage}
*
* @static
* @return mixed|void
*/
public static function publish_format_html() {
ob_start();
?>
<small>Originally published at: {blogurl}</small><br>{excerpt}
<?php
$output = ob_get_clean();
return apply_filters( 'discourse_publish_format_html', $output );
}
}

144
lib/meta-box.php Normal file
View file

@ -0,0 +1,144 @@
<?php
/**
* Adds a Discourse Publish meta box to posts that may be published to Discourse.
*
* @package WPDiscourse
*/
namespace WPDiscourse\MetaBox;
use WPDiscourse\Utilities\Utilities as DiscourseUtilities;
/**
* Class MetaBox
*/
class MetaBox {
/**
* Gives access to the plugin options.
*
* @access protected
* @var mixed|void
*/
protected $options;
/**
* MetaBox constructor.
*/
public function __construct() {
$this->options = get_option( 'discourse' );
add_action( 'add_meta_boxes', array( $this, 'add_meta_box' ) );
add_action( 'save_post', array( $this, 'save_meta_box' ), 10, 1 );
}
/**
* Registers a meta box for the allowed post types.
*
* @param string $post_type The post_type of the current post.
*/
public function add_meta_box( $post_type ) {
if ( isset( $this->options['allowed_post_types'] ) &&
in_array( $post_type, $this->options['allowed_post_types'], true )
) {
add_meta_box( 'discourse-publish-meta-box', esc_html__( 'Publish to Discourse' ), array(
$this,
'render_meta_box',
), null, 'side', 'high', null );
}
}
/**
* The callback function for creating the meta box.
*
* @param object $post The current Post object.
*/
public function render_meta_box( $post ) {
$categories = DiscourseUtilities::get_discourse_categories();
if ( is_wp_error( $categories ) ) {
$selected_category = null;
$publish_to_discourse = 0;
} elseif ( ! get_post_meta( $post->ID, 'has_been_saved', true ) ) {
// If the post has not yet been saved, use the default setting. If it has been saved use the meta value.
$selected_category = isset( $this->options['publish-category'] ) ? intval( $this->options['publish-category'] ) : 1;
$publish_to_discourse = isset( $this->options['auto-publish'] ) ? intval( $this->options['auto-publish'] ) : 0;
} else {
$selected_category = get_post_meta( $post->ID, 'publish_post_category', true );
$publish_to_discourse = get_post_meta( $post->ID, 'publish_to_discourse', true );
}
wp_nonce_field( 'publish_to_discourse', 'publish_to_discourse_nonce' );
?>
<label for="publish_to_discourse"><?php esc_html_e( 'Publish post to Discourse:', 'wp-discourse' ); ?>
<input type="checkbox" name="publish_to_discourse" id="publish_to_discourse" value="1"
<?php checked( $publish_to_discourse ); ?> >
</label>
<br>
<label for="publish_post_category"><?php esc_html_e( 'Category to publish to:', 'wp-discourse' ); ?>
<?php if ( is_null( $selected_category ) ) : ?>
<div class="warning">
<p>
<?php
esc_html_e( "The Discourse categories list is not currently available. To publish this post to Discourse, please check the wp-discourse settings for 'Discourse URL', 'API Key', and 'Publishing username'. Also, make sure that your Discourse forum is online.", 'wp-discourse' );
?>
</p>
</div>
<?php else : ?>
<select name="publish_post_category" id="publish_post_category">
<?php foreach ( $categories as $category ) : ?>
<option
value="<?php echo( esc_attr( $category['id'] ) ); ?>"
<?php selected( $selected_category, $category['id'] ); ?>>
<?php echo( esc_html( $category['name'] ) ); ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
</label>
<?php
}
/**
* Verifies the nonce and saves the meta data.
*
* @param int $post_id The id of the current post.
*
* @return int
*/
function save_meta_box( $post_id ) {
if ( ! isset( $_POST['publish_to_discourse_nonce'] ) || // Input var okay.
! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['publish_to_discourse_nonce'] ) ), 'publish_to_discourse' ) // Input var okay.
) {
return 0;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return 0;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return 0;
}
// Indicate that the post has been saved so that the meta-box gets its values from the meta-data instead of the defaults.
update_post_meta( $post_id, 'has_been_saved', 1 );
if ( isset( $_POST['publish_post_category'] ) ) { // Input var okay.
update_post_meta( $post_id, 'publish_post_category', intval( wp_unslash( $_POST['publish_post_category'] ) ) ); // Input var okay.
}
if ( isset( $_POST['publish_to_discourse'] ) ) { // Input var okay.
update_post_meta( $post_id, 'publish_to_discourse', intval( wp_unslash( $_POST['publish_to_discourse'] ) ) ); // Input var okay.
} else {
update_post_meta( $post_id, 'publish_to_discourse', 0 );
}
return $post_id;
}
}

View file

@ -0,0 +1,61 @@
<?php
/**
* Adds limitied support for the WooCommerce plugin.
*
* This file will soon be moved into its own plugin.
*
* @link https://github.com/discourse/wp-discourse/blob/master/lib/plugin-support/woocommerce_support.php
* @package WPDiscourse\PluginSupport
*/
namespace WPDiscourse\PluginSupport;
/**
* Class WooCommerceSupport
*/
class WooCommerceSupport {
/**
* WooCommerceSupport constructor.
*/
function __construct() {
add_filter( 'woocommerce_login_redirect', array( $this, 'set_redirect' ) );
add_filter( 'woocommerce_product_review_count', array( $this, 'comments_number' ) );
}
/**
* Replaces the WooCommerce comments count with the Discourse comments count.
*
* @param int $count The comments count returned from WooCommerce.
*
* @return mixed
*/
function comments_number( $count ) {
global $post;
$options = get_option( 'discourse' );
if ( array_key_exists( 'allowed_post_types', $options ) && in_array( 'product', $options['allowed_post_types'], true ) ) {
$count = get_post_meta( $post->ID, 'discourse_comments_count', true );
return $count;
}
return $count;
}
/**
* Sets the login redirect so that it can include the query parameters required for single sign on with Discourse.
*
* @param string $redirect The redirect URL supplied by WooCommerce.
*
* @return mixed
*/
function set_redirect( $redirect ) {
if ( isset( $_GET['redirect_to'] ) && esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ) ) { // Input var okay.
$redirect = esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ); // Input var okay.
return $redirect;
}
return $redirect;
}
}

567
lib/settings-validator.php Normal file
View file

@ -0,0 +1,567 @@
<?php
/**
* Validation methods for the settings page.
*
* @link https://github.com/discourse/wp-discourse/blob/master/lib/settings-validator.php
* @package WPDiscourse
*/
namespace WPDiscourse\Validator;
/**
* Class SettingsValidator
*
* @package WPDiscourse\Validator
*/
class SettingsValidator {
/**
* Indicates whether or not SSO is enabled.
*
* @access protected
* @var bool
*/
protected $sso_enabled = false;
/**
* Indicates whether or not 'use_discourse_comments' is enabled.
*
* @access protected
* @var bool
*/
protected $use_discourse_comments = false;
/**
* SettingsValidator constructor.
*
* Adds the callback function for each of the validator filters that are applied
* in `admin.php`.
*/
public function __construct() {
add_filter( 'validate_url', array( $this, 'validate_url' ) );
add_filter( 'validate_api_key', array( $this, 'validate_api_key' ) );
add_filter( 'validate_publish_username', array(
$this,
'validate_publish_username',
) );
add_filter( 'validate_publish_category', array(
$this,
'validate_publish_category',
) );
add_filter( 'validate_publish_category_update', array(
$this,
'validate_publish_category_update',
) );
add_filter( 'validate_full_post_content', array(
$this,
'validate_full_post_content',
) );
add_filter( 'validate_auto_publish', array(
$this,
'validate_auto_publish',
) );
add_filter( 'validate_auto_track', array( $this, 'validate_auto_track' ) );
add_filter( 'validate_allowed_post_types', array(
$this,
'validate_allowed_post_types',
) );
add_filter( 'validate_use_discourse_comments', array(
$this,
'validate_use_discourse_comments',
) );
add_filter( 'validate_show_existing_comments', array(
$this,
'validate_show_existing_comments',
) );
add_filter( 'validate_existing_comments_heading', array(
$this,
'validate_existing_comments_heading',
) );
add_filter( 'validate_max_comments', array(
$this,
'validate_max_comments',
) );
add_filter( 'validate_min_replies', array(
$this,
'validate_min_replies',
) );
add_filter( 'validate_min_score', array( $this, 'validate_min_score' ) );
add_filter( 'validate_min_trust_level', array(
$this,
'validate_min_trust_level',
) );
add_filter( 'validate_bypass_trust_level_score', array(
$this,
'validate_bypass_trust_level_score',
) );
add_filter( 'validate_custom_excerpt_length', array(
$this,
'validate_custom_excerpt_length',
) );
add_filter( 'validate_custom_datetime_format', array(
$this,
'validate_custom_datetime_format',
) );
add_filter( 'validate_only_show_moderator_liked', array(
$this,
'validate_only_show_moderator_liked',
) );
add_filter( 'validate_display_subcategories', array(
$this,
'validate_display_subcategories',
) );
add_filter( 'validate_debug_mode', array( $this, 'validate_debug_mode' ) );
add_filter( 'validate_enable_sso', array( $this, 'validate_enable_sso' ) );
add_filter( 'validate_sso_secret', array( $this, 'validate_sso_secret' ) );
add_filter( 'validate_login_path', array( $this, 'validate_login_path' ) );
}
/**
* Validates the Discourse URL.
*
* @param string $input The input to be validated.
*
* @return string
*/
public function validate_url( $input ) {
$regex = '/^(http:|https:)/';
// Make sure the url starts with a valid protocol.
if ( ! preg_match( $regex, $input ) ) {
add_settings_error( 'discourse', 'discourse_url', __( 'The Discourse URL needs to begin with either \'http:\' or \'https:\'.' ) );
return '';
}
if ( filter_var( $input, FILTER_VALIDATE_URL ) ) {
return untrailingslashit( esc_url_raw( $input ) );
} else {
add_settings_error( 'discourse', 'discourse_url', __( 'The Discourse URL you provided is not a valid URL.', 'wp-discourse' ) );
return untrailingslashit( esc_url_raw( $input ) );
}
}
/**
* Validates the api key.
*
* @param string $input The input to be validated.
*
* @return string
*/
public function validate_api_key( $input ) {
$regex = '/^\s*([0-9]*[a-z]*|[a-z]*[0-9]*)*\s*$/';
if ( empty( $input ) ) {
add_settings_error( 'discourse', 'api_key', __( 'You must provide an API key.', 'wp-discourse' ) );
return '';
} elseif ( preg_match( $regex, $input ) ) {
return trim( $input );
} else {
add_settings_error( 'discourse', 'api_key', __( 'The API key you provided is not valid.', 'wp-discourse' ) );
return $this->sanitize_text( $input );
}
}
/**
* Validates the publish_username.
*
* @param string $input The input to be validated.
*
* @return string
*/
public function validate_publish_username( $input ) {
if ( ! empty( $input ) ) {
return $this->sanitize_text( $input );
} else {
add_settings_error( 'discourse', 'publish_username', __( 'You must provide a Discourse username with which to publish the posts', 'wp-discourse' ) );
return '';
}
}
/**
* Validated the 'display_subcategories' checkbox.
*
* @param int $input The input to be validated.
*
* @return int
*/
public function validate_display_subcategories( $input ) {
return $this->sanitize_checkbox( $input );
}
/**
* Validates the 'publish_category' select input.
*
* Returns the category id.
*
* @param string $input The input to be validated.
*
* @return string
*/
public function validate_publish_category( $input ) {
return $this->sanitize_int( $input );
}
/**
* Validates the 'publish_category_update' checkbox.
*
* @param string $input The input to be validated.
*
* @return int
*/
public function validate_publish_category_update( $input ) {
return $this->sanitize_checkbox( $input );
}
/**
* Validates the 'full_post_content' checkbox.
*
* @param string $input The input to be validated.
*
* @return int
*/
public function validate_full_post_content( $input ) {
return $this->sanitize_checkbox( $input );
}
/**
* Validates the 'auto_publish' checkbox.
*
* @param string $input The input to be validated.
*
* @return int
*/
public function validate_auto_publish( $input ) {
return $this->sanitize_checkbox( $input );
}
/**
* Validates the 'auto_track' checkbox.
*
* @param string $input The input to be validates.
*
* @return int
*/
public function validate_auto_track( $input ) {
return $this->sanitize_checkbox( $input );
}
/**
* Validates the 'allowed_post_types' multi-select.
*
* @param array $input The array of allowed post-types.
*
* @return array
*/
public function validate_allowed_post_types( $input ) {
$output = array();
foreach ( $input as $post_type ) {
$output[] = sanitize_text_field( $post_type );
}
return $output;
}
/**
* Validates the 'use_discourse_comments' checkbox.
*
* If this function is called, it sets the 'use_discourse_comments' property to true. This makes it possible
* to only show warnings for the comment settings if Discourse is being used for comments.
*
* @param string $input The input to be validated.
*
* @return int
*/
public function validate_use_discourse_comments( $input ) {
$this->use_discourse_comments = true;
return $this->sanitize_checkbox( $input );
}
/**
* Validates the 'show_existing_comments' checkbox.
*
* @param string $input The input to be validated.
*
* @return int
*/
public function validate_show_existing_comments( $input ) {
return $this->sanitize_checkbox( $input );
}
/**
* Validates the 'existing_comments_heading' input.
*
* @param string $input The input to be validated.
*
* @return string
*/
public function validate_existing_comments_heading( $input ) {
return $this->sanitize_html( $input );
}
/**
* Validates the 'max_comments' number input.
*
* @param int $input The input to be validated.
*
* @return mixed
*/
public function validate_max_comments( $input ) {
return $this->validate_int( $input, 'max_comments', 1, null,
__( 'The max visible comments setting requires a positive integer.', 'wp-discourse' ),
$this->use_discourse_comments );
}
/**
* Validates the 'min_replies' number input.
*
* @param int $input The input to be validated.
*
* @return mixed
*/
public function validate_min_replies( $input ) {
return $this->validate_int( $input, 'min_replies', 0, null,
__( 'The min number of replies setting requires a number greater than or equal to 0.', 'wp-discourse' ),
$this->use_discourse_comments );
}
/**
* Validates the 'min_score' number input.
*
* @param int $input The input to be validated.
*
* @return mixed
*/
public function validate_min_score( $input ) {
return $this->validate_int( $input, 'min_score', 0, null,
__( 'The min score of posts setting requires a number greater than or equal to 0.', 'wp-discourse' ),
$this->use_discourse_comments );
}
/**
* Validates the 'min_trust_level' number input.
*
* @param int $input The input to be validated.
*
* @return mixed
*/
public function validate_min_trust_level( $input ) {
return $this->validate_int( $input, 'min_trust_level', 0, 5,
__( 'The trust level setting requires a number between 0 and 5.', 'wp-discourse' ),
$this->use_discourse_comments );
}
/**
* Validates the 'bypass_trust_level_score' number input.
*
* @param int $input The input to be validated.
*
* @return mixed
*/
public function validate_bypass_trust_level_score( $input ) {
return $this->validate_int( $input, 'bypass_trust_level', 0, null,
__( 'The bypass trust level score setting requires an integer greater than or equal to 0.', 'wp-discourse' ),
$this->use_discourse_comments );
}
/**
* Validates the 'custom_excerpt_length' number input.
*
* @param int $input The input to be validated.
*
* @return mixed
*/
public function validate_custom_excerpt_length( $input ) {
return $this->validate_int( $input, 'excerpt_length', 1, null,
__( 'The custom excerpt length setting requires a positive integer.', 'wp-discourse' ),
$this->use_discourse_comments );
}
/**
* Validates the 'custom_date_time' text input.
*
* @param string $input The input to be validated.
*
* @return string
*/
public function validate_custom_datetime_format( $input ) {
return sanitize_text_field( $input );
}
/**
* Validates the 'only_show_moderator_liked' checkbox.
*
* @param string $input The input to be validated.
*
* @return int
*/
public function validate_only_show_moderator_liked( $input ) {
return $this->sanitize_checkbox( $input );
}
/**
* Validates the 'debug_mode' checkbox.
*
* @param string $input The input to be validated.
*
* @return int
*/
public function validate_debug_mode( $input ) {
return $this->sanitize_checkbox( $input );
}
/**
* Validated the 'enable_sso'checkbox.
*
* This function is only called if the checkbox is checked. It sets the `sso_enabled` property to true.
* This allows sso validation notices to only be displayed if sso is enabled.
*
* @param string $input The input to be validated.
*
* @return int
*/
public function validate_enable_sso( $input ) {
$this->sso_enabled = true;
return $this->sanitize_checkbox( $input );
}
/**
* Validates the 'sso_secret' text input.
*
* @param string $input The input to be validated.
*
* @return string
*/
public function validate_sso_secret( $input ) {
if ( strlen( sanitize_text_field( $input ) ) >= 10 ) {
return sanitize_text_field( $input );
// Only add a settings error if sso is enabled, otherwise just sanitize the input.
} elseif ( $this->sso_enabled ) {
add_settings_error( 'discourse', 'sso_secret', __( 'The SSO secret key setting must be at least 10 characters long.', 'wp-discourse' ) );
return sanitize_text_field( $input );
} else {
return sanitize_text_field( $input );
}
}
/**
* Validates the 'login_path' text input.
*
* @param string $input The input to be validated.
*
* @return string
*/
public function validate_login_path( $input ) {
if ( $this->sso_enabled && $input ) {
$regex = '/^\/([a-z0-9\-]+)*(\/[a-z0-9\-]+)*(\/)?$/';
if ( ! preg_match( $regex, $input ) ) {
add_settings_error( 'discourse', 'login_path', __( 'The path to login page setting needs to be a valid file path, starting with \'/\'.', 'wp-discourse' ) );
return $this->sanitize_text( $input );
}
// It's valid.
return $this->sanitize_text( $input );
}
// Sanitize, but don't validate. SSO is not enabled.
return $this->sanitize_text( $input );
}
/**
* Helper methods
******************************/
/**
* A helper method to sanitize text inputs.
*
* @param string $input The input to be sanitized.
*
* @return string
*/
protected function sanitize_text( $input ) {
return sanitize_text_field( $input );
}
/**
* A helper method to sanitize the value returned from checkbox inputs.
*
* @param string $input The value returned from the checkbox.
*
* @return int
*/
protected function sanitize_checkbox( $input ) {
return 1 === intval( $input ) ? 1 : 0;
}
/**
* A helper function to sanitize HTML.
*
* @param string $input HTML input to be sanitized.
*
* @return string
*/
protected function sanitize_html( $input ) {
return wp_kses_post( $input );
}
/**
* A helper function to sanitize an int.
*
* @param mixed|int $input The input to be validated.
*
* @return int
*/
protected function sanitize_int( $input ) {
return intval( $input );
}
/**
* A helper function to validate and sanitize integers.
*
* @param int $input The input to be validated.
* @param string $option_id The option being validated.
* @param null $min The minimum allowed value.
* @param null $max The maximum allowed value.
* @param string $error_message The error message to return.
* @param bool $add_error Whether or not to add a setting error.
*
* @return mixed
*/
protected function validate_int( $input, $option_id, $min = null, $max = null, $error_message = '', $add_error = false ) {
$options = array();
if ( isset( $min ) ) {
$options['min_range'] = $min;
}
if ( isset( $max ) ) {
$options['max_range'] = $max;
}
if ( filter_var( $input, FILTER_VALIDATE_INT, array( 'options' => $options ) ) === false ) {
if ( $add_error ) {
add_settings_error( 'discourse', $option_id, $error_message );
return filter_var( $input, FILTER_SANITIZE_NUMBER_INT );
}
// The input is not valid, but the setting's section is not being used, sanitize the input and return it.
return filter_var( $input, FILTER_SANITIZE_NUMBER_INT );
} else {
// Valid input.
return filter_var( $input, FILTER_SANITIZE_NUMBER_INT );
}
}
}

92
lib/sso.php Normal file
View file

@ -0,0 +1,92 @@
<?php
/**
* Single-sign-on for Discourse via PHP
*
* @link https://github.com/ArmedGuy/discourse_sso_php
* @package WPDiscourse
*/
namespace WPDiscourse\SSO;
/**
* Class Discourse_SSO
*/
class Discourse_SSO {
/**
* The SSO secret key.
*
* @access private
* @var string
*/
private $sso_secret;
/**
* Discourse_SSO constructor.
*
* @param string $secret The SSO secret key.
*/
function __construct( $secret ) {
$this->sso_secret = $secret;
}
/**
* Validates the payload against the sig.
*
* @param string $payload A Base64 encoded string.
* @param string $sig HMAC-SHA256 of $sso_secret, $payload should be equal to $sig.
*
* @return bool
*/
public function validate( $payload, $sig ) {
$payload = urldecode( $payload );
if ( hash_hmac( 'sha256', $payload, $this->sso_secret ) === $sig ) {
return true;
} else {
return false;
}
}
/**
* Gets the nonce from the payload.
*
* @param string $payload A Base64 encoded string.
*
* @return mixed
* @throws Exception Thrown when the nonce in not found in the payload.
*/
public function get_nonce( $payload ) {
$payload = urldecode( $payload );
$query = array();
parse_str( base64_decode( $payload ), $query );
if ( isset( $query['nonce'] ) ) {
return $query['nonce'];
} else {
throw new Exception( 'Nonce not found in payload!' );
}
}
/**
* Creates the sso-login query params that are sent to Discourse.
*
* @param array $params The array of parameters to send.
*
* @return string
* @throws Exception Thrown when the required params aren't present.
*/
public function build_login_string( $params ) {
if ( ! isset( $params['external_id'] ) ) {
throw new Exception( "Missing required parameter 'external_id'" );
}
if ( ! isset( $params['nonce'] ) ) {
throw new Exception( "Missing required parameter 'nonce'" );
}
if ( ! isset( $params['email'] ) ) {
throw new Exception( "Missing required parameter 'email'" );
}
$payload = base64_encode( http_build_query( $params ) );
$sig = hash_hmac( 'sha256', $payload, $this->sso_secret );
return http_build_query( array( 'sso' => $payload, 'sig' => $sig ) );
}
}

144
lib/utilities.php Normal file
View file

@ -0,0 +1,144 @@
<?php
/**
* Static utility functions used throughout the plugin.
*
* @package WPDiscourse
*/
namespace WPDiscourse\Utilities;
/**
* Class Utilities
*
* @package WPDiscourse
*/
class Utilities {
/**
* Checks the connection status to Discourse.
*
* @return int
*/
public static function check_connection_status() {
$options = get_option( 'discourse' );
$url = array_key_exists( 'url', $options ) ? $options['url'] : '';
$url = add_query_arg( array(
'api_key' => array_key_exists( 'api-key', $options ) ? $options['api-key'] : '',
'api_username' => array_key_exists( 'publish-username', $options ) ? $options['publish-username'] : '',
), $url . '/users/' . $options['publish-username'] . '.json' );
$url = esc_url_raw( $url );
$response = wp_remote_get( $url );
return self::validate( $response );
}
/**
* Validates the response from `wp_remote_get` or `wp_remote_post`.
*
* @param array $response The response from `wp_remote_get` or `wp_remote_post`.
*
* @return int
*/
public static function validate( $response ) {
// There will be a WP_Error if the server can't be accessed.
if ( is_wp_error( $response ) ) {
error_log( $response->get_error_message() );
return 0;
// There is a response from the server, but it's not what we're looking for.
} elseif ( intval( wp_remote_retrieve_response_code( $response ) ) !== 200 ) {
$error_message = wp_remote_retrieve_response_message( $response );
error_log( 'There has been a problem accessing your Discourse forum. Error Message: ' . $error_message );
return 0;
} else {
// Valid response.
return 1;
}
}
/**
* Returns the user's Discourse homepage.
*
* @param string $url The base URL of the Discourse forum.
* @param object $post The Post object.
*
* @return string
*/
public static function homepage( $url, $post ) {
return $url . '/users/' . strtolower( $post->username );
}
/**
* Substitutes the value for `$size` into the template.
*
* @param string $template The avatar template.
* @param int $size The size of the avarar.
*
* @return mixed
*/
public static function avatar( $template, $size ) {
return str_replace( '{size}', $size, $template );
}
/**
* Replaces relative image src with absolute.
*
* @param string $url The base url of the forum.
* @param string $content The content to be checked.
*
* @return mixed
*/
public static function convert_relative_img_src_to_absolute( $url, $content ) {
if ( preg_match( "/<img\s*src\s*=\s*[\'\"]?(https?:)?\/\//i", $content ) ) {
return $content;
}
$search = '#<img src="((?!\s*[\'"]?(?:https?:)?\/\/)\s*([\'"]))?#';
$replace = "<img src=\"{$url}$1";
return preg_replace( $search, $replace, $content );
}
/**
* Gets the Discourse categories.
*
* @return array|mixed|object|\WP_Error|WP_Error
*/
public static function get_discourse_categories() {
$options = get_option( 'discourse' );
$url = add_query_arg( array(
'api_key' => $options['api-key'],
'api_username' => $options['publish-username'],
), $options['url'] . '/site.json' );
$force_update = isset( $options['publish-category-update'] ) ? $options['publish-category-update'] : '0';
$remote = get_transient( 'discourse_settings_categories_cache' );
$cache = $remote;
if ( empty( $remote ) || $force_update ) {
$remote = wp_remote_get( $url );
if ( ! self::validate( $remote ) ) {
if ( ! empty( $cache ) ) {
return $cache;
}
return new \WP_Error( 'connection_not_established', 'There was an error establishing a connection with Discourse' );
}
$remote = json_decode( wp_remote_retrieve_body( $remote ), true );
if ( array_key_exists( 'categories', $remote ) ) {
$remote = $remote['categories'];
if ( ! isset( $options['display-subcategories'] ) || 0 === intval( $options['display-subcategories'] ) ) {
foreach ( $remote as $category => $values ) {
if ( array_key_exists( 'parent_category_id', $values ) ) {
unset( $remote[ $category ] );
}
}
}
set_transient( 'discourse_settings_categories_cache', $remote, HOUR_IN_SECONDS );
} else {
return new \WP_Error( 'key_not_found', 'The categories key was not found in the response from Discourse.' );
}
}
return $remote;
}
}

14
phpunit.xml.dist Normal file
View file

@ -0,0 +1,14 @@
<phpunit
bootstrap="tests/bootstrap.php"
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
>
<testsuites>
<testsuite>
<directory prefix="test-" suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
</phpunit>

66
readme.txt Normal file
View file

@ -0,0 +1,66 @@
=== WP Discourse ===
Contributors: cdck, retiehs, samsaffron, scossar, techapj
Tags: discourse, forum, comments, sso
Requires at least: 4.4
Tested up to: 4.5.2
Stable tag: trunk
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
WP Discourse allows you to use Discourse as a community engine for your WordPress website.
== Description ==
The WP Discourse plugin acts as an interface between your WordPress site and your
[Discourse](http://www.discourse.org/) forum.
It allows you to:
- Publish WordPress posts to Discourse
- Use Discourse to generate comments and discussion for your WordPress posts
- Select which comments are to be displayed on the WordPress site based on post score and commenter trust level
- Use your WordPress site as the Single Sign On provider for your Discourse forum
== Installation ==
#### From your WordPress dashboard
1. Visit 'Plugins > Add New'
2. Search for 'WP Discourse'
3. Activate WP Discourse from your Plugins page
#### From wordpress.org
1. Download WP Discourse
2. Upload the 'wp-discourse' directory to your '/wp-content/plugins/' directory
3. Activate WP Discourse from your Plugins page
== Frequently Asked Questions ==
= Does this plugin install Discourse for me? =
No this plugin acts as an interface between Discourse and WordPress. For it to work you will need to first set up
Discourse forum. You can install Discourse for yourself following either of these guides:
- [Install Discourse in Under 30 Minutes](https://github.com/discourse/discourse/blob/master/docs/INSTALL-cloud.md)
- [How to use the Discourse One-Click Application on DigitalOcean](https://www.digitalocean.com/community/tutorials/how-to-use-the-discourse-one-click-application-on-digitalocean)
= Is it possible to customize the comment templates?
Yes, the html templates used for publishing posts on Discourse and for displaying comments on WordPress can be customized in your theme.
This is done by hooking into the filters that are applied to each template.
For more details on template customization, take a look at this section of our wiki: [Template Customization](https://github.com/discourse/wp-discourse/wiki/Template-Customization)
== Screenshots ==
1. Select whether a post is to be published to Discourse, and what category it is to be published into.
2. A WordPress posts with no comments.
3. Adding a comment on the Discourse forum.
4. The comment appears on WordPress.
== Changelog ==

103
templates/comments.php Executable file
View file

@ -0,0 +1,103 @@
<?php
/**
* The template for comments.
*
* @package WPDiscourse
*/
use WPDiscourse\Templates\HTMLTemplates as Templates;
use WPDiscourse\Utilities\Utilities as DiscourseUtilities;
$custom = get_post_custom();
// If, when a new post is published to Discourse, there is not a valid response from
// the forum, the `discourse_permalink` key will not be set. Display the `bad_response_html` template.
if ( ! array_key_exists( 'discourse_permalink', $custom ) ) {
echo wp_kses_post( Templates::bad_response_html() );
} else {
$options = get_option( 'discourse' );
$is_enable_sso = ( isset( $options['enable-sso'] ) && 1 === intval( $options['enable-sso'] ) );
$permalink = (string) $custom['discourse_permalink'][0];
if ( $is_enable_sso ) {
$permalink = esc_url( $options['url'] ) . '/session/sso?return_path=' . $permalink;
}
$discourse_url_name = preg_replace( '(https?://)', '', esc_url( $options['url'] ) );
if ( isset( $custom['discourse_comments_raw'] ) ) {
$discourse_info = json_decode( $custom['discourse_comments_raw'][0] );
} else {
$discourse_info = array();
}
$defaults = array(
'posts_count' => 0,
'posts' => array(),
'participants' => array(),
);
// Add <time> tag to WP allowed html tags.
global $allowedposttags;
$allowedposttags['time'] = array( 'datetime' => array() );
// Use custom datetime format string if provided, else global date format.
$datetime_format = '' === $options['custom-datetime-format'] ? get_option( 'date_format' ) : $options['custom-datetime-format'];
// Add some protection in the event our metadata doesn't look how we expect it to.
$discourse_info = (object) wp_parse_args( (array) $discourse_info, $defaults );
$more_replies = intval( ( $discourse_info->posts_count - count( $discourse_info->posts ) - 1 ) );
$more = ( 0 === count( $discourse_info->posts ) ) ? '' : 'more ';
if ( 0 === $more_replies ) {
$more_replies = '';
} elseif ( 1 === $more_replies ) {
$more_replies = '1 ' . $more . 'reply';
} else {
$more_replies = $more_replies . ' ' . $more . 'replies';
}
$discourse_url = esc_url( $options['url'] );
$discourse_html = '';
$comments_html = '';
$participants_html = '';
if ( count( $discourse_info->posts ) > 0 ) {
foreach ( $discourse_info->posts as &$post ) {
$comment_html = wp_kses_post( Templates::comment_html() );
$comment_html = str_replace( '{discourse_url}', $discourse_url, $comment_html );
$comment_html = str_replace( '{discourse_url_name}', $discourse_url_name, $comment_html );
$comment_html = str_replace( '{topic_url}', $permalink, $comment_html );
$avatar_url = DiscourseUtilities::avatar( $post->avatar_template, 64 );
$comment_html = str_replace( '{avatar_url}', esc_url( $avatar_url ), $comment_html );
$user_url = DiscourseUtilities::homepage( $options['url'], $post );
$comment_html = str_replace( '{user_url}', esc_url( $user_url ), $comment_html );
$comment_html = str_replace( '{username}', esc_html( $post->username ), $comment_html );
$comment_html = str_replace( '{fullname}', esc_html( $post->name ), $comment_html );
$comment_body = DiscourseUtilities::convert_relative_img_src_to_absolute( $discourse_url, $post->cooked );
$comment_html = str_replace( '{comment_body}', wp_kses_post( $comment_body ), $comment_html );
$comment_html = str_replace( '{comment_created_at}', mysql2date( $datetime_format, get_date_from_gmt( $post->created_at ) ), $comment_html );
$comments_html .= $comment_html;
}
foreach ( $discourse_info->participants as &$participant ) {
$participant_html = wp_kses_post( Templates::participant_html() );
$participant_html = str_replace( '{discourse_url}', $discourse_url, $participant_html );
$participant_html = str_replace( '{discourse_url_name}', $discourse_url_name, $participant_html );
$participant_html = str_replace( '{topic_url}', $permalink, $participant_html );
$avatar_url = DiscourseUtilities::avatar( $participant->avatar_template, 64 );
$participant_html = str_replace( '{avatar_url}', esc_url( $avatar_url ), $participant_html );
$user_url = DiscourseUtilities::homepage( $options['url'], $participant );
$participant_html = str_replace( '{user_url}', esc_url( $user_url ), $participant_html );
$participant_html = str_replace( '{username}', esc_html( $participant->username ), $participant_html );
$participants_html .= $participant_html;
}
$discourse_html = wp_kses_post( Templates::replies_html() );
$discourse_html = str_replace( '{more_replies}', $more_replies, $discourse_html );
} else {
$discourse_html = wp_kses_post( Templates::no_replies_html() );
}
$discourse_html = str_replace( '{discourse_url}', $discourse_url, $discourse_html );
$discourse_html = str_replace( '{discourse_url_name}', $discourse_url_name, $discourse_html );
$discourse_html = str_replace( '{topic_url}', $permalink, $discourse_html );
$discourse_html = str_replace( '{comments}', $comments_html, $discourse_html );
$discourse_html = str_replace( '{participants}', $participants_html, $discourse_html );
echo wp_kses_post( $discourse_html );
}

27
tests/bootstrap.php Normal file
View file

@ -0,0 +1,27 @@
<?php
/**
* PHPUnit bootstrap file
*
* @package wp-discourse
*/
require_once( dirname( __DIR__) . '/vendor/autoload.php' );
$_tests_dir = getenv( 'WP_TESTS_DIR' );
if ( ! $_tests_dir ) {
$_tests_dir = '/tmp/wordpress-tests-lib';
}
// Give access to tests_add_filter() function.
require_once $_tests_dir . '/includes/functions.php';
/**
* Manually load the plugin being tested.
*/
function _manually_load_plugin() {
require dirname( dirname( __FILE__ ) ) . '/wp-discourse.php';
}
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );
// Start up the WP testing environment.
require $_tests_dir . '/includes/bootstrap.php';

18
tests/lib/test-admin.php Normal file
View file

@ -0,0 +1,18 @@
<?php
require_once( __DIR__ . '/../../lib/admin.php' );
class TestAdmin extends WP_UnitTestCase {
protected $admin;
public function setUp() {
$this->admin = new \WPDiscourse\DiscourseAdmin\DiscourseAdmin();
}
public function test_constructor_hooks_into_correct_filters_and_actions() {
$this->assertEquals( 10, has_action( 'admin_init', array( $this->admin, 'admin_init' ) ) );
$this->assertEquals( 10, has_action( 'admin_enqueue_scripts', array( $this->admin, 'admin_styles' ) ) );
$this->assertEquals( 10, has_action( 'admin_menu', array( $this->admin, 'discourse_admin_menu' ) ) );
$this->assertEquals( 10, has_action( 'load-settings_page_discourse', array( $this->admin, 'connection_status_notice' ) ) );
}
}

View file

@ -0,0 +1,168 @@
<?php
require_once( __DIR__ . '/../../lib/discourse-comment.php' );
class TestDiscourseComment extends WP_UnitTestCase {
public $comment;
public function setUp() {
$this->comment = new \WPDiscourse\DiscourseComment\DiscourseComment();
$options = array(
'url' => 'http://forum.example.com',
'use-discourse-comments' => 1,
'show-existing-comments' => 0,
);
update_option( 'discourse', $options );
}
public function test_constructor_hooks_into_required_filters_and_actions() {
$this->assertEquals( 10, has_filter( 'comments_number', array( $this->comment, 'comments_number' ) ) );
$this->assertEquals( 20, has_filter( 'comments_template', array( $this->comment, 'comments_template' ) ) );
$this->assertEquals( 10, has_action( 'wp_enqueue_scripts', array(
$this->comment,
'discourse_comments_js'
) ) );
}
public function test_comments_template_syncs_comments_if_post_is_published_to_discourse() {
global $post;
$post_id = $this->factory->post->create( array(
'post_title' => 'This is a test',
) );
$post = get_post( $post_id );
update_post_meta( $post_id, 'publish_to_discourse', 1 );
$old = 'http://example.com/wordpress/wp-content/themes/twentysixteen/comments.php';
$comment_mock = $this->getMock( '\WPDiscourse\DiscourseComment\DiscourseComment', array( 'sync_comments' ) );
$comment_mock->expects( $this->once() )
->method( 'sync_comments' )
->with( $post_id );
$comment_mock->comments_template( $old );
}
public function test_comments_template_does_not_syncs_comments_if_post_is_not_published_to_discourse() {
global $post;
$post_id = $this->factory->post->create( array(
'post_title' => 'This is a test',
) );
$post = get_post( $post_id );
$old = 'http://example.com/wordpress/wp-content/themes/twentysixteen/comments.php';
$comment_mock = $this->getMock( '\WPDiscourse\DiscourseComment\DiscourseComment', array( 'sync_comments' ) );
$comment_mock->expects( $this->never() )
->method( 'sync_comments' );
$comment_mock->comments_template( $old );
}
public function test_comments_template_returns_discourse_template_when_post_published_to_discourse_and_there_are_no_wp_comments() {
global $post;
$post_id = $this->factory->post->create( array(
'post_title' => 'This is a test',
) );
$post = get_post( $post_id );
update_post_meta( $post_id, 'publish_to_discourse', 1 );
$old = 'http://example.com/wordpress/wp-content/themes/twentysixteen/comments.php';
$comment_mock = $this->getMock( '\WPDiscourse\DiscourseComment\DiscourseComment', array(
'sync_comments',
) );
$template = $comment_mock->comments_template( $old );
$regex = '/wp-discourse\/templates\/comments.php$/';
$this->assertEquals( 1, preg_match( $regex, $template ) );
}
public function test_comments_template_returns_wp_template_when_post_not_published_to_discourse() {
global $post;
$post_id = $this->factory->post->create( array(
'post_title' => 'This is a test',
) );
$post = get_post( $post_id );
$old = 'http://example.com/wordpress/wp-content/themes/twentysixteen/comments.php';
$comment_mock = $this->getMock( '\WPDiscourse\DiscourseComment\DiscourseComment', array(
'sync_comments',
) );
$template = $comment_mock->comments_template( $old );
$this->assertEquals( $old, $template );
}
public function test_comments_template_returns_wp_template_when_show_existing_comments_is_true_and_there_are_comments() {
global $post;
$options = array(
'use-discourse-comments' => 1,
'show-existing-comments' => 1,
'existing-comments-heading' => 'Old comments',
);
update_option( 'discourse', $options );
$post_id = $this->factory->post->create( array(
'post_title' => 'This is a test',
) );
$post = get_post( $post_id );
update_post_meta( $post_id, 'publish_to_discourse', 1 );
$comment_id = $this->factory->comment->create( array(
'comment_post_ID' => $post_id,
) );
$old = 'http://example.com/wordpress/wp-content/themes/twentysixteen/comments.php';
$comment_mock = $this->getMock( '\WPDiscourse\DiscourseComment\DiscourseComment', array(
'sync_comments',
) );
$template = $comment_mock->comments_template( $old );
$this->markTestIncomplete(
'There needs to be a way to update the comment count property of the post for this test to work.'
);
}
public function test_comments_number_returns_wp_comments_number_if_dicourse_comments_are_not_being_used() {
global $post;
$post_id = $this->factory->post->create( array(
'post_title' => 'This is a test',
) );
$post = get_post( $post_id );
$this->assertEquals( '3 Replies', $this->comment->comments_number( '3 Replies' ) );
}
public function test_comments_number_returns_discourse_comments_number_when_discourse_comments_are_being_used() {
global $post;
$post_id = $this->factory->post->create( array(
'post_tite' => 'This is a test',
) );
$post = get_post( $post_id );
update_post_meta( $post_id, 'publish_to_discourse', 1 );
update_post_meta( $post_id, 'discourse_comments_count', 5 );
$comment_mock = $this->getMock( '\WPDiscourse\DiscourseComment\DiscourseComment', array(
'sync_comments',
) );
$this->assertEquals( '5 Replies', $comment_mock->comments_number( '3 Replies' ) );
}
public function test_sync_comments_updates_post_metadata() {
$this->markTestIncomplete(
'It would be nice to be able to test this.'
);
}
}

View file

@ -0,0 +1,221 @@
<?php
require_once( __DIR__ . '/../../lib/discourse-publish.php' );
class TestDiscoursePublish extends WP_UnitTestCase {
protected $publisher;
public function setUp() {
$this->publisher = new \WPDiscourse\DiscoursePublish\DiscoursePublish();
$options = array(
'publish-category' => '',
'publish-username' => 'system',
'allowed_post_types' => array( 'post' ),
);
update_option( 'discourse', $options );
}
public function test_constructor_hooks_into_correct_actions() {
$this->assertEquals( 13, has_action( 'save_post', array( $this->publisher, 'publish_post_after_save' ) ) );
$this->assertEquals( 10, has_action( 'transition_post_status', array(
$this->publisher,
'publish_post_after_transition'
) ) );
$this->assertEquals( 10, has_action( 'xmlrpc_publish_post', array(
$this->publisher,
'xmlrpc_publish_post_to_discourse'
) ) );
}
public function test_publish_post_after_transition_calls_sync_to_discourse_when_valid_post_type_and_publish_to_discourse_is_set() {
$post_id = $this->factory->post->create( array(
'post_title' => 'This is a test',
) );
$post = get_post( $post_id );
update_post_meta( $post_id, 'publish_to_discourse', 1 );
$publish_mock = $this->getMock( '\WPDiscourse\DiscoursePublish\DiscoursePublish',
array( 'sync_to_discourse' ) );
$publish_mock->expects( $this->once() )
->method( 'sync_to_discourse' )
->with(
$post_id,
$post->post_title,
$post->post_content
);
$publish_mock->publish_post_after_transition( 'publish', 'pending', $post );
}
public function test_publish_post_after_transition_does_not_call_sync_to_discourse_when_transitioning_to_private() {
$post_id = $this->factory->post->create( array(
'post_title' => 'This is a test',
) );
$post = get_post( $post_id );
update_post_meta( $post_id, 'publish_to_discourse', 1 );
$publish_mock = $this->getMock( '\WPDiscourse\DiscoursePublish\DiscoursePublish',
array( 'sync_to_discourse' ) );
$publish_mock->expects( $this->never() )
->method( 'sync_to_discourse' );
$publish_mock->publish_post_after_transition( 'private', 'publish', $post );
}
public function test_publish_post_after_transition_does_not_call_sync_to_discourse_when_publish_to_discourse_not_set() {
$post_id = $this->factory->post->create( array(
'post_title' => 'This is a test',
) );
$post = get_post( $post_id );
$publish_mock = $this->getMock( '\WPDiscourse\DiscoursePublish\DiscoursePublish',
array( 'sync_to_discourse' ) );
$publish_mock->expects( $this->never() )
->method( 'sync_to_discourse' );
$publish_mock->publish_post_after_transition( 'publish', 'publish', $post );
}
public function test_publish_post_after_transition_does_not_call_sync_to_discourse_when_not_valid_post_type() {
$post_id = $this->factory->post->create( array(
'post_title' => 'This is a test',
'post_type' => 'page',
) );
$post = get_post( $post_id );
update_post_meta( $post_id, 'publish_to_discourse', 1 );
$publish_mock = $this->getMock( '\WPDiscourse\DiscoursePublish\DiscoursePublish',
array( 'sync_to_discourse' ) );
$publish_mock->expects( $this->never() )
->method( 'sync_to_discourse' );
$publish_mock->publish_post_after_transition( 'publish', 'publish', $post );
}
public function test_xmlrpc_publish_post_to_discourse_calls_sync_to_discourse_when_valid_post_type_and_publish_to_discourse_is_set() {
$post_id = $this->factory->post->create( array(
'post_title' => 'This is a test',
'post_type' => 'page',
) );
$post = get_post( $post_id );
$options = array(
'publish-category' => 'uncategorized',
'publish-username' => 'system',
'allowed_post_types' => array( 'page' ),
);
update_option( 'discourse', $options );
$publish_mock = $this->getMock( '\WPDiscourse\DiscoursePublish\DiscoursePublish',
array( 'sync_to_discourse' ) );
$publish_mock->expects( $this->once() )
->method( 'sync_to_discourse' )
->with( $post_id, $post->post_title, $post->post_content );
$publish_mock->xmlrpc_publish_post_to_discourse( $post_id );
}
public function test_xmlrpc_publish_post_to_discourse_sets_publish_to_discourse_metadata() {
$post_id = $this->factory->post->create( array(
'post_title' => 'This is a test',
) );
update_post_meta( $post_id, 'publish_to_discourse', 0 );
$publish_mock = $this->getMock( '\WPDiscourse\DiscoursePublish\DiscoursePublish',
array( 'sync_to_discourse' ) );
$publish_mock->xmlrpc_publish_post_to_discourse( $post_id );
$this->assertEquals( 1, get_post_meta( $post_id, 'publish_to_discourse', true ) );
}
public function test_xmlrpc_publish_post_to_discourse_does_not_call_sync_to_discourse_for_wrong_post_type() {
$post_id = $this->factory->post->create( array(
'post_title' => 'This is a test',
'post_type' => 'page',
) );
$post = get_post( $post_id );
$options = array(
'publish-category' => 'uncategorized',
'publish-username' => 'system',
'allowed_post_types' => array( 'post' ),
);
update_option( 'discourse', $options );
$publish_mock = $this->getMock( '\WPDiscourse\DiscoursePublish\DiscoursePublish',
array( 'sync_to_discourse' ) );
$publish_mock->expects( $this->never() )
->method( 'sync_to_discourse' );
$publish_mock->xmlrpc_publish_post_to_discourse( $post_id );
}
public function test_publish_post_after_save_publishes_post_when_post_is_set_to_be_published() {
$post_id = $this->factory->post->create( array(
'post_title' => 'This is a test',
) );
$post = get_post( $post_id );
update_post_meta( $post_id,'publish_to_discourse', 1 );
$publish_mock = $this->getMock( '\WPDiscourse\DiscoursePublish\DiscoursePublish',
array( 'sync_to_discourse' ) );
$publish_mock->expects( $this->once() )
->method( 'sync_to_discourse' )
->with( $post_id, $post->post_title, $post->post_content );
$publish_mock->publish_post_after_save( $post_id, $post );
}
public function test_publish_post_after_save_does_not_publish_draft() {
$post_id = $this->factory->post->create( array(
'post_title' => 'This is a test',
) );
$post = get_post( $post_id );
update_post_meta( $post_id,'publish_to_discourse', 1 );
$post_data = array(
'ID' => $post_id,
'post_status' => 'draft',
);
wp_update_post( $post_data );
$publish_mock = $this->getMock( '\WPDiscourse\DiscoursePublish\DiscoursePublish',
array( 'sync_to_discourse' ) );
$publish_mock->expects( $this->never() )
->method( 'sync_to_discourse' );
$publish_mock->publish_post_after_save( $post_id, $post );
}
public function test_publish_post_after_save_does_not_publish_wrong_post_type() {
$post_id = $this->factory->post->create( array(
'post_title' => 'This is a test',
'post_type' => 'page',
) );
$post = get_post( $post_id );
update_post_meta( $post_id,'publish_to_discourse', 1 );
$publish_mock = $this->getMock( '\WPDiscourse\DiscoursePublish\DiscoursePublish',
array( 'sync_to_discourse' ) );
$publish_mock->expects( $this->never() )
->method( 'sync_to_discourse' );
$publish_mock->publish_post_after_save( $post_id, $post );
}
}

View file

@ -0,0 +1,52 @@
<?php
require_once( __DIR__ . '/../../lib/discourse-sso.php' );
class TestDiscourseSSO extends WP_UnitTestCase {
protected $discourse_sso;
public function setUp() {
$this->discourse_sso = new \WPDiscourse\DiscourseSSO\DiscourseSSO();
$options = array(
'enable-sso' => 1,
'url' => 'http://forum.example.com',
);
update_option( 'discourse', $options );
}
public function test_constructor_hooks_into_correct_filter_and_actions() {
$this->assertEquals( 10, has_filter( 'query_vars', array( $this->discourse_sso, 'sso_add_query_vars' ) ) );
$this->assertEquals( 10, has_filter( 'login_url', array( $this->discourse_sso, 'set_login_url' ) ) );
$this->assertEquals( 10, has_action( 'parse_query', array( $this->discourse_sso, 'sso_parse_request' ) ) );
}
public function test_set_login_url_returns_wp_login_url_if_login_path_not_set() {
$options = array(
'login-path' => '',
);
update_option( 'discourse', $options );
$wp_login = 'wp-login.php';
$redirect = '/';
$returned_path = explode( '?', $this->discourse_sso->set_login_url( $wp_login, $redirect ) )[0];
$this->assertEquals( $wp_login, $returned_path );
}
public function test_set_login_url_returns_supplied_login_path() {
$options = array(
'login-path' => '/',
);
update_option( 'discourse', $options );
$wp_login = 'wp-login.php';
$redirect = '/welcome';
$returned_path = explode( '?', $this->discourse_sso->set_login_url( $wp_login, $redirect ) )[0];
$this->assertEquals( get_option( 'discourse' )['login-path'], $returned_path );
}
}

View file

@ -0,0 +1,15 @@
<?php
require_once( __DIR__ . '/../../lib/discourse.php' );
class TestDiscourse extends WP_UnitTestCase {
protected $discourse;
public function setUp() {
$this->discourse = new \WPDiscourse\Discourse\Discourse();
}
public function test_constructor_hooks_into_correct_filters_and_actions() {
$this->assertEquals( 10, has_filter( 'user_contactmethods', array( $this->discourse, 'extend_user_profile' ) ) );
}
}

View file

@ -0,0 +1,67 @@
<?php
require_once( __DIR__ . '/../../lib/meta-box.php' );
use phpmock\phpunit\PHPMock;
class TestMetaBox extends \PHPUnit_Framework_TestCase {
use PHPMock;
protected $metabox;
public function setUp() {
$this->metabox = new \WPDiscourse\MetaBox\MetaBox();
$options = array(
'allowed_post_types' => array( 'post', 'page' ),
);
update_option( 'discourse', $options );
parent::setUp();
}
public function test_constructor_adds_correct_actions() {
$this->assertEquals( 10, has_action( 'add_meta_boxes', array( $this->metabox, 'add_meta_box' ) ) );
$this->assertEquals( 10, has_action( 'save_post', array( $this->metabox, 'save_meta_box' ) ) );
}
public function test_add_meta_box_does_not_add_box_to_wrong_post_type() {
$add_meta_box = $this->getFunctionMock( 'WPDiscourse\MetaBox', 'add_meta_box' );
$add_meta_box->expects( $this->never() );
$this->metabox->add_meta_box( 'product' );
}
public function test_add_meta_box_adds_meta_box_to_correct_post_type() {
$add_meta_box = $this->getFunctionMock( 'WPDiscourse\MetaBox', 'add_meta_box' );
$add_meta_box->expects( $this->once() );
$this->metabox->add_meta_box( 'page' );
}
public function test_save_meta_box_updates_post_has_been_saved_meta_data() {
$postarr = array(
'ID' => 0,
'post_author' => 1,
'post_status' => 'publish',
'post_title' => 'This is a test',
);
$post_id = wp_insert_post( $postarr, true );
$_POST['publish_to_discourse_nonce'] = 'nonce';
$wp_verify_nonce = $this->getFunctionMock( 'WPDiscourse\MetaBox', 'wp_verify_nonce' );
$wp_verify_nonce->expects( $this->once() )
->with( $this->anything() )
->willReturn( true );
$current_user_can = $this->getFunctionMock( 'WPDiscourse\MetaBox', 'current_user_can' );
$current_user_can->expects( $this->once() )
->with( $this->anything() )
->willReturn( true );
$this->metabox->save_meta_box( $post_id );
$this->assertEquals( 1, get_post_meta( $post_id, 'has_been_saved', true ) );
}
}

View file

@ -0,0 +1,165 @@
<?php
require_once( __DIR__ . '/../../lib/settings-validator.php' );
class TestSettingsValidator extends WP_UnitTestCase {
function setUp() {
$this->validator = new \WPDiscourse\Validator\SettingsValidator();
}
// Validate URL.
function test_validate_url_returns_empty_string_for_invalid_protocol() {
$urls = array( 'htxtp://example.com', 'example.com', 'mailto://example.com' );
foreach ( $urls as $url ) {
$this->assertEquals( '', $this->validator->validate_url( $url ) );
}
}
function test_validate_url_adds_settings_error_for_invalid_input() {
$urls = array( 'htxtp://example.com', 'http://', 'http://%xample.com' );
foreach ( $urls as $url ) {
$num_errors = count( get_settings_errors() );
$this->validator->validate_url( $url );
$new_errors = count( get_settings_errors() );
$this->assertNotSame( $num_errors, $new_errors );
}
}
function test_validate_url_returns_a_valid_url() {
$urls = array( 'http://example.com', 'https://example.com' );
foreach ( $urls as $url ) {
$this->assertSame( $url, $this->validator->validate_url( $url ) );
}
}
function test_validate_url_strips_trailing_slash() {
$url = 'https://example.com/';
$this->assertSame( 'https://example.com', $this->validator->validate_url( $url ) );
}
// Validate api key.
function test_validate_api_key_sanitizes_input() {
$api_key = 'thisisatest<script>alert("thisisatest");</script>';
$this->assertSame( 'thisisatest', $this->validator->validate_api_key( $api_key ) );
}
function test_validate_api_key_adds_settings_error_for_invalid_input() {
$api_key = 'this-is-a-test';
$num_errors = count( get_settings_errors() );
$this->validator->validate_api_key( $api_key );
$new_errors = count( get_settings_errors() );
$this->assertNotSame( $num_errors, $new_errors );
}
function test_validate_api_key_trims_valid_input() {
$api_key = '1wdlkjsoiuelkj3r45 ';
$this->assertSame( '1wdlkjsoiuelkj3r45', $this->validator->validate_api_key( $api_key ) );
}
// Validate publish category
function test_validate_publish_category_returns_an_int() {
$category = '1';
$this->assertSame( 1, $this->validator->validate_publish_category( $category ) );
}
// Validate publish category update.
function test_validate_publish_category_update_returns_1_for_valid() {
$update = '1';
$this->assertSame( 1, $this->validator->validate_publish_category_update( $update ) );
}
function test_validate_publish_category_update_returns_0_for_invalid() {
$update = 'update';
$this->assertSame( 0, $this->validator->validate_publish_category_update( $update ) );
}
// Validate max comments.
function test_validate_max_comments_adds_settings_error_for_negative_numbers_when_use_discourse_comments_is_true() {
$num_errors = count( get_settings_errors() );
$this->validator->validate_use_discourse_comments( 1 );
$this->validator->validate_max_comments( - 100 );
$new_errors = count( get_settings_errors() );
$this->assertNotSame( $num_errors, $new_errors );
}
function test_validate_max_comments_sanitizes_input_but_does_not_add_error_when_discourse_comments_not_true() {
$num_errors = count( get_settings_errors() );
$this->assertSame( $this->validator->validate_max_comments( 'one hundred' ), '' );
$new_errors = count( get_settings_errors() );
$this->assertSame( $num_errors, $new_errors );
}
// Validate sso secret.
function test_validate_sso_secret_sanitizes_input_but_does_not_add_error_when_not_using_sso() {
$num_errors = count( get_settings_errors() );
$this->assertSame( $this->validator->validate_sso_secret( 'abcde<script>fg</script>' ), 'abcde' );
$new_errors = count( get_settings_errors() );
$this->assertSame( $num_errors, $new_errors );
}
function test_validate_sso_secret_adds_settings_error_when_sso_enabled() {
$num_errors = count( get_settings_errors() );
$this->validator->validate_enable_sso( 1 );
$this->validator->validate_sso_secret( 'abc' );
$new_errors = count( get_settings_errors() );
$this->assertNotSame( $num_errors, $new_errors );
}
// Validate login path.
function test_validate_login_path_must_begin_with_forward_slash() {
$this->validator->validate_enable_sso( 1 );
$num_errors = count( get_settings_errors() );
$this->validator->validate_login_path( 'my-account' );
$new_errors = count( get_settings_errors() );
$this->assertNotSame( $num_errors, $new_errors );
}
function test_validate_login_path_accepts_valid_input() {
$paths = array( '/login', '/login/path/', '/login-path' );
$this->validator->validate_enable_sso( 1 );
foreach ( $paths as $path ) {
$num_errors = count( get_settings_errors() );
$this->validator->validate_login_path( $path );
$new_errors = count( get_settings_errors() );
$this->assertSame( $num_errors, $new_errors );
}
}
}

View file

@ -0,0 +1,251 @@
<?php
require_once( __DIR__ . '/../../lib/utilities.php' );
use WPDiscourse\Utilities\Utilities as DiscourseUtilities;
use phpmock\phpunit\PHPMock;
class TestUtilities extends \PHPUnit_Framework_TestCase {
use PHPMock;
function test_avatar_substitutes_size_into_template() {
$template = 'http://forum.example.com/user_avatar/scossar/{size}/1_1.png';
$this->assertEquals( 'http://forum.example.com/user_avatar/scossar/60/1_1.png',
DiscourseUtilities::avatar( $template, 60 ) );
}
function test_homepage_returns_discourse_users_url() {
$post = new \stdClass();
$post->username = 'scossar';
$this->assertEquals( 'http://forum.example.com/users/scossar',
DiscourseUtilities::homepage( 'http://forum.example.com', $post ) );
}
function test_convert_relative_img_src_to_absolute_when_supplied_with_absolute_src() {
$content = '<img src="http://example.com/uploads/example.png" />';
$this->assertEquals( $content, DiscourseUtilities::convert_relative_img_src_to_absolute( 'http://example.com', $content ) );
}
function test_convert_relative_img_src_to_absolute_when_first_image_is_absolute_and_second_is_relative() {
$this->markTestIncomplete( 'This will fail, fix!' );
$content = '<img src="//testbucket.s3-us-west-2.amazonaws.com/example.jpg"><img src="/uploads/example.png" />';
$expected_result = '<img src="//testbucket.s3-us-west-2.amazonaws.com/example.jpg"><img src="http://example.com/uploads/example.png" />';
$this->assertEquals( $expected_result, DiscourseUtilities::convert_relative_img_src_to_absolute( 'http://example.com', $content ) );
}
function test_convert_relative_img_src_to_absolute_when_supplied_with_relative_src() {
$content = '<img src="/uploads/example.png" />';
$this->assertEquals( '<img src="http://example.com/uploads/example.png" />', DiscourseUtilities::convert_relative_img_src_to_absolute( 'http://example.com', $content ) );
}
// Brittle test.
function test_get_discourse_categories_returns_cached_categories_when_force_update_false_and_there_are_cached_categories() {
$this->markTestIncomplete();
$options = array(
'api-key' => 'thisisatest',
'publish-username' => 'system',
'url' => 'http://forum.example.com',
'publish-category-update' => 0,
);
update_option( 'discourse', $options );
$categories = array(
'category one',
'category two',
'category three',
);
$get_transient = $this->getFunctionMock( 'WPDiscourse\Utilities', 'get_transient' );
$get_transient->expects( $this->once() )
->with( 'discourse_settings_categories_cache' )
->willReturn( $categories );
$this->assertEquals( $categories, DiscourseUtilities::get_discourse_categories() );
}
// Brittle test.
function test_discourse_categories_returns_cached_categories_when_remote_returns_an_error() {
$this->markTestIncomplete();
$options = array(
'api-key' => 'thisisatest',
'publish-username' => 'system',
'url' => 'http://forum.example.com',
'publish-category-update' => 1,
);
update_option( 'discourse', $options );
$categories = array(
'category one',
'category two',
'category three',
);
$get_transient = $this->getFunctionMock( 'WPDiscourse\Utilities', 'get_transient' );
$get_transient->expects( $this->once() )
->with( 'discourse_settings_categories_cache' )
->willReturn( $categories );
$wp_remote_get = $this->getFunctionMock( 'WPDiscourse\Utilities', 'wp_remote_get' );
$wp_remote_get->expects( $this->once() )
->with( $this->anything() )
->willReturn( new \WP_Error );
$this->assertEquals( $categories, DiscourseUtilities::get_discourse_categories() );
}
// Brittle test.
public function test_get_discourse_categories_sets_transient() {
$this->markTestIncomplete();
$options = array(
'api-key' => 'thisisatest',
'publish-username' => 'system',
'url' => 'http://forum.example.com',
'publish-category-update' => 1,
);
update_option( 'discourse', $options );
$response = array(
'categories' => array(
array(
'id' => 1,
'name' => 'category one',
),
array(
'id' => 2,
'name' => 'category two',
),
array(
'id' => 3,
'name' => 'category three',
),
),
);
// Mocks getting the site.json from Discourse.
$wp_remote_get = $this->getFunctionMock( 'WPDiscourse\Utilities', 'wp_remote_get' );
$wp_remote_get->expects( $this->once() )
->with( $this->anything() );
// Mocks passing validation.
$wp_remote_retrieve_response_code = $this->getFunctionMock( 'WPDiscourse\Utilities', 'wp_remote_retrieve_response_code' );
$wp_remote_retrieve_response_code->expects( $this->any() )
->with( $this->anything() )
->willReturn( 200 );
// Mocks retrieving the body.
$wp_remote_retrieve_body = $this->getFunctionMock( 'WPDiscourse\Utilities', 'wp_remote_retrieve_body' );
$wp_remote_retrieve_body->expects( $this->any() )
->with( $this->anything() );
// Mocks json_decode. Returns the $response array created above. What is being
// tested runs starts here.
$json_decode = $this->getFunctionMock( 'WPDiscourse\Utilities', 'json_decode' );
$json_decode->expects( $this->once() )
->with( $this->anything(), true )
->willReturn( $response );
$set_transient = $this->getFunctionMock( 'WPDiscourse\Utilities', 'set_transient' );
$set_transient->expects( $this->once() )
->with(
'discourse_settings_categories_cache',
$response['categories'],
HOUR_IN_SECONDS
);
DiscourseUtilities::get_discourse_categories();
}
// Brittle test.
public function test_get_discourse_categories_removes_subcategories_when_display_subcategories_is_not_set() {
$this->markTestIncomplete();
$options = array(
'api-key' => 'thisisatest',
'publish-username' => 'system',
'url' => 'http://forum.example.com',
'publish-category-update' => 1,
'display-subcategories' => 0
);
update_option( 'discourse', $options );
$response = array(
'categories' => array(
array(
'id' => 1,
'name' => 'category one',
),
array(
'id' => 2,
'name' => 'category two',
'parent_category_id' => 1,
),
array(
'id' => 3,
'name' => 'category three',
),
),
);
// Mocks getting the site.json from Discourse.
$wp_remote_get = $this->getFunctionMock( 'WPDiscourse\Utilities', 'wp_remote_get' );
$wp_remote_get->expects( $this->once() )
->with( $this->anything() );
// Mocks passing validation.
$wp_remote_retrieve_response_code = $this->getFunctionMock( 'WPDiscourse\Utilities', 'wp_remote_retrieve_response_code' );
$wp_remote_retrieve_response_code->expects( $this->any() )
->with( $this->anything() )
->willReturn( 200 );
// Mocks retrieving the body.
$wp_remote_retrieve_body = $this->getFunctionMock( 'WPDiscourse\Utilities', 'wp_remote_retrieve_body' );
$wp_remote_retrieve_body->expects( $this->any() )
->with( $this->anything() );
// Mocks json_decode. Returns the $response array created above. What is being
// tested runs starts here.
$json_decode = $this->getFunctionMock( 'WPDiscourse\Utilities', 'json_decode' );
$json_decode->expects( $this->once() )
->with( $this->anything(), true )
->willReturn( $response );
// The subcategory is removed.
$this->assertEquals( 2, count( DiscourseUtilities::get_discourse_categories() ) );
}
public function test_validate_returns_zero_for_a_wp_error() {
$response = new \WP_Error();
$this->assertEquals( 0, DiscourseUtilities::validate( $response ) );
}
public function test_validate_returns_zero_if_response_code_is_not_200() {
$response_codes = array( 300, 400, 500 );
foreach ( $response_codes as $code ) {
$response = array(
'response' => array(
'code' => $code,
'message' => 'not found',
),
);
$this->assertEquals( 0, DiscourseUtilities::validate( $response ) );
}
}
public function test_validate_returns_one_if_response_code_is_200() {
$response = array(
'response' => array(
'code' => 200,
'message' => 'not found',
),
);
$this->assertEquals( 1, DiscourseUtilities::validate( $response ) );
}
}

14
uninstall.php Normal file
View file

@ -0,0 +1,14 @@
<?php
/**
* Uninstall the plugin.
*
* @package WPDiscourse
*/
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
exit;
}
delete_option( 'discourse_version' );
delete_option( 'discourse' );
delete_transient( 'discourse_settings_categories_cache' );

View file

@ -1,381 +1,57 @@
<?php
/*
Plugin Name: WP-Discourse
Description: Allows you to publish your posts to a Discourse instance and view top Discourse comments on your blog
Version: 0.0.1
Author: Sam Saffron
Author URI: http://www.discourse.org
*/
/* Copyright 2011 Sam Saffron (sam.saffron@discourse.org)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
class Discourse {
var $domain = 'discourse';
//Version
static $version ='0.0.1';
//Options and defaults
static $options = array(
'url'=>'',
'api-key'=>'',
'publish-username'=>'',
'publish-category'=>'',
'auto-publish'=>0,
'auto-update'=>0,
'max-comment'=>5,
'use-discourse-comments'=>0,
'use-fullname-in-comments'=>1,
'publish-format'=>'<small>Originally published at: {blogurl}</small><br>{excerpt}'
);
public function __construct() {
register_activation_hook(__FILE__,array(__CLASS__, 'install' ));
register_uninstall_hook(__FILE__,array( __CLASS__, 'uninstall' ));
add_action( 'init', array( $this, 'init' ) );
add_action( 'admin_init', array( $this, 'admin_init' ) );
add_action( 'admin_menu', array( $this, 'discourse_admin_menu' ));
}
static function install(){
update_option("discourse_version",self::$version);
add_option('discourse',self::$options);
}
static function uninstall(){
delete_option('discourse_version');
delete_option('discourse');
}
public function init() {
//Allow translations
load_plugin_textdomain( 'discourse', false, basename(dirname(__FILE__)).'/languages');
//replace comments with discourse comments
add_filter('comments_number', array($this,'comments_number'));
add_filter('comments_template', array($this,'comments_template'));
}
function comments_number($count) {
global $post;
if(self::use_discourse_comments($post->ID)){
self::sync_comments($post->ID);
$count = get_post_meta($post->ID, 'discourse_comments_count', true);
if(!$count){
$count = "Leave a reply";
} else {
$count = $count == 1 ? "1 Reply" : $count . " Replies";
}
}
return $count;
}
function use_discourse_comments($postid){
return get_post_meta($postid, 'publish_to_discourse', true) == 1 &&
get_post_meta($postid, 'discourse_post_id', true) > 0;
}
function sync_comments($postid) {
global $wpdb;
# every 10 minutes do a json call to sync comment count and top comments
$last_sync = (int)get_post_meta($postid, 'discourse_last_sync', true);
$time = date_timestamp_get(date_create());
if($last_sync + 60 * 10 < $time) {
$got_lock = $wpdb->get_row( "SELECT GET_LOCK('discourse_lock', 0) got_it");
if($got_lock->got_it == "1") {
$discourse_options = get_option('discourse');
$comment_count = intval($discourse_options['max-comments']);
$permalink = (string)get_post_meta($postid, 'discourse_permalink', true) . '.json?best=' . $comment_count;
$soptions = array('http' => array('ignore_errors' => true, 'method' => 'GET'));
$context = stream_context_create($soptions);
$result = file_get_contents($permalink, false, $context);
$json = json_decode($result);
delete_post_meta($postid, 'discourse_comments_count');
add_post_meta($postid, 'discourse_comments_count', $json->posts_count - 1 , true);
delete_post_meta($postid, 'discourse_comments_raw');
add_post_meta($postid, 'discourse_comments_raw', $wpdb->escape($result) , true);
delete_post_meta($postid, 'discourse_last_sync');
add_post_meta($postid, 'discourse_last_sync', $time, true);
$wpdb->get_results("SELECT RELEASE_LOCK('discourse_lock')");
}
}
}
function comments_template($old) {
global $post;
if(self::use_discourse_comments($post->ID)) {
self::sync_comments($post->ID);
return dirname(__FILE__) . '/comments.php';
}
return $old;
}
/*
* Settings
*/
public function admin_init(){
register_setting( 'discourse', 'discourse', array($this, 'discourse_validate_options'));
add_settings_section( 'default_discourse', 'Default Settings', array($this, 'init_default_settings'), 'discourse' );
add_settings_field('discourse_url', 'Url', array($this, 'url_input'), 'discourse', 'default_discourse');
add_settings_field('discourse_api_key', 'API Key', array($this, 'api_key_input'), 'discourse', 'default_discourse');
add_settings_field('discourse_publish_username', 'Publishing username', array($this, 'publish_username_input'), 'discourse', 'default_discourse');
add_settings_field('discourse_publish_category', 'Published category', array($this, 'publish_category_input'), 'discourse', 'default_discourse');
add_settings_field('discourse_publish_format', 'Publish format', array($this, 'publish_format_textarea'), 'discourse', 'default_discourse');
add_settings_field('discourse_auto_publish', 'Auto Publish', array($this, 'auto_publish_checkbox'), 'discourse', 'default_discourse');
add_settings_field('discourse_auto_update', 'Auto Update Posts', array($this, 'auto_update_checkbox'), 'discourse', 'default_discourse');
add_settings_field('discourse_use_discourse_comments', 'Use Discourse Comments', array($this, 'use_discourse_comments_checkbox'), 'discourse', 'default_discourse');
add_settings_field('discourse_max_comments', 'Max visible comments', array($this, 'max_comments_input'), 'discourse', 'default_discourse');
add_settings_field('discourse_use_fullname_in_comments', 'Full name in comments', array($this, 'use_fullname_in_comments_checkbox'), 'discourse', 'default_discourse');
add_action( 'post_submitbox_misc_actions', array($this,'publish_to_discourse'));
add_action( 'save_post', array($this, 'save_postdata'));
add_action ( 'transition_post_status', array($this, 'post_status_changed'), 10, 3 );
}
function post_status_changed($old, $new, $post){
if($post->post_type == "revision") { return; }
if($new == 'publish' && get_post_meta($post->ID, 'publish_to_discourse', true) == 1) {
self::sync_to_discourse($post->ID, $post->post_title, $post->post_content);
}
}
function save_postdata($postid)
{
if ( !current_user_can( 'edit_page', $postid ) ) return $postid;
if(empty($postid) || !isset($_POST['publish_to_discourse'])) return $postid;
# trust me ... word press is crazy like this, try changing a title.
if(!isset($_POST['ID'])) return $postid;
if($_POST['action'] == 'editpost'){
delete_post_meta($_POST['ID'], 'publish_to_discourse');
}
$publish = $_POST['publish_to_discourse'];
add_post_meta($_POST['ID'], 'publish_to_discourse', $publish, true);
return $postid;
}
function sync_to_discourse($postid, $title, $raw) {
$discourse_id = get_post_meta($postid, 'discourse_post_id', true);
$options = get_option('discourse');
$post = get_post($post_id);
$excerpt = apply_filters('the_content', $raw);
$excerpt = wp_trim_words($excerpt);
$baked = $options['publish-format'];
$baked = str_replace("{excerpt}", $excerpt, $baked);
$baked = str_replace("{blogurl}", get_permalink($postid), $baked);
$data = array(
'wp-id' => $postid,
'api_key' => $options['api-key'],
'api_username' => $options['publish-username'],
'title' => $title,
'post[raw]' => $baked,
'post[category]' => $options['publish-category']
);
if(!$discourse_id > 0) {
$url = $options['url'] .'/posts';
// use key 'http' even if you send the request to https://...
$soptions = array('http' => array('ignore_errors' => true, 'method' => 'POST','content' => http_build_query($data)));
$context = stream_context_create($soptions);
$result = file_get_contents($url, false, $context);
$json = json_decode($result);
#todo may have $json->errors with list of errors
if(property_exists($json, 'id')) {
$discourse_id = (int)$json->id;
}
if(isset($discourse_id) && $discourse_id > 0) {
add_post_meta($postid, 'discourse_post_id', $discourse_id, true);
}
}
else {
# for now the updates are just causing grief, leave'em out
return;
$url = $options['url'] .'/posts/' . $discourse_id ;
$soptions = array('http' => array('ignore_errors' => true, 'method' => 'PUT','content' => http_build_query($data)));
$context = stream_context_create($soptions);
$result = file_get_contents($url, false, $context);
$json = json_decode($result);
if(isset($json->post)) {
$json = $json->post;
}
# todo may have $json->errors with list of errors
}
if(isset($json->topic_slug)){
delete_post_meta($postid,'discourse_permalink');
add_post_meta($postid,'discourse_permalink', $options['url'] . '/t/' . $json->topic_slug . '/' . $json->topic_id, true);
}
}
function publish_to_discourse()
{
global $post;
$value = get_post_meta($post->ID, 'publish_to_discourse', true);
echo '<div class="misc-pub-section misc-pub-section-last">
<span>'
. '<label><input type="checkbox"' . (!empty($value) ? ' checked="checked" ' : null) . 'value="1" name="publish_to_discourse" /> Publish to Discourse</label>'
.'</span></div>';
}
function init_default_settings() {
}
function url_input(){
self::text_input('url', 'Enter your discourse url Eg: http://discuss.mysite.com');
}
function api_key_input(){
self::text_input('api-key', '');
}
function publish_username_input(){
self::text_input('publish-username', 'Discourse username of publisher');
}
function publish_category_input(){
self::text_input('publish-category', 'Category post will be published in Discourse (optional)');
}
function publish_format_textarea(){
self::text_area('publish-format', 'Markdown format for published articles, use {excerpt} for excerpt and {blogurl} for the url of the blog post');
}
function max_comments_input(){
self::text_input('max-comments', 'Maximum number of comments to display');
}
function use_fullname_in_comments_checkbox(){
self::checkbox_input('use-fullname-in-comments', 'Use the users full name in blog comment section');
}
function auto_publish_checkbox(){
self::checkbox_input('auto-publish', 'Publish all new posts to Discourse');
}
function auto_update_checkbox(){
self::checkbox_input('auto-update', 'Update published blog posts on Discourse');
}
function use_discourse_comments_checkbox(){
self::checkbox_input('use-discourse-comments', 'Use Discourse to comment on Discourse published posts (hiding existing comment section)');
}
function checkbox_input($option, $description) {
$options = get_option( 'discourse' );
if (array_key_exists($option, $options) and $options[$option] == 1) {
$value = 'checked="checked"';
} else {
$value = '';
}
?>
<input id='discourse_<?php echo $option?>' name='discourse[<?php echo $option?>]' type='checkbox' value='1' <?php echo $value?> /> <?php echo $description ?>
<?php
}
function text_input($option, $description) {
$options = get_option( 'discourse' );
if (array_key_exists($option, $options)) {
$value = $options[$option];
} else {
$value = '';
}
?>
<input id='discourse_<?php echo $option?>' name='discourse[<?php echo $option?>]' type='text' value='<?php echo esc_attr( $value ); ?>' /> <?php echo $description ?>
<?php
}
function text_area($option, $description) {
$options = get_option( 'discourse' );
if (array_key_exists($option, $options)) {
$value = $options[$option];
} else {
$value = '';
}
?>
<textarea cols=100 rows=6 id='discourse_<?php echo $option?>' name='discourse[<?php echo $option?>]'><?php echo esc_attr( $value ); ?></textarea><br><?php echo $description ?>
<?php
}
function discourse_validate_options($input) {
return $input;
}
function discourse_admin_menu(){
add_options_page( 'Discourse', 'Discourse', 'manage_options', 'discourse', array ( $this, 'discourse_options_page' ));
}
function discourse_options_page() {
?>
<div class="wrap">
<h2>Discourse Options</h2>
<form action="options.php" method="POST">
<?php settings_fields( 'discourse' ); ?>
<?php do_settings_sections('discourse'); ?>
<?php submit_button(); ?>
</form>
</div>
<?php
}
}
$discourse = new Discourse();
/**
* Plugin Name: WP-Discourse
* Description: Use Discourse as a community engine for your WordPress blog
* Version: 0.7.0
* Author: Sam Saffron, Robin Ward
* Text Domain: wp-discourse
* Domain Path: /languages
* Author URI: https://github.com/discourse/wp-discourse
* Plugin URI: https://github.com/discourse/wp-discourse
* GitHub Plugin URI: https://github.com/discourse/wp-discourse
*
* @package WPDiscourse
*/
/** Copyright 2014 Civilized Discourse Construction Kit, Inc (team@discourse.org)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
define( 'WPDISCOURSE_PATH', plugin_dir_path( __FILE__ ) );
define( 'WPDISCOURSE_URL', plugins_url( '', __FILE__ ) );
require_once( __DIR__ . '/lib/utilities.php' );
require_once( __DIR__ . '/lib/html-templates.php' );
require_once( __DIR__ . '/lib/discourse.php' );
require_once( __DIR__ . '/lib/settings-validator.php' );
require_once( __DIR__ . '/lib/admin.php' );
require_once( __DIR__ . '/lib/sso.php' );
require_once( __DIR__ . '/lib/discourse-sso.php' );
require_once( __DIR__ . '/lib/discourse-publish.php' );
require_once( __DIR__ . '/lib/discourse-comment.php' );
require_once( __DIR__ . '/lib/meta-box.php' );
require_once( __DIR__ . '/lib/plugin-support/woocommerce-support.php' );
$discourse_settings_validator = new WPDiscourse\Validator\SettingsValidator();
$discourse = new WPDiscourse\Discourse\Discourse();
$discourse_admin = new WPDiscourse\DiscourseAdmin\DiscourseAdmin();
$discourse_publisher = new WPDiscourse\DiscoursePublish\DiscoursePublish();
$discourse_comment = new WPDiscourse\DiscourseComment\DiscourseComment();
$discourse_sso = new WPDiscourse\DiscourseSSO\DiscourseSSO();
$discourse_publish_metabox = new WPDiscourse\MetaBox\MetaBox();
$discourse_woocommerce = new WPDiscourse\PluginSupport\WooCommerceSupport();
register_activation_hook( __FILE__, array( $discourse, 'install' ) );