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 index d494c8c..215bc4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,11 +4,15 @@ php: - 5.3 - 5.4 - 5.5 + - 5.6 + - hhvm env: - - WP_VERSION=latest WP_MULTISITE=0 + - WP_VERSION=3.8 + - WP_VERSION=latest before_script: - - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION + - 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/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/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/freifunkmeta.php b/freifunkmeta.php index 4eac73a..8818cb7 100644 --- a/freifunkmeta.php +++ b/freifunkmeta.php @@ -3,34 +3,49 @@ Plugin Name: Freifunk Metadata Shortcodes Plugin URI: http://mschuette.name/ Description: Defines shortcodes to display Freifunk metadata -Version: 0.3 +Version: 0.4dev Author: Martin Schuette Author URI: http://mschuette.name/ */ -define('FF_META_DEFAULT_CACHETIME', 15); -define('FF_META_DEFAULT_DIR', 'https://raw.githubusercontent.com/freifunk/directory.api.freifunk.net/master/directory.json'); -define('FF_META_DEFAULT_CITY', 'hamburg'); +define( 'FF_META_DEFAULT_CACHETIME', 15 ); +define( 'FF_META_DEFAULT_DIR', 'https://raw.githubusercontent.com/freifunk/directory.api.freifunk.net/master/directory.json' ); +define( 'FF_META_DEFAULT_CITY', 'hamburg' ); /** * class to fetch and cache data from external URLs + * returns either an array from decoded JSON data, or WP_Error */ class FF_Meta_Externaldata { - public function get($url) { - /* gets metadata from URL, handles caching */ - $cachekey = 'ff_metadata_'.hash('crc32', $url); - $cachetime = get_option('ff_meta_cachetime', FF_META_DEFAULT_CACHETIME) * MINUTE_IN_SECONDS; + public function get( $url ) { + //error_log( "FF_Meta_Externaldata::get( $url )" ); + /* gets metadata from URL, handles caching, + * hashed because cache keys should be <= 40 chars */ + $cachekey = 'ff_metadata_'.hash( 'crc32', $url ); + $cachetime = get_option( + 'ff_meta_cachetime', FF_META_DEFAULT_CACHETIME + ) * MINUTE_IN_SECONDS; - // Caching - if ( false === ( $data = get_transient($cachekey) ) ) { - $http_response = wp_remote_get($url); - $json = wp_remote_retrieve_body($http_response); - $data = json_decode ($json, $assoc = true); - set_transient( $cachekey, $data, $cachetime ); - } - return $data; - } + // Caching + if ( WP_DEBUG || ( false === ( $data = get_transient( $cachekey ) ) ) ) { + $args = array( 'sslverify' => 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; + } } /** @@ -38,22 +53,42 @@ class FF_Meta_Externaldata */ class FF_Directory { - private $directory; + private $directory; + private $ed; - function __construct() { - $ed = new FF_Meta_Externaldata(); - $this->directory = $ed->get(FF_META_DEFAULT_DIR); - } + 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; + } + } - function get_url_by_city($city) { - $val = $this->directory[$city]; + function get_url_by_city( $city ) { + if ( array_key_exists( $city, $this->directory ) ) { + return $this->directory[$city]; + } else { + return false; + } + } - if (empty($val)) { - return false; - } else { - return $val; - } - } + // 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; + } } /** @@ -61,49 +96,63 @@ class FF_Directory */ class FF_Community { - public $name; - public $street; - public $zip; - public $city; - public $lon; - public $lat; + public $name; + public $street; + public $zip; + public $city; + public $lon; + public $lat; - /** - * 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'] : ''; - } + /** + * 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'] : ''; + } - /** - * Alternative constructor from city name - */ - static function make_from_city($city) { - // TODO: test - if (false === ($url = $this->dir->get_url_by_city($city))) { - return "\n"; - } - if (false === ($metadata = FF_Meta_Externaldata::get($url))) { - return "\n"; - } - return new FF_Community($metadata); - } + /** + * 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 ); - 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) . '
%s
%s
%s %s
';
- $contact = $citydata['contact'];
- // 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
\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']);
- }
- $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 .= '
Name | Stadt | Knoten |
---|---|---|
%s | %s | %s |
Data from external URLs is cached for this number of minutes.
"; - } + 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() { - if (false === ($directory = FF_Meta_Externaldata::get ( FF_META_DEFAULT_DIR ))) { - // TODO: error handling - return; - } - $default_city = get_option( 'ff_meta_city', FF_META_DEFAULT_CITY ); + 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.
"; - } + echo "'; + echo 'This is the default city parameter.
'; + } - function options_page() { - ?> -some_name
some_street
some_zip some_city