diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a880a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +bin +vendor +composer.lock +*.sublime-project +*.sublime-workspace +nbproject/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..38b233c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +language: php +sudo: false + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - hhvm + +env: + - WP_VERSION=3.8 + - WP_VERSION=latest + +before_script: + - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION + - fgrep wp_version /tmp/wordpress/wp-includes/version.php + +script: phpunit diff --git a/README.md b/README.md index d8dbcab..1b7a199 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,23 @@ # Freifunk Metadata Wordpress Plugin +[![Build Status](https://travis-ci.org/mschuett/freifunkmeta.svg?branch=master)](https://travis-ci.org/mschuett/freifunkmeta) + A small Wordpress plugin to render Freifunk metadata according to the [api.freifunk.net](https://github.com/freifunk/api.freifunk.net) specification ([german description](http://freifunk.net/blog/2013/12/die-freifunk-api/)). It reads (and caches) JSON input from a configured URL and provides shortcodes to output the data. -Currently implemented are `[ff_services]` and `[ff_contact]`. +Currently implemented are `[ff_services]`, `[ff_contact]`, and `[ff_state]`. + +An `[ff_location]` is also usable, but needs more work. ## Example Text: + Location: + + [ff_location] + Services: [ff_services] @@ -20,8 +28,11 @@ Text: Contact Jena: - [ff_contact url="http://freifunk-jena.de/jena.json"] + [ff_contact jena] Output: +![_location output example](http://mschuette.name/wp/wp-upload/freifunk_meta_location_sample.png) + ![shortcode output example](http://mschuette.name/wp/wp-upload/freifunk_meta_example.png) + diff --git a/behat.yml b/behat.yml new file mode 100644 index 0000000..6b10c9c --- /dev/null +++ b/behat.yml @@ -0,0 +1,16 @@ +default: + context: + parameters: + base_url: http://localhost:8080/ + role_map: + ender: subscriber + starter: editor + extensions: + Behat\MinkExtension\Extension: + base_url: http://localhost:8080/ + goutte: ~ + selenium2: + wd_host: http://localhost:8910/wd/hub + browser_name: phantomjs + default_session: selenium2 + javascript_session: selenium2 \ No newline at end of file diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh new file mode 100644 index 0000000..0aaf09f --- /dev/null +++ b/bin/install-wp-tests.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash + +if [ $# -lt 3 ]; then + echo "usage: $0 [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" +else + # http serves a single offer, whereas https serves multiple. we only want one + download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json + grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json + LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') + if [[ -z "$LATEST_VERSION" ]]; then + echo "Latest WordPress version could not be found" + exit 1 + fi + WP_TESTS_TAG="tags/$LATEST_VERSION" +fi + +set -ex + +install_wp() { + + if [ -d $WP_CORE_DIR ]; then + return; + fi + + mkdir -p $WP_CORE_DIR + + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + 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 + + 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 + + cd $WP_TESTS_DIR + + 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 diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..65d629a --- /dev/null +++ b/composer.json @@ -0,0 +1,16 @@ +{ + "minimum-stability": "stable", + "config": { + "bin-dir": "bin", + "vendor-dir": "vendor" + }, + "require-dev": { + "phpunit/phpunit": "4.1.*", + "behat/behat": "2.5.3", + "behat/behat": "2.5.3", + "behat/mink": "1.5", + "behat/mink-extension": "*", + "behat/mink-goutte-driver": "*", + "behat/mink-selenium2-driver": "*" + } +} diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php new file mode 100644 index 0000000..d615d33 --- /dev/null +++ b/features/bootstrap/FeatureContext.php @@ -0,0 +1,67 @@ +login($username, $password); + } + + /** + * @When /^I write a post with title "([^"]*)" and content "([^"]*)"$/ + */ + public function iWriteAPostWithTitleAndContent($post_title, $content) + { + $this->fill_in_post('post', $post_title, 'publish', $content); + } + + /** + * @Given /^the plugin "([^"]*)" is "([^"]*)"$/ + */ + public function thePluginIs($plugin, $state) + { + if ($state == "active") { + $action = "activate"; + } else { + $action = "deactivate"; + } + shell_exec(escapeshellcmd("wp plugin $action $plugin")); + } + + /** + * @When /^I search for "([^"]*)"$/ + */ + public function iSearchFor($term) + { + return array( + new When("I fill in \"s\" with \"$term\""), + new When("I press \"searchsubmit\""), + ); + } + +} diff --git a/features/bootstrap/WordPressContext.php b/features/bootstrap/WordPressContext.php new file mode 100644 index 0000000..130cecc --- /dev/null +++ b/features/bootstrap/WordPressContext.php @@ -0,0 +1,248 @@ +base_url = $params['base_url']; + $this->role_map = $params['role_map']; + } + + /** + * Given a list of usernames (user_login field), checks for every username + * if they exist. Returns a list of the users that do not exist. + * + * @param array $users + * @return array + * @author Maarten Jacobs + **/ + protected function check_users_exist(array $users) { + $session = $this->getSession(); + + // Check if the users exist, saving the inexistent users + $inexistent_users = array(); + $this->visit( 'wp-admin/users.php' ); + $current_page = $session->getPage(); + foreach ($users as $username) { + if (!$current_page->hasContent($username)) { + $inexistent_users[] = $username; + } + } + + return $inexistent_users; + } + + /** + * Creates a user for every username given (user_login field). + * The inner values can also maps of the following type: + * array( + * 'username' => + * 'password' => (default: pass) + * 'email' => (default: username@test.dev) + * 'role' => (default: checks rolemap, or 'subscriber') + * ) + * + * @param array $users + * @author Maarten Jacobs + **/ + protected function create_users(array $users) { + $session = $this->getSession(); + + foreach ($users as $username) { + if (is_array($username)) { + $name = $username['username']; + $password = array_key_exists('password', $username) ? $username['password'] : 'pass'; + $email = array_key_exists('email', $username) ? $username['email'] : str_replace(' ', '_', $name) . '@test.dev'; + } else { + $name = $username; + $password = 'pass'; + $email = str_replace(' ', '_', $name) . '@test.dev'; + } + + $this->visit( 'wp-admin/user-new.php' ); + $current_page = $session->getPage(); + + // Fill in the form + $current_page->findField('user_login')->setValue($name); + $current_page->findField('email')->setValue($email); + $current_page->findField('pass1')->setValue($password); + $current_page->findField('pass2')->setValue($password); + + // Set role + $role = ucfirst( strtolower( $this->role_map[$name] ) ); + $current_page->findField('role')->selectOption($role); + + // Submit form + $current_page->findButton('Add New User')->click(); + } + } + + /** + * Fills in the form of a generic post. + * Given the status, will either publish or save as draft. + * + * @param string $post_type + * @param string $post_title + * @param string $status Either 'draft' or anything else for 'publish' + * @author Maarten Jacobs + **/ + protected function fill_in_post($post_type, $post_title, $status = 'publish', $content = '

Testing all the things. All the time.

') { + // The post type, if not post, will be appended. + // Rather than a separate page per type, this is how WP works with forms for separate post types. + $uri_suffix = $post_type !== 'post' ? '?post_type=' . $post_type : ''; + $this->visit( 'wp-admin/post-new.php' . $uri_suffix ); + $session = $this->session = $this->getSession(); + $current_page = $session->getPage(); + + // Fill in the title + $current_page->findField( 'post_title' )->setValue( $post_title ); + // Fill in some nonsencical data for the body + // clickLink and setValue seem to be failing for TinyMCE (same for Cucumber unfortunately) + $session->executeScript( 'jQuery( "#content-html" ).click()' ); + $session->executeScript( 'jQuery( "#content" ).val( "' . $content . '" )' ); + + // Click the appropriate button depending on the given status + $state_button = 'Save Draft'; + switch ($status) { + case 'draft': + // We're good. + break; + + case 'publish': + default: + // Save as draft first + $current_page->findButton($state_button)->click(); + $state_button = 'Publish'; + break; + } + $current_page->findButton($state_button)->click(); + // go to view of new post + $session->getPage()->clickLink("View $post_type"); + } + + /** + * Makes sure the current user is logged out, and then logs in with + * the given username and password. + * + * @param string $username + * @param string $password + * @author Maarten Jacobs + **/ + protected function login($username, $password = 'pass') { + $session = $this->session = $this->getSession(); + $current_page = $session->getPage(); + + // Check if logged in as that user + $this->visit( 'wp-admin' ); + if ($current_page->hasContent( "Howdy, {$username}" )) { + // We're already logged in as this user. + // Double-check + $this->assertPageContainsText('Dashboard'); + return true; + } + + // Logout + $this->visit( 'wp-login.php?action=logout' ); + if ($session->getPage()->hasLink('log out')) { + $session->getPage()->clickLink('log out'); + $current_page = $session->getPage(); + } + + // And login + $current_page->fillField('user_login', $username); + $current_page->fillField('user_pass', $password); + $current_page->findButton('wp-submit')->click(); + + // Assert that we are on the dashboard + $this->assertPageContainsText('Dashboard'); + } + + /** + * Given the current page is post list page, we enter the title in the searchbox + * and search for that post. + * + * @param string $post_title The title of the post as it would appear in the WP backend + * @param boolean $do_assert If set to anything but false, will assert for the existence of the post title after the search + * @return void + * @author Maarten Jacobs + **/ + protected function searchForPost( $post_title, $do_assert = FALSE ) { + + $current_page = $this->getSession()->getPage(); + + // Search for the post + $search_field = $current_page->findField( 'post-search-input' ); // Searching on #id + // When there is no content, then the searchbox is not shown + // So we skip search in that case + if ($search_field) { + $search_field->setValue( $post_title ); + + $current_page->findField( 'Search Posts' ) // Searching on value + ->click(); + } + + // We don't stop tests even if the searchbox does not exist. + // That would prevent the dev from knowing what the hell's going on. + // Can I assert all the things? + if ( $do_assert ) { + $this->assertPageContainsText($post_title); + } + + } + + /** + * @Given /^I trash the "([^"]*)" titled "([^"]*)"$/ + */ + public function iTrashThePostTitled( $post_type, $post_title ) { + + $session = $this->session = $this->getSession(); + + // Visit the posts page + $uri_suffix = $post_type !== 'post' ? '?post_type=' . $post_type : ''; + $postlist_uri = 'wp-admin/edit.php' . $uri_suffix; + $this->visit( $postlist_uri ); + $current_page = $session->getPage(); + + // Check if the post with that title is on the current page + if (!$current_page->hasContent( $post_title )) { + // If not, search for the post + $this->searchForPost( $post_title ); + } + $this->assertPageContainsText($post_title); + + // Select the post in the checkbox column + // This is tricky: the checkbox has a non-unique name (of course, that's the way to do it) + // So we need to check the box in a different way + // The easiest: jQuery + $session->executeScript( "jQuery( \"tr:contains('$post_title') :checkbox\" ).click()" ); + + // Trash it + // - Select the 'Move to Trash' option + $current_page->selectFieldOption( 'action', 'Move to Trash' ); + // - Click to Apply + $current_page->findButton( 'doaction' )->click(); + + // Check if the post is no longer visible on the posts page + $this->visit( $postlist_uri ); + $this->assertPageNotContainsText( $post_title ); + // Make a search, because it could be on another page + $this->searchForPost( $post_title ); + $this->assertPageNotContainsText( $post_title ); + + } + +} diff --git a/features/shortcodes.feature b/features/shortcodes.feature new file mode 100644 index 0000000..28df0fd --- /dev/null +++ b/features/shortcodes.feature @@ -0,0 +1,21 @@ +Feature: Use Shortcodes + In order to use my Plugin + As a website author + I need to write posts with shortcodes + + Background: + Given I am logged in as "admin" with "vagrant" + + Scenario: Without the plugin + Given the plugin "freifunkmeta" is "inactive" + When I write a post with title "test" and content "[ff_contact]" + #Then print current URL + Then I should see "ff_contact" + + Scenario: With the plugin + Given the plugin "freifunkmeta" is "active" + When I write a post with title "test" and content "[ff_contact]" + #Then print current URL + Then I should see "Twitter" in the ".ff_contact" element + And I should not see "ff_contact" + diff --git a/features/wp-search.feature b/features/wp-search.feature new file mode 100644 index 0000000..879e7d8 --- /dev/null +++ b/features/wp-search.feature @@ -0,0 +1,10 @@ +Feature: Search + In order to find older articles + As a website user + I need to be able to search for a word + + Scenario: Searching for a post + Given I am on the homepage + When I search for "Welcome" + Then I should see "Hello world!" + And I should see "Welcome to WordPress. This is your first post." diff --git a/freifunk_marker.png b/freifunk_marker.png new file mode 100644 index 0000000..279c962 Binary files /dev/null and b/freifunk_marker.png differ diff --git a/freifunkmeta.php b/freifunkmeta.php index e6d0f4e..ca39e8e 100644 --- a/freifunkmeta.php +++ b/freifunkmeta.php @@ -1,158 +1,536 @@ false ); + $http_response = wp_remote_get( $url, $args ); + if ( is_wp_error( $http_response ) ) { + $error_msg = sprintf( + 'Unable to retrieve URL %s, error: %s', + $url, $http_response->get_error_message() + ); + error_log( $error_msg, 4 ); + return $http_response; + } else { + $json = wp_remote_retrieve_body( $http_response ); + $data = json_decode( $json, $assoc = true ); + set_transient( $cachekey, $data, $cachetime ); + } + } + return $data; + } } -if ( ! shortcode_exists( 'ff_services' ) ) { - add_shortcode( 'ff_services', 'ff_meta_shortcode_services'); -} -// Example: -// [ff_services] -// [ff_services url="http://meta.hamburg.freifunk.net/ffhh.json"] -function ff_meta_shortcode_services( $atts ) { - $default_url = get_option( 'ff_meta_url' ); - extract(shortcode_atts( array( - 'url' => $default_url, - ), $atts)); +/** + * holds the community directory + */ +class FF_Directory +{ + private $directory; + private $ed; - $metadata = ff_meta_getmetadata ($url); - $services = $metadata['services']; + function __construct( $ext_data_service = null ) { + if ( is_null( $ext_data_service ) ) { + $this->ed = new FF_Meta_Externaldata(); + } else { + $this->ed = $ext_data_service; + } + $data = $this->ed->get( FF_META_DEFAULT_DIR ); + if ( is_wp_error( $data ) ) { + $this->directory = array(); + } else { + $this->directory = $data; + } + } - // Output - $outstr = '
    '; - foreach ($services as $service) { - $outstr .= sprintf('
  • %s (%s): %s
  • ', - $service['serviceName'], $service['serviceDescription'], - $service['internalUri'], $service['internalUri']); - } - $outstr .= '
'; - return $outstr; + function get_url_by_city( $city ) { + if ( array_key_exists( $city, $this->directory ) ) { + return $this->directory[$city]; + } else { + return false; + } + } + + // get one big array of all known community data + function get_all_data() { + $all_locs = array(); + foreach ( $this->directory as $tmp_city => $url ) { + $tmp_meta = $this->ed->get( $url ); + if ( ! is_wp_error( $tmp_meta ) ) { + $all_locs[$tmp_city] = $tmp_meta; + } + } + return $all_locs; + } } -if ( ! shortcode_exists( 'ff_contact' ) ) { - add_shortcode( 'ff_contact', 'ff_meta_shortcode_contact'); -} -// Example: -// [ff_contact] -// [ff_contact url="http://meta.hamburg.freifunk.net/ffhh.json"] -function ff_meta_shortcode_contact( $atts ) { - $default_url = get_option( 'ff_meta_url' ); - extract(shortcode_atts( array( - 'url' => $default_url, - ), $atts)); +/** + * OO interface to handle a single community/city + */ +class FF_Community +{ + public $name; + public $street; + public $zip; + public $city; + public $lon; + public $lat; - $metadata = ff_meta_getmetadata ($url); - $contact = $metadata['contact']; + /** + * Default constructor from metadata + */ + function __construct( $metadata ) { + $loc = $metadata['location']; + $this->name = ( isset( $loc['address'] ) && isset( $loc['address']['Name'] ) ) + ? $loc['address']['Name'] : ''; + $this->street = ( isset( $loc['address'] ) && isset( $loc['address']['Street'] ) ) + ? $loc['address']['Street'] : ''; + $this->zip = ( isset( $loc['address'] ) && isset( $loc['address']['Zipcode'] ) ) + ? $loc['address']['Zipcode'] : ''; + $this->city = isset( $loc['city'] ) ? $loc['city'] : ''; + $this->lon = isset( $loc['lon'] ) ? $loc['lon'] : ''; + $this->lat = isset( $loc['lat'] ) ? $loc['lat'] : ''; + } - // Output -- rather ugly but the data is not uniform, some fields are URIs, some are usernames, ... - $outstr = '

'; - if (!empty($contact['email'])) { - $outstr .= sprintf("E-Mail: %s
\n", $contact['email'], $contact['email']); - } - if (!empty($contact['ml'])) { - $outstr .= sprintf("Mailingliste: %s
\n", $contact['ml'], $contact['ml']); - } - if (!empty($contact['irc'])) { - $outstr .= sprintf("IRC: %s
\n", $contact['irc'], $contact['irc']); - } - if (!empty($contact['twitter'])) { - // catch username instead of URI - if ($contact['twitter'][0] === "@") { - $twitter_url = 'http://twitter.com/'.ltrim($contact['twitter'], "@"); - $twitter_handle = $contact['twitter']; - } else { - $twitter_url = $contact['twitter']; - $twitter_handle = '@' . substr($contact['twitter'], strrpos($contact['twitter'], '/') + 1); - } - $outstr .= sprintf("Twitter: %s
\n", $twitter_url, $twitter_handle); - } - if (!empty($contact['facebook'])) { - $outstr .= sprintf("Facebook: %s
\n", $contact['facebook'], $contact['facebook']); - } - if (!empty($contact['googleplus'])) { - $outstr .= sprintf("G+: %s
\n", $contact['googleplus'], $contact['googleplus']); - } - if (!empty($contact['jabber'])) { - $outstr .= sprintf("XMPP: %s
\n", $contact['jabber'], $contact['jabber']); - } - # maybe we do not want public phone numbers... - #if (!empty($contact['phone'])) { - # $outstr .= sprintf("Telephon: %s
\n", $contact['phone']); - #} - $outstr .= '

'; - return $outstr; + /** + * Alternative constructor from city name + */ + static function make_from_city( $city, $ext_data_service = null ) { + if ( is_null( $ext_data_service ) ) { + $ed = new FF_Meta_Externaldata(); + } else { + $ed = $ext_data_service; + } + $directory = new FF_Directory( $ed ); + + if ( false === ( $url = $directory->get_url_by_city( $city ) ) ) { + return '\n"; + } + if ( false === ( $metadata = $ed->get( $url ) ) ) { + return "\n"; + } + return new FF_Community( $metadata ); + } + + function format_address() { + if ( empty( $this->name ) || empty( $this->street ) || empty( $this->zip ) ) { + return ''; + } + // TODO: style address + map as single box + // TODO: once it is "ready" package openlayers.js into the plugin + // ( cf. http://docs.openlayers.org/library/deploying.html ) + // TODO: handle missing values ( i.e. only name & city ) + return sprintf( + '

%s
%s
%s %s

', + $this->name, $this->street, $this->zip, $this->city + ); + } } -// Options Page: -add_action( 'admin_menu', 'ff_meta_admin_menu' ); -function ff_meta_admin_menu() { - add_options_page( - 'FF Meta Plugin', // page title - 'FF Meta', // menu title - 'manage_options', // req'd capability - 'ff_meta_plugin', // menu slug - 'ff_meta_options_page' // callback function - ); -} - -add_action( 'admin_init', 'ff_meta_admin_init' ); -function ff_meta_admin_init() { - register_setting( - 'ff_meta_settings-group', // group name - 'ff_meta_url' // option name - ); - add_settings_section( - 'ff_meta_section-one', // ID - 'Section One', // Title - 'ff_meta_section_one_callback', // callback to fill - 'ff_meta_plugin' // page to display on - ); - add_settings_field( - 'ff_meta_url', // ID - 'URL of meta.json', // Title - 'ff_meta_url_callback', // callback to fill field - 'ff_meta_plugin', // menu page=slug to display field on - 'ff_meta_section-one' // section to display the field in - ); -} - -function ff_meta_section_one_callback() { - echo 'This Plugin provides shortcodes to display information from the Freifunk meta.json.'; -} - -function ff_meta_url_callback() { - $url = get_option( 'ff_meta_url' ); - echo ""; -} - -function ff_meta_options_page() { - ?> -
-

Freifunk Meta Plugin Options

-
- - - -
-
- ed = new FF_Meta_Externaldata(); + } else { + $this->ed = $ext_data_service; + } + $this->dir = new FF_Directory( $this->ed ); + } + + function __construct( $ext_data_service = null ) { + if ( is_null( $ext_data_service ) ) { + $this->ed = new FF_Meta_Externaldata(); + } else { + $this->ed = $ext_data_service; + } + $this->dir = new FF_Directory( $this->ed ); + } + + function register_stuff() { + if ( ! shortcode_exists( 'ff_state' ) ) { + add_shortcode( 'ff_state', array( $this, 'shortcode_handler' ) ); + } + if ( ! shortcode_exists( 'ff_services' ) ) { + add_shortcode( 'ff_services', array( $this, 'shortcode_handler' ) ); + } + if ( ! shortcode_exists( 'ff_contact' ) ) { + add_shortcode( 'ff_contact', array( $this, 'shortcode_handler' ) ); + } + if ( ! shortcode_exists( 'ff_location' ) ) { + add_shortcode( 'ff_location', array( $this, 'shortcode_handler' ) ); + } + if ( ! shortcode_exists( 'ff_list' ) ) { + add_shortcode( 'ff_list', array( $this, 'shortcode_handler' ) ); + } + + add_action( 'admin_menu', array( $this, 'admin_menu' ) ); + add_action( 'admin_init', array( $this, 'admin_init' ) ); + register_uninstall_hook( __FILE__, array( 'ff_meta', 'uninstall_hook' ) ); + } + + private function aux_get_all_locations_json() { + if ( WP_DEBUG || ( false === ( $json_locs = get_transient( 'FF_metadata_json_locs' ) ) ) ) { + $all_locs = array(); + $comm_list = $this->dir->get_all_data(); + foreach ( $comm_list as $entry ) { + if ( isset( $entry['location'] ) + && isset( $entry['location']['lat'] ) + && isset( $entry['location']['lon'] ) + ) { + $all_locs[$entry['location']['city']] = array( + 'lat' => $entry['location']['lat'], + 'lon' => $entry['location']['lon'], + ); + } + } + $json_locs = json_encode( $all_locs ); + $cachetime = get_option( 'FF_meta_cachetime', FF_META_DEFAULT_CACHETIME ) * MINUTE_IN_SECONDS; + set_transient( 'FF_metadata_json_locs', $json_locs, $cachetime ); + } + return $json_locs; + } + + function output_ff_state( $citydata ) { + if ( isset( $citydata['state'] ) && isset( $citydata['state']['nodes'] ) ) { + return sprintf( '%s', $citydata['state']['nodes'] ); + } else { + return ''; + } + } + + function output_ff_location( $citydata ) { + // normal per-city code + $loc = new FF_Community( $citydata ); + $outstr = $loc->format_address(); + $json_locs = $this->aux_get_all_locations_json(); + + if ( ! empty( $loc->name ) && ! empty( $loc->name ) ) { + $icon_url = plugin_dir_url( __FILE__ ) . 'freifunk_marker.png'; + $loccity = $loc->city; + $outstr .= << + + + + + +EOT; + } + return $outstr; + } + + function output_ff_services( $citydata ) { + if ( ! isset( $citydata['services'] ) ) { + return ''; + } + $services = $citydata['services']; + $outstr = ''; + foreach ( $services as $service ) { + $internalUri = isset($service['internalUri']) ? $service['internalUri'] : ''; + $externalUri = isset($service['externalUri']) ? $service['externalUri'] : ''; + $outstr .= sprintf( + '', + $service['serviceName'], $service['serviceDescription'], + $internalUri, $internalUri, + $externalUri, $externalUri + ); + } + $outstr .= '
DienstBeschreibungFreifunk URIInternet URI
%s%s%s%s
'; + return $outstr; + } + + function output_ff_contact( $citydata ) { + if ( ! isset( $citydata['contact'] ) ) { + return ''; + } + $contact = $citydata['contact']; + $outstr = '

'; + // Output -- rather ugly but the data is not uniform, + // some fields are URIs, some are usernames, ... + if ( ! empty( $contact['email'] ) ) { + $outstr .= sprintf( + 'E-Mail: %s
', + $contact['email'], $contact['email'] + ); + } + if ( ! empty( $contact['ml'] ) ) { + $outstr .= sprintf( + 'Mailingliste: %s
', + $contact['ml'], $contact['ml'] + ); + } + if ( ! empty( $contact['irc'] ) ) { + $outstr .= sprintf( + 'IRC: %s
', + $contact['irc'], $contact['irc'] + ); + } + if ( ! empty( $contact['twitter'] ) ) { + // catch username instead of URI + if ( $contact['twitter'][0] === '@' ) { + $twitter_url = 'http://twitter.com/' . ltrim( $contact['twitter'], '@' ); + $twitter_handle = $contact['twitter']; + } else { + $twitter_url = $contact['twitter']; + $twitter_handle = '@' . substr( + $contact['twitter'], strrpos( $contact['twitter'], '/' ) + 1 + ); + } + $outstr .= sprintf( + 'Twitter: %s
', + $twitter_url, $twitter_handle + ); + } + if ( ! empty( $contact['facebook'] ) ) { + $outstr .= sprintf( + 'Facebook: %s
', + $contact['facebook'], $contact['facebook'] + ); + } + if ( ! empty( $contact['googleplus'] ) ) { + $outstr .= sprintf( + 'G+: %s
', + $contact['googleplus'], $contact['googleplus'] + ); + } + if ( ! empty( $contact['jabber'] ) ) { + $outstr .= sprintf( + 'XMPP: %s
', + $contact['jabber'], $contact['jabber'] + ); + } + $outstr .= '

'; + return $outstr; + } + + function output_ff_list() { + $comm_list = $this->dir->get_all_data(); + $outstr = ''; + $outstr .= ''; + foreach ( $comm_list as $handle => $entry ) { + $outstr .= sprintf( + '', + esc_url( $entry['url'] ), + isset( $entry['name'] ) ? esc_html( $entry['name'] ) : esc_html($handle), + isset( $entry['location']['city'] ) ? esc_html( $entry['location']['city'] ) : 'n/a', + isset( $entry['state']['nodes'] ) ? esc_html( $entry['state']['nodes'] ) : 'n/a' + ); + } + $outstr .= '
NameStadtKnoten
%s%s%s
'; + return $outstr; + } + + function shortcode_handler( $atts, $content, $shortcode ) { + // $atts[0] holds the city name, if given + if ( empty( $atts[0] ) ) { + $city = get_option( 'FF_meta_city', FF_META_DEFAULT_CITY ); + } else { + $city = $atts[0]; + } + + if ( false === ( $cityurl = $this->dir->get_url_by_city( $city ) ) ) { + return "\n"; + } + + $ed = new FF_Meta_Externaldata(); + if ( false === ( $metadata = $this->ed->get( $cityurl ) ) ) { + return "\n"; + } + + $outstr = "
"; + switch ( $shortcode ) { + case 'ff_state': + $outstr .= $this->output_ff_state( $metadata ); + break; + case 'ff_location': + $outstr .= $this->output_ff_location( $metadata ); + break; + case 'ff_services': + $outstr .= $this->output_ff_services( $metadata ); + break; + case 'ff_contact': + $outstr .= $this->output_ff_contact( $metadata ); + break; + case 'ff_list': + $outstr .= $this->output_ff_list(); + break; + default: + $outstr .= ''; + break; + } + $outstr .= '
'; + return $outstr; + } + + function admin_menu() { + // Options Page: + add_options_page( + 'FF Meta Plugin', // page title + 'FF Meta', // menu title + 'manage_options', // req'd capability + 'ff_meta_plugin', // menu slug + array( 'FF_meta', 'options_page' ) // callback function + ); + } + + function admin_init() { + register_setting( + 'ff_meta_settings-group', // group name + 'ff_meta_cachetime' // option name + ); + register_setting( + 'ff_meta_settings-group', // group name + 'ff_meta_city' // option name + ); + add_settings_section( + 'ff_meta_section-one', // ID + 'Section One', // Title + array( 'FF_Meta', 'section_one_callback' ), // callback to fill + 'ff_meta_plugin' // page to display on + ); + add_settings_field( + 'ff_meta_city', // ID + 'Default community', // Title + array( 'FF_Meta', 'city_callback' ), // callback to fill field + 'ff_meta_plugin', // menu page=slug to display field on + 'ff_meta_section-one', // section to display the field in + array( 'label_for' => 'ff_meta_city_id' ) // ID of input element + ); + add_settings_field( + 'ff_meta_cachetime', // ID + 'Cache time', // Title + array( 'FF_Meta', 'cachetime_callback' ), // callback to fill field + 'ff_meta_plugin', // menu page=slug to display field on + 'ff_meta_section-one', // section to display the field in + array( 'label_for' => 'ff_meta_cachetime_id' ) // ID of input element + ); + } + + function section_one_callback() { + echo 'This Plugin provides shortcodes to display information' + .' from the Freifunk meta.json.'; + } + + function cachetime_callback() { + $time = get_option( 'ff_meta_cachetime', FF_META_DEFAULT_CACHETIME ); + echo 'Data from external URLs is cached' + .' for this number of minutes.

'; + } + + function city_callback() { + $ed = new FF_Meta_Externaldata(); + if ( false === ( $directory = $this->ed->get( FF_META_DEFAULT_DIR ) ) ) { + // TODO: error handling + return; + } + $default_city = get_option( 'ff_meta_city', FF_META_DEFAULT_CITY ); + + echo "'; + echo '

This is the default city parameter.

'; + } + + function options_page() { + ?> +
+

Freifunk Meta Plugin Options

+
+ + + +
+
+ register_stuff(); +$GLOBALS['wp-plugin-ffmeta'] = $ffmeta; diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..44f0fdb --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,14 @@ + + + + ./tests/ + + + diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..2a4de90 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,15 @@ +FFM = new FF_Meta(new MockDataService()); + $this->FFM->reinit_external_data_service(new MockDataService()); + } + + /* some very basic things */ + function test_basic_json_parsing() { + $json = file_get_contents(__DIR__.'/example_ffhh.json'); + $data = json_decode($json, $assoc = true); + + $this->assertArrayHasKey('name', $data); + $this->assertArrayHasKey('state', $data); + $this->assertArrayHasKey('location', $data); + $this->assertArrayHasKey('services', $data); + } + + function test_externaldata_mock() { + $ed = new MockDataService(); + $url_dir = 'https://raw.githubusercontent.com/freifunk/directory.api.freifunk.net/master/directory.json'; + $url_ff = 'http://meta.hamburg.freifunk.net/ffhh.json'; + $url_inv = 'http://meta.hamburg.freifunk.net/invalid.txt'; + + // verify that $ed->get does not read the URLs above, but local example_*.json files + $data_ff = $ed->get($url_ff); + $this->assertArrayHasKey('name', $data_ff); + $this->assertArrayHasKey('state', $data_ff); + $this->assertArrayHasKey('location', $data_ff); + $this->assertArrayHasKey('services', $data_ff); + + $data_dir = $ed->get($url_dir); + $this->assertArrayHasKey('hamburg', $data_dir); + $this->assertEquals(2, count($data_dir)); + + $data_inv = $ed->get($url_inv); + $this->assertEquals(0, count($data_inv)); + } + + /* the aux. classes */ + function test_ff_directory() { + $dir = new FF_Directory(new MockDataService()); + $valid = $dir->get_url_by_city('hamburg'); + $invalid = $dir->get_url_by_city('jena'); + + $this->assertTrue(!!$valid); + $this->assertTrue(!$invalid); + } + + /** + * @expectedException PHPUnit_Framework_Error + */ + function test_ff_community_invalid() { + $data = array(); + $comm = new FF_Community($data); + } + + function test_ff_community_empty() { + $data = array('location' => array()); + $comm = new FF_Community($data); + $this->assertEmpty($comm->street); + $this->assertEmpty($comm->name); + + $string = $comm->format_address(); + $this->assertEquals('', $string); + } + + function test_ff_community_filled() { + $data = array('location' => array( + 'address' => array( + 'Name' => 'some_name', + 'Street' => 'some_street', + 'Zipcode' => 'some_zip' + ), + 'city' => 'some_city', + 'lon' => 'some_lon', + 'lat' => 'some_lat', + )); + $comm = new FF_Community($data); + $this->assertEquals('some_name', $comm->name); + $this->assertEquals('some_street', $comm->street); + $this->assertEquals('some_zip', $comm->zip); + $this->assertEquals('some_city', $comm->city); + $this->assertEquals('some_lon', $comm->lon); + $this->assertEquals('some_lat', $comm->lat); + + $string = $comm->format_address(); + $this->assertEquals('

some_name
some_street
some_zip some_city

', $string); + } + + function test_ff_community_make_from_city() { + $comm = FF_Community::make_from_city('hamburg', new MockDataService()); + $this->assertEquals('Chaos Computer Club Hansestadt Hamburg', $comm->name); + $this->assertEquals('Humboldtstr. 53', $comm->street); + $this->assertEquals('22083', $comm->zip); + $this->assertEquals('Hamburg', $comm->city); + $this->assertEquals(10.024418, $comm->lon); + $this->assertEquals(53.574267, $comm->lat); + } + + /* the output methods */ + function test_output_ff_state_null() { + $data = array("state" => array("nodes" => null)); + $ret = $this->FFM->output_ff_state($data); + $this->assertEmpty($ret); + } + + function test_output_ff_state() { + $data = array("state" => array("nodes" => 429)); + $ret = $this->FFM->output_ff_state($data); + $this->assertRegExp('/429/', $ret); + } + + function test_output_ff_services_null() { + $data = array(); + $ret = $this->FFM->output_ff_services($data); + $this->assertEmpty($ret); + $this->assertEquals('', $ret); + } + + function test_output_ff_services() { + $data = array( + 'services' => array(array( + 'serviceName' => 'jabber', + 'serviceDescription' => 'chat', + 'internalUri' => 'xmpp://jabber.local', + 'externalUri' => 'xmpp://jabber.example.org', + ))); + $ret = $this->FFM->output_ff_services($data); + $this->assertEquals(''. + ''. + ''. + '
DienstBeschreibungFreifunk URIInternet URI
jabberchatxmpp://jabber.localxmpp://jabber.example.org
', $ret); + } + + function test_output_ff_contact_null() { + $data = array(); + $ret = $this->FFM->output_ff_contact($data); + $this->assertEquals('', $ret); + } + + function test_output_ff_contact_filled() { + $data = array('contact' => array( + 'email' => 'mail@example.com', + 'jabber' => 'example@freifunk.net' + )); + $ret = $this->FFM->output_ff_contact($data); + $this->assertRegExp('/E-Mail/', $ret); + $this->assertRegExp('/mailto:mail@example\.com/', $ret); + $this->assertRegExp('/XMPP/', $ret); + $this->assertRegExp('/xmpp:example/', $ret); + + $data = array('contact' => array( + 'twitter' => 'http://twitter.com/freifunk' + )); + $ret = $this->FFM->output_ff_contact($data); + $this->assertRegExp('/twitter\.com\/freifunk/', $ret); + + $data = array('contact' => array( + 'twitter' => '@freifunk' + )); + $ret = $this->FFM->output_ff_contact($data); + $this->assertRegExp('/Twitter/', $ret); + $this->assertRegExp('/twitter\.com\/freifunk/', $ret); + + $data = array('contact' => array( + 'ml' => 'mail@example.com', + 'irc' => 'irc://irc.hackint.net/example', + 'facebook' => 'freifunk', + )); + $ret = $this->FFM->output_ff_contact($data); + $this->assertRegExp('/mailto:mail@example\.com/', $ret); + $this->assertRegExp('/irc\.hackint\.net\/example/', $ret); + $this->assertRegExp('/Facebook:/', $ret); + } +} diff --git a/tests/test-WpIntegrationTests.php b/tests/test-WpIntegrationTests.php new file mode 100644 index 0000000..d41dc20 --- /dev/null +++ b/tests/test-WpIntegrationTests.php @@ -0,0 +1,70 @@ +plugin = $GLOBALS['wp-plugin-ffmeta']; + $this->plugin->reinit_external_data_service(new MockDataService()); + } + + function test_post_ff_state() { + $post_content = '[ff_state]'; + $post_attribs = array( 'post_title' => 'Test', 'post_content' => $post_content ); + $post = $this->factory->post->create_and_get( $post_attribs ); + + // w/o filter: + $this->assertEquals($post_content, $post->post_content); + + // with filter: + $output = apply_filters( 'the_content', $post->post_content ); + $this->assertEquals("
429
\n", $output); + } + + function test_post_ff_state_othercity() { + $post_content = '[ff_state ffm]'; + $post_attribs = array( 'post_title' => 'Test', 'post_content' => $post_content ); + $post = $this->factory->post->create_and_get( $post_attribs ); + $output = apply_filters( 'the_content', $post->post_content ); + + $this->assertEquals("
\n", $output); + } + + function test_post_ff_state_inv_city() { + $post_content = '[ff_state jena]'; + $post_attribs = array( 'post_title' => 'Test', 'post_content' => $post_content ); + $post = $this->factory->post->create_and_get( $post_attribs ); + $output = apply_filters( 'the_content', $post->post_content ); + + $this->assertRegExp('/