r94720 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r94719‎ | r94720 | r94721 >
Date:00:58, 17 August 2011
Author:khorn
Status:resolved (Comments)
Tags:
Comment:
In the process of adding an API to ContributionTracking, I ended up refactoring the majority of the extension.
Many changes and additions, including the new API, a jquery module that uses the API, and an unlisted sysop-only API testing page.
Modified paths:
  • /trunk/extensions/ContributionTracking/ApiContributionTracking.php (added) (history)
  • /trunk/extensions/ContributionTracking/ContributionTracking.php (modified) (history)
  • /trunk/extensions/ContributionTracking/ContributionTracking.processor.php (added) (history)
  • /trunk/extensions/ContributionTracking/ContributionTracking.sql (modified) (history)
  • /trunk/extensions/ContributionTracking/ContributionTracking_Tester.php (added) (history)
  • /trunk/extensions/ContributionTracking/ContributionTracking_body.php (modified) (history)
  • /trunk/extensions/ContributionTracking/modules (added) (history)
  • /trunk/extensions/ContributionTracking/modules/jquery.contributionTracking.js (added) (history)
  • /trunk/extensions/ContributionTracking/tests (added) (history)
  • /trunk/extensions/ContributionTracking/tests/ContributionTrackingAPITest.php (added) (history)
  • /trunk/extensions/ContributionTracking/tests/ContributionTrackingProcessorTest.php (added) (history)
  • /trunk/extensions/ContributionTracking/tests/ContributionTrackingTest.php (added) (history)

Diff [purge]

Index: trunk/extensions/ContributionTracking/ContributionTracking.sql
@@ -10,6 +10,8 @@
1111 `optout` tinyint(1) unsigned NOT NULL,
1212 `language` varchar(8) default NULL,
1313 `ts` char(14) default NULL,
 14+ `owa_session` varchar(255) default NULL,
 15+ `owa_ref` int(11) default NULL,
1416 PRIMARY KEY (`id`),
1517 UNIQUE KEY `contribution_id` (`contribution_id`),
1618 KEY `ts` (`ts`)
Index: trunk/extensions/ContributionTracking/ApiContributionTracking.php
@@ -0,0 +1,197 @@
 2+<?php
 3+
 4+/**
 5+ * This API will allow for the elimination of the interstitial page defined in
 6+ * ContributionTracking_body.php. Instead of posting contribution data to that
 7+ * page, a request to ApiContributionTracking will save contribution tracking
 8+ * data locally and prepare a set of data to be immediately reposted to the
 9+ * gateway by the original calling page. The ajax side of this is handled by
 10+ * jquery.contributionTracking.js.
 11+ * For a working example of the whole process, see
 12+ * ContributionTracking_Tester.php (must be sysop for permission).
 13+ * @author Katie Horn <khorn@wikimedia.org>
 14+ */
 15+class ApiContributionTracking extends ApiBase {
 16+
 17+ public function execute( $params = null ) {
 18+ if ( $params === null ) {
 19+ $params = $this->extractRequestParams();
 20+ }
 21+ $params = $this->getStagedParams( $params );
 22+ $contribution_tracking_id = ContributionTrackingProcessor::saveNewContribution( $params );
 23+ $this->doReturn( $contribution_tracking_id, $params );
 24+ }
 25+
 26+ /**
 27+ * Stages incoming request parameters for the ContributionTrackingProcessor
 28+ * @param array $params Incoming request parameters
 29+ * @return array Paramaters ready to be sent off to the processor.
 30+ */
 31+ function getStagedParams( $params = null ) {
 32+
 33+ foreach ( $params as $key => $value ) {
 34+ if ( $value == '' ) {
 35+ if ( $key === 'comment-option' || $key === 'email-opt' ) {
 36+ $params[$key] = false;
 37+ } else {
 38+ unset( $params[$key] ); //gotcha. And might I add: BOO-URNS.
 39+ }
 40+ }
 41+ }
 42+ return $params;
 43+ }
 44+
 45+ /**
 46+ * Assembles the data for the API to return.
 47+ * @param integer $id The Contribution Tracking ID.
 48+ * @param array $params Original (staged) request paramaters.
 49+ */
 50+ function doReturn( $id, $params ) {
 51+// foreach ($params as $key=>$value){
 52+// if ($value != ''){
 53+// $this->getResult()->addValue(array('returns', 'parrot'), $key, $value);
 54+// }
 55+// }
 56+ $params['contribution_tracking_id'] = $id;
 57+
 58+ $repost = ContributionTrackingProcessor::getRepostFields( $params );
 59+
 60+ $this->getResult()->addValue( array( 'returns', 'action' ), 'url', $repost['action'] );
 61+ foreach ( $repost['fields'] as $key => $value ) {
 62+ $this->getResult()->addValue( array( 'returns', 'fields' ), $key, $value );
 63+ }
 64+ }
 65+
 66+ /**
 67+ *
 68+ * @return array An array of parameters allowed by ApiContributionTracking
 69+ */
 70+ public function getAllowedParams() {
 71+ return array(
 72+ 'amount' => array(
 73+ ApiBase::PARAM_TYPE => 'string',
 74+ ApiBase::PARAM_REQUIRED => true,
 75+ ),
 76+ 'referrer' => array(
 77+ ApiBase::PARAM_TYPE => 'string',
 78+ ApiBase::PARAM_REQUIRED => true,
 79+ ),
 80+ 'gateway' => array(
 81+ ApiBase::PARAM_TYPE => 'string',
 82+ ApiBase::PARAM_REQUIRED => true,
 83+ ),
 84+ 'comment' => array(
 85+ ApiBase::PARAM_TYPE => 'string',
 86+ ),
 87+ 'comment-option' => array(
 88+ ApiBase::PARAM_TYPE => 'boolean',
 89+ ),
 90+ 'utm_source' => array(
 91+ ApiBase::PARAM_TYPE => 'string',
 92+ ),
 93+ 'utm_medium' => array(
 94+ ApiBase::PARAM_TYPE => 'string',
 95+ ),
 96+ 'utm_campaign' => array(
 97+ ApiBase::PARAM_TYPE => 'string',
 98+ ),
 99+ 'email-opt' => array(
 100+ ApiBase::PARAM_TYPE => 'boolean',
 101+ ),
 102+ 'language' => array(
 103+ ApiBase::PARAM_TYPE => 'string',
 104+ ),
 105+ 'owa_session' => array(
 106+ ApiBase::PARAM_TYPE => 'string',
 107+ ),
 108+ 'owa_ref' => array(
 109+ ApiBase::PARAM_TYPE => 'string',
 110+ ),
 111+ 'contribution_tracking_id' => array(
 112+ ApiBase::PARAM_TYPE => 'string',
 113+ ),
 114+ 'returnto' => array(
 115+ ApiBase::PARAM_TYPE => 'string',
 116+ ),
 117+ 'tshirt' => array(
 118+ ApiBase::PARAM_TYPE => 'boolean',
 119+ ),
 120+ 'size' => array(
 121+ ApiBase::PARAM_TYPE => 'string',
 122+ ),
 123+ 'premium_language' => array(
 124+ ApiBase::PARAM_TYPE => 'string',
 125+ ),
 126+ 'currency_code' => array(
 127+ ApiBase::PARAM_TYPE => 'string',
 128+ ),
 129+ 'fname' => array(
 130+ ApiBase::PARAM_TYPE => 'string',
 131+ ),
 132+ 'lname' => array(
 133+ ApiBase::PARAM_TYPE => 'string',
 134+ ),
 135+ 'email' => array(
 136+ ApiBase::PARAM_TYPE => 'string',
 137+ ),
 138+ 'recurring_paypal' => array(
 139+ ApiBase::PARAM_TYPE => 'boolean',
 140+ ),
 141+ 'amountGiven' => array(
 142+ ApiBase::PARAM_TYPE => 'string',
 143+ ),
 144+ );
 145+ }
 146+
 147+ public function getParamDescription() {
 148+ return array(
 149+ 'amount' => 'Transaction amount (required)',
 150+ 'referrer' => 'String identifying the referring entity (required)',
 151+ 'gateway' => array(
 152+ 'String identifying the specific entity used to process this payment. ',
 153+ 'Probably "paypal". (required)' ),
 154+ 'comment' => 'String with a comment. Actually saved as "note" in the database',
 155+ 'comment-option' => 'Boolean assumed to be from a checkbox. This is actually the inverse of the anonymous flag.',
 156+ 'utm_source' => 'String identifying "utm_source"',
 157+ 'utm_medium' => 'String identifying "utm_medium"',
 158+ 'utm_campaign' => 'String identifying "utm_campaign"',
 159+ 'email-opt' => 'Boolean assumed to be from a checkbox. This is actually the inverse of the E-mail opt-out checkbox.',
 160+ 'language' => array(
 161+ 'User language code. Messages will be translated appropriately (where possible).',
 162+ 'This will also determine what "Thank You" page the user sees upon completion of a donation at the gateway.' ),
 163+ 'owa_session' => 'String identifying the "owa_session"',
 164+ 'owa_ref' => 'String with the referring URL.',
 165+ 'contribution_tracking_id' => 'Our ID for the current contribution. Not supplied for new contributions.', //in fact, why is this here?
 166+ 'returnto' => 'String identifying an alternate "Thank You" page to show the user on completion of their transaction.',
 167+ 'tshirt' => 'Boolean indicating whether or not there is a t-shirt involved.',
 168+ 'size' => 'String indicating the desired size of the above t-shirt (if involved)',
 169+ 'premium_language' => 'Language code for the shirt. This will have no effect on message translation outside of the physical scope of the shirt.',
 170+ 'currency_code' => 'Currency code for the current transaction.',
 171+ 'fname' => "String: Donor's first name",
 172+ 'lname' => "String: Donor's last name",
 173+ 'email' => "String: Donor's email",
 174+ 'recurring_paypal' => 'Boolean identifying a recurring donation. Do not supply at all for a one-time donation.',
 175+ 'amountGiven' => 'Normalized amount.'
 176+ );
 177+ }
 178+
 179+ public function getDescription() {
 180+ return array(
 181+ 'Track donor contributions via API',
 182+ 'This API exists so we are able to eliminate the interstitial page',
 183+ 'that would otherwise be used to track contributions before sending',
 184+ 'the donor off to paypal (or wherever).',
 185+ );
 186+ }
 187+
 188+ public function getExamples() {
 189+ return array(
 190+ 'api.php?action=contributiontracking&comment=examplecomment&referrer=examplereferrer&gateway=paypal&amount=5.50',
 191+ );
 192+ }
 193+
 194+ public function getVersion() {
 195+ return "1.0"; //Is this really _that_ arbitrary?
 196+ }
 197+
 198+}
Property changes on: trunk/extensions/ContributionTracking/ApiContributionTracking.php
___________________________________________________________________
Added: svn:eol-style
1199 + native
Index: trunk/extensions/ContributionTracking/tests/ContributionTrackingAPITest.php
@@ -0,0 +1,285 @@
 2+<?php
 3+
 4+/**
 5+ * Yes, I realize this whole test class is full of things that are more
 6+ * regression run by phpunit, than actual unit tests. For the sake of coverage,
 7+ * it's going to stay that way until we can completely refactor
 8+ * ContributionTracking_body.php (beyond splitting its newly-shared
 9+ * functionality out into something the new API can also reach).
 10+ * //TODO: Refactor ContributionTracking_body.php, and clean up this whole mess.
 11+ * //TODO: Add tests to make sure that garbage requests fail gracefully.
 12+ *
 13+ * //FIXME: Yes, this test class and ContributionTrackingTest are nearly exactly
 14+ * the same. They should probably be combined into a thing that tests both entry
 15+ * methods simultaneously with the same requests.
 16+ * @group Fundraising
 17+ * @group Splunge
 18+ * @author Katie Horn <khorn@wikimedia.org>
 19+ */
 20+class ContributionTrackingAPITest extends MediaWikiTestCase {
 21+
 22+ /**
 23+ * Takes $request parameters and checks them against $expected parameters in
 24+ * the data about to be returned by ApiContributionTracking.
 25+ * All assert failures will start with the $message_prefix so we know which
 26+ * test actually failed.
 27+ * @param array $request The request parameters
 28+ * @param array $expected Expected contents of the hidden form about to be
 29+ * reposted to the gateway.
 30+ * @param string $message_prefix A readable string that identifies the test
 31+ * on failed assert.
 32+ */
 33+ function assertExecute_responseAsExpected( $request, $expected, $message_prefix ) {
 34+ $result = $this->getAPIResultData( $request );
 35+ $result = $result['returns'];
 36+
 37+ $reposters = array( );
 38+
 39+ foreach ( $expected['fields'] as $name => $value ) {
 40+ if ( $name === 'custom' ) {
 41+ $this->assertTrue( is_numeric( $result['fields'][$name] ), $message_prefix . ": 'custom' should be a number." );
 42+ } elseif ( $name === 'item_name' && array_key_exists( 'language', $request ) && $request['language'] !== 'en' ) {
 43+ //TODO: Actually deal with the encoding mismatch here. Urgh.
 44+ $this->assertTrue( ($result['fields'][$name] != 'One-time Donation' ), $message_prefix . ": Alternate language is returning English strings by the API." );
 45+ } else {
 46+ $this->assertEquals( $value, $result['fields'][$name], $message_prefix . ": Field $name was not reposted as expected by the API" );
 47+ }
 48+ }
 49+
 50+ //and don't forget to check it's the proper action!
 51+ $this->assertEquals( $result['action']['url'], $expected['action'], $message_prefix . ": Contribution Tracking API form action was incorrect." );
 52+ }
 53+
 54+ /**
 55+ * Gets ApiContributionTracking's response in array format, for the given
 56+ * $request params.
 57+ * @global FauxRequest $wgRequest used to shoehorn in our own request vars.
 58+ * @param <type> $request Request vars we are sending to
 59+ * ApiContributionTracking.
 60+ * @return array Values to be returned by ApiContributionTracking
 61+ */
 62+ function getAPIResultData( $request ) {
 63+ global $wgRequest;
 64+ $request['format'] = 'xml';
 65+ $request['action'] = 'contributiontracking';
 66+ $wgRequest = new FauxRequest( $request );
 67+
 68+ $ctapi = new ApiMain( $wgRequest, true );
 69+ $ctapi->execute();
 70+ $api_response = $ctapi->getResult()->getData();
 71+ return $api_response;
 72+ }
 73+
 74+ /**
 75+ * Sets up a bare-bones request to send to ApiContributionTracking, and the
 76+ * values we expect to see in the response. Then calls
 77+ * assertExecute_responseAsExpected for the actual
 78+ * processing and assertions.
 79+ */
 80+ function testExecuteforRepostFields_minimal() {
 81+ $minimal = array(
 82+ 'referrer' => 'phpunit_api',
 83+ 'gateway' => 'paypal',
 84+ 'amount' => '8.80'
 85+ );
 86+
 87+ $returnTitle = Title::newFromText( 'Donate-thanks/en' );
 88+ $expected = array(
 89+ 'action' => 'https://www.paypal.com/cgi-bin/webscr',
 90+ 'fields' => array(
 91+ 'business' => 'donations@wikimedia.org',
 92+ 'item_number' => 'DONATE',
 93+ 'no_note' => 0,
 94+ 'return' => $returnTitle->getFullUrl(),
 95+ 'currency_code' => 'USD',
 96+ 'cmd' => '_xclick',
 97+ 'notify_url' => 'https://civicrm.wikimedia.org/fundcore_gateway/paypal',
 98+ 'item_name' => 'One-time donation',
 99+ 'amount' => '8.80',
 100+ 'custom' => '', //this is overridden later. Should be the id of the inserted transaction.
 101+ )
 102+ );
 103+
 104+ $this->assertExecute_responseAsExpected( $minimal, $expected, "Minimal Repost Test" );
 105+ }
 106+
 107+ /**
 108+ * Sets up a recurring payment type request to send to
 109+ * ApiContributionTracking, and the values we expect to see in the response
 110+ * after processing. Then calls assertExecute_responseAsExpected for the
 111+ * actual processing and assertions.
 112+ */
 113+ function testExecuteforRepostFields_recurring() {
 114+ //test paypal recurring
 115+ $recurring = array(
 116+ 'referrer' => 'phpunit_api',
 117+ 'gateway' => 'paypal',
 118+ 'amount' => '8.80',
 119+ 'recurring_paypal' => true
 120+ );
 121+ $returnTitle = Title::newFromText( 'Donate-thanks/en' );
 122+ $expected = array(
 123+ 'action' => 'https://www.paypal.com/cgi-bin/webscr',
 124+ 'fields' => array(
 125+ 'business' => 'donations@wikimedia.org',
 126+ 'item_number' => 'DONATE',
 127+ 'no_note' => 0,
 128+ 'return' => $returnTitle->getFullUrl(),
 129+ 'currency_code' => 'USD',
 130+ 'cmd' => '_xclick',
 131+ 'notify_url' => 'https://civicrm.wikimedia.org/fundcore_gateway/paypal',
 132+ 'item_name' => 'One-time donation',
 133+ 'a3' => '8.80',
 134+ 'custom' => '', //this is overridden later. Should be the id of the inserted transaction.
 135+ 't3' => 'M',
 136+ 'p3' => '1',
 137+ 'srt' => '12',
 138+ 'src' => '1',
 139+ 'sra' => '1',
 140+ 'cmd' => '_xclick-subscriptions',
 141+ 'item_name' => 'Recurring monthly donation',
 142+ )
 143+ );
 144+
 145+
 146+ $this->assertExecute_responseAsExpected( $recurring, $expected, "Paypal Recurring Test" );
 147+ }
 148+
 149+ /**
 150+ * Sets up a non-english request (in a language that has a translation) to
 151+ * send to ApiContributionTracking, and the values we expect to see in the
 152+ * response after processing. Then calls
 153+ * assertExecute_responseAsExpected for the actual processing and
 154+ * assertions.
 155+ * FIXME: something about the encoding makes this not work as expected.
 156+ */
 157+ function testExecuteforRepostFields_language() {
 158+
 159+ //test alternate language
 160+ $language = array(
 161+ 'referrer' => 'phpunit_api',
 162+ 'gateway' => 'paypal',
 163+ 'amount' => '8.80',
 164+ 'language' => 'ja'
 165+ );
 166+
 167+
 168+ $returnTitle = Title::newFromText( 'Donate-thanks/ja' );
 169+ $expected = array(
 170+ 'action' => 'https://www.paypal.com/cgi-bin/webscr',
 171+ 'fields' => array(
 172+ 'business' => 'donations@wikimedia.org',
 173+ 'item_number' => 'DONATE',
 174+ 'no_note' => 0,
 175+ 'return' => $returnTitle->getFullUrl(), //Important to the language test.
 176+ 'currency_code' => 'USD',
 177+ 'cmd' => '_xclick',
 178+ 'notify_url' => 'https://civicrm.wikimedia.org/fundcore_gateway/paypal',
 179+ 'item_name' => '1回だけ寄付', //This should be translated.
 180+ 'amount' => '8.80',
 181+ 'custom' => '',
 182+ )
 183+ );
 184+
 185+ $this->assertExecute_responseAsExpected( $language, $expected, "Translation Test" );
 186+ }
 187+
 188+ /**
 189+ * Sets up a "premium" request to send to ApiContributionTracking, and the
 190+ * values we expect to see in the response after processing. Then calls
 191+ * assertExecute_responseAsExpected for the actual processing and assertions.
 192+ */
 193+ function testExecuteforRepostFields_tshirts() {
 194+
 195+ //test T-shirtness
 196+ $tshirts = array(
 197+ 'referrer' => 'phpunit_api',
 198+ 'gateway' => 'paypal',
 199+ 'amount' => '8.80',
 200+ 'language' => 'en',
 201+ 'tshirt' => 'true',
 202+ 'size' => 'medium',
 203+ 'premium_language' => 'ja'
 204+ );
 205+
 206+ $returnTitle = Title::newFromText( 'Donate-thanks/en' );
 207+ $expected = array(
 208+ 'action' => 'https://www.paypal.com/cgi-bin/webscr',
 209+ 'fields' => array(
 210+ 'business' => 'donations@wikimedia.org',
 211+ 'item_number' => 'DONATE',
 212+ 'no_note' => 0,
 213+ 'return' => $returnTitle->getFullUrl(),
 214+ 'currency_code' => 'USD',
 215+ 'cmd' => '_xclick',
 216+ 'notify_url' => 'https://civicrm.wikimedia.org/fundcore_gateway/paypal',
 217+ 'item_name' => 'One-time donation',
 218+ 'amount' => '8.80',
 219+ 'custom' => '',
 220+ 'on0' => 'Shirt size',
 221+ 'os0' => 'medium',
 222+ 'on1' => 'Shirt language',
 223+ 'os1' => 'ja',
 224+ 'no_shipping' => 2
 225+ )
 226+ );
 227+
 228+ $this->assertExecute_responseAsExpected( $tshirts, $expected, "T-shirt Test" );
 229+ }
 230+
 231+ /**
 232+ * Tests to make sure the contribution was saved in the database properly.
 233+ * Assertions:
 234+ * The saved contribution ID is set to be reposted to paypal
 235+ * Each parameter saved to the contribution_tracking table is identical
 236+ * to the value we were trying to save, in the row matching the ID passed to
 237+ * paypal
 238+ * The owa_ref URL value is stored in the owa_ref table, and referenced
 239+ * by the correct id in the owa_ref column
 240+ *
 241+ */
 242+ function testExecuteForContributionSave() {
 243+ //TODO: Test inserting pure garbage.
 244+ $complete = array(
 245+ 'comment' => 'Interstitial Save',
 246+ 'referrer' => 'phpunit_api',
 247+ 'comment-option' => 'yep',
 248+ 'utm_source' => 'here',
 249+ 'utm_medium' => 'large',
 250+ 'utm_campaign' => 'testy01',
 251+ 'language' => 'en',
 252+ 'owa_session' => 'foo2',
 253+ 'owa_ref' => 'execute_save',
 254+ 'gateway' => 'paypal',
 255+ 'amount' => '6.60'
 256+ );
 257+ $table1_check = $complete;
 258+ $table1_check['anonymous'] = 0;
 259+ $table1_check['optout'] = 1;
 260+ $table1_check['note'] = $complete['comment'];
 261+ unset( $table1_check['owa_ref'] );
 262+ unset( $table1_check['comment'] );
 263+ unset( $table1_check['comment-option'] );
 264+ unset( $table1_check['gateway'] );
 265+ unset( $table1_check['amount'] );
 266+
 267+ $result = $this->getAPIResultData( $complete );
 268+ $result = $result['returns']['fields'];
 269+
 270+ //We're using paypal, one-time, so the ID will come back in the hidden "custom" field
 271+ $this->assertTrue( is_numeric( $result['custom'] ), "The saved transaction ID was not found" );
 272+
 273+ $db = ContributionTrackingProcessor::contributionTrackingConnection();
 274+ $row = $db->selectRow( 'contribution_tracking', '*', array( 'id' => $result['custom'] ) );
 275+
 276+ foreach ( $table1_check as $key => $value ) {
 277+ $this->assertEquals( $value, $row->$key, "$key does not match in the database." );
 278+ }
 279+
 280+ $row = $db->selectRow( 'contribution_tracking_owa_ref', '*', array( 'id' => $row->owa_ref ) );
 281+ $this->assertEquals( $complete['owa_ref'], $row->url, "OWA Reference lookup does not match" );
 282+ }
 283+
 284+}
 285+
 286+?>
Property changes on: trunk/extensions/ContributionTracking/tests/ContributionTrackingAPITest.php
___________________________________________________________________
Added: svn:eol-style
1287 + native
Index: trunk/extensions/ContributionTracking/tests/ContributionTrackingProcessorTest.php
@@ -0,0 +1,429 @@
 2+<?php
 3+
 4+/**
 5+ * Tests for the ContributionTrackingProcessor class. This class is used by both
 6+ * the interstitial page and the API to process donation requests, determine
 7+ * where the donor should be sent next, and send them there with all the
 8+ * required information in a format accepted by the gateway.
 9+ * @group Fundraising
 10+ * @group Splunge
 11+ * @author Katie Horn <khorn@wikimedia.org>
 12+ */
 13+class ContributionTrackingProcessorTest extends MediaWikiTestCase {
 14+
 15+ /**
 16+ * tests the rekey function in the ContributionTrackingProcessor.
 17+ */
 18+ function testRekey() {
 19+ $start = array(
 20+ 'bears' => 'green',
 21+ 'emus' => 'purple'
 22+ );
 23+ $expected = array(
 24+ 'llamas' => 'green',
 25+ 'emus' => 'purple'
 26+ );
 27+
 28+ ContributionTrackingProcessor::rekey( $start, 'bears', 'llamas' );
 29+
 30+ $this->assertEquals( $start, $expected, "Rekey is not working as expected." );
 31+ }
 32+
 33+ /**
 34+ * Tests the stage_checkbox function
 35+ * $start coming out as $expected will tell us that it works as expected
 36+ * with both existing and non-existant keys
 37+ */
 38+ function testStageCheckbox() {
 39+ $start = array(
 40+ 'bears' => 'green',
 41+ 'emus' => 'purple'
 42+ );
 43+ $expected = array(
 44+ 'bears' => 1,
 45+ 'emus' => 'purple'
 46+ );
 47+
 48+ ContributionTrackingProcessor::stage_checkbox( $start, 'bears' );
 49+ ContributionTrackingProcessor::stage_checkbox( $start, 'llamas' );
 50+
 51+ $this->assertEquals( $start, $expected, "stage_checkbox is not working as expected." );
 52+ }
 53+
 54+ /**
 55+ * tests the stage_contribution function, and as a by-product,
 56+ * the getContributionDefaults function as well.
 57+ * Asserts that:
 58+ * A staged contribution with no relevant fields will come back equal
 59+ * to exactly the defaults
 60+ * A staged contribution with some relevant fields will come back as
 61+ * the defaults, with keys overwritten by the supplied fields where they
 62+ * exist
 63+ * A staged contribution with some relevant fields and some irrelevant
 64+ * fields will come back as the defaults, with relevant keys overwritten by
 65+ * the supplied fields where they exist. The irrelevant fields should not
 66+ * come back at all.
 67+ * A staged contribution with boolean (checkbox) fields will come back
 68+ * with those values either set to "1" or "0", depending solely on whether
 69+ * they exist in the supplied parameters or not.
 70+ */
 71+ function testStageContribution() {
 72+ $start = array(
 73+ 'bears' => 'green',
 74+ 'emus' => 'purple'
 75+ );
 76+ $expected = ContributionTrackingProcessor::getContributionDefaults();
 77+ $result = ContributionTrackingProcessor::stage_contribution( $start );
 78+ $this->assertEquals( $expected, $result, "Staged Contribution with no defined fields should be exactly all the default values." );
 79+
 80+ $additional = array(
 81+ 'note' => 'B Flat',
 82+ 'referrer' => 'phpunit_processor',
 83+ 'anonymous' => 'Raspberries'
 84+ );
 85+
 86+ $expected = array(
 87+ 'note' => 'B Flat',
 88+ 'referrer' => 'phpunit_processor',
 89+ 'anonymous' => 1,
 90+ 'utm_source' => null,
 91+ 'utm_medium' => null,
 92+ 'utm_campaign' => null,
 93+ 'optout' => 0,
 94+ 'language' => null,
 95+ 'owa_session' => null,
 96+ 'owa_ref' => null,
 97+ 'ts' => null,
 98+ );
 99+ $result = ContributionTrackingProcessor::stage_contribution( $additional );
 100+ $this->assertEquals( $expected, $result, "Contribution not staging properly." );
 101+
 102+ $start = array_merge( $start, $additional );
 103+ $result = ContributionTrackingProcessor::stage_contribution( $start );
 104+ $this->assertEquals( $expected, $result, "Contribution not staging properly." );
 105+
 106+
 107+ $complete = array(
 108+ 'note' => 'Batman',
 109+ 'referrer' => 'phpunit_processor',
 110+ 'anonymous' => 'of course',
 111+ 'utm_source' => 'batcave',
 112+ 'utm_medium' => 'Alfred',
 113+ 'utm_campaign' => 'Joker',
 114+ 'language' => 'squeak!',
 115+ 'owa_session' => 'arghargh',
 116+ 'owa_ref' => 'test',
 117+ 'ts' => '11235813'
 118+ );
 119+
 120+ $expected = $complete;
 121+ $expected['anonymous'] = 1;
 122+ $expected['optout'] = 0;
 123+
 124+ $result = ContributionTrackingProcessor::stage_contribution( $complete );
 125+ $this->assertEquals( $expected, $result, "Contribution not staging properly." );
 126+ }
 127+
 128+ /**
 129+ * Tests saveNewContribution()
 130+ * Assertions:
 131+ * saveNewContributions returns a number.
 132+ * Each parameter saved to the contribution_tracking table is identical
 133+ * to the value we were trying to save, in the row matching the ID returned
 134+ * from saveNewContribution.
 135+ * The owa_ref URL value is stored in the owa_ref table, and referenced
 136+ * by the correct id in the owa_ref column
 137+ *
 138+ */
 139+ function testSaveNewContribution() {
 140+ //TODO: Test inserting pure garbage.
 141+ $complete = array(
 142+ 'note' => 'Batman is pretty awesome.',
 143+ 'referrer' => 'phpunit_processor',
 144+ 'anonymous' => 'of course',
 145+ 'utm_source' => 'batcave',
 146+ 'utm_medium' => 'Alfred',
 147+ 'utm_campaign' => 'Joker',
 148+ 'language' => 'squeak!',
 149+ 'owa_session' => 'arghargh',
 150+ 'owa_ref' => 'test'
 151+ );
 152+ $table1_check = $complete;
 153+ $table1_check['anonymous'] = 1;
 154+ $table1_check['optout'] = 0;
 155+ unset( $table1_check['owa_ref'] );
 156+
 157+ $id = ContributionTrackingProcessor::saveNewContribution( $complete );
 158+ $this->assertTrue( is_numeric( $id ), "Returned value is not an ID." );
 159+
 160+ $db = ContributionTrackingProcessor::contributionTrackingConnection();
 161+ $row = $db->selectRow( 'contribution_tracking', '*', array( 'id' => $id ) );
 162+
 163+ foreach ( $table1_check as $key => $value ) {
 164+ $this->assertEquals( $value, $row->$key, "$key does not match in the database." );
 165+ }
 166+
 167+ $row = $db->selectRow( 'contribution_tracking_owa_ref', '*', array( 'id' => $row->owa_ref ) );
 168+ $this->assertEquals( $complete['owa_ref'], $row->url, "OWA Reference lookup does not match" );
 169+ }
 170+
 171+ /**
 172+ * tests the getRepostFields function.
 173+ * Assertions:
 174+ * getRepostFields returns an array.
 175+ * getRepostFields returns expected fields for a one-time paypal
 176+ * donation.
 177+ * getRepostFields returns expected fields for a one-time paypal
 178+ * donation.
 179+ * getRepostFields returns expected fields for a one-time paypal
 180+ * donation.
 181+ * getRepostFields returns translated fields (when they will be
 182+ * displayed by the gateway) and return-to's for the specified language.
 183+ *
 184+ */
 185+ function testGetRepostFields() {
 186+ //TODO: More here.
 187+ $minimal = array(
 188+ 'referrer' => 'phpunit_processor',
 189+ 'gateway' => 'paypal',
 190+ 'amount' => '8.80'
 191+ );
 192+
 193+ $returnTitle = Title::newFromText( 'Donate-thanks/en' );
 194+ $expected = array(
 195+ 'action' => 'https://www.paypal.com/cgi-bin/webscr',
 196+ 'fields' => array(
 197+ 'business' => 'donations@wikimedia.org',
 198+ 'item_number' => 'DONATE',
 199+ 'no_note' => 0,
 200+ 'return' => $returnTitle->getFullUrl(),
 201+ 'currency_code' => 'USD',
 202+ 'cmd' => '_xclick',
 203+ 'notify_url' => 'https://civicrm.wikimedia.org/fundcore_gateway/paypal',
 204+ 'item_name' => 'One-time donation',
 205+ 'amount' => '8.80',
 206+ 'custom' => '',
 207+ )
 208+ );
 209+
 210+ $ret = ContributionTrackingProcessor::getRepostFields( $minimal );
 211+ $this->assertTrue( is_array( $ret ), "Returned value is not an array" );
 212+
 213+ $this->assertEquals( $ret, $expected, "Fields for reposting (Paypal, one-time) do not match expected fields" );
 214+
 215+ //test paypal recurring
 216+ $minimal['recurring_paypal'] = true;
 217+ $expected['fields']['t3'] = 'M';
 218+ $expected['fields']['p3'] = '1';
 219+ $expected['fields']['srt'] = '12';
 220+ $expected['fields']['src'] = '1';
 221+ $expected['fields']['sra'] = '1';
 222+ $expected['fields']['cmd'] = '_xclick-subscriptions';
 223+ $expected['fields']['item_name'] = 'Recurring monthly donation';
 224+ $expected['fields']['a3'] = '8.80';
 225+ unset( $expected['fields']['amount'] );
 226+
 227+
 228+ $ret = ContributionTrackingProcessor::getRepostFields( $minimal );
 229+ $this->assertEquals( $ret, $expected, "Fields for reposting (Paypal, recurring) do not match expected fields" );
 230+
 231+
 232+ //test moneybookers... just in case anybody cares anymore.
 233+ unset( $minimal['recurring_paypal'] );
 234+ $minimal['gateway'] = 'moneybookers';
 235+
 236+ $expected = array(
 237+ 'action' => 'https://www.moneybookers.com/app/payment.pl',
 238+ 'fields' => Array
 239+ (
 240+ 'merchant_fields' => 'os0',
 241+ 'pay_to_email' => 'donation@wikipedia.org',
 242+ 'status_url' => 'https://civicrm.wikimedia.org/fundcore_gateway/moneybookers',
 243+ 'language' => 'en',
 244+ 'detail1_description' => 'One-time donation',
 245+ 'detail1_text' => 'DONATE',
 246+ 'currency' => 'USD',
 247+ 'amount' => '8.80',
 248+ 'custom' => '',
 249+ )
 250+ );
 251+ $ret = ContributionTrackingProcessor::getRepostFields( $minimal );
 252+ $this->assertEquals( $ret, $expected, "Fields for reposting (moneybookers, one-time) do not match expected fields" );
 253+
 254+ //test alternate language
 255+ $minimal['gateway'] = 'paypal';
 256+ $minimal['language'] = 'ja'; //japanese.
 257+
 258+ $returnTitle = Title::newFromText( 'Donate-thanks/ja' );
 259+ $expected = array(
 260+ 'action' => 'https://www.paypal.com/cgi-bin/webscr',
 261+ 'fields' => array(
 262+ 'business' => 'donations@wikimedia.org',
 263+ 'item_number' => 'DONATE',
 264+ 'no_note' => 0,
 265+ 'return' => $returnTitle->getFullURL(), //Important to the language test.
 266+ 'currency_code' => 'USD',
 267+ 'cmd' => '_xclick',
 268+ 'notify_url' => 'https://civicrm.wikimedia.org/fundcore_gateway/paypal',
 269+ 'item_name' => '1回だけ寄付', //This should be translated.
 270+ 'amount' => '8.80',
 271+ 'custom' => '',
 272+ )
 273+ );
 274+
 275+ $ret = ContributionTrackingProcessor::getRepostFields( $minimal );
 276+ $this->assertEquals( $ret, $expected, "Fields for reposting (paypal, one-time, language=ja) do not match expected fields" );
 277+
 278+ //test T-shirtness
 279+ $minimal['gateway'] = 'paypal';
 280+ $minimal['language'] = 'en';
 281+ $minimal['tshirt'] = true;
 282+ $minimal['size'] = 'medium';
 283+ $minimal['premium_language'] = 'ja';
 284+
 285+ $returnTitle = Title::newFromText( 'Donate-thanks/en' );
 286+ $expected = array(
 287+ 'action' => 'https://www.paypal.com/cgi-bin/webscr',
 288+ 'fields' => array(
 289+ 'business' => 'donations@wikimedia.org',
 290+ 'item_number' => 'DONATE',
 291+ 'no_note' => 0,
 292+ 'return' => $returnTitle->getFullURL(),
 293+ 'currency_code' => 'USD',
 294+ 'cmd' => '_xclick',
 295+ 'notify_url' => 'https://civicrm.wikimedia.org/fundcore_gateway/paypal',
 296+ 'item_name' => 'One-time donation',
 297+ 'amount' => '8.80',
 298+ 'custom' => '',
 299+ 'on0' => 'Shirt size',
 300+ 'os0' => 'medium',
 301+ 'on1' => 'Shirt language',
 302+ 'os1' => 'ja',
 303+ 'no_shipping' => 2
 304+ )
 305+ );
 306+
 307+ $ret = ContributionTrackingProcessor::getRepostFields( $minimal );
 308+ $this->assertEquals( $ret, $expected, "Fields for reposting (paypal, one-time, T-shirt) do not match expected fields" );
 309+ }
 310+
 311+ /**
 312+ * tests the stage_repost function
 313+ * Assertions:
 314+ * Garbage in, defaults out.
 315+ * The recurring_paypal key is treated like a boolean
 316+ */
 317+ function testStageRepost() {
 318+ $start = array(
 319+ 'bears' => 'green',
 320+ 'emus' => 'purple'
 321+ );
 322+ ContributionTrackingProcessor::getLanguage( array( 'language' => 'en' ) );
 323+ $expected = ContributionTrackingProcessor::getRepostDefaults();
 324+ $expected['item_name'] = 'One-time donation';
 325+ $expected['notify_url'] = 'https://civicrm.wikimedia.org/fundcore_gateway/paypal';
 326+
 327+ $result = ContributionTrackingProcessor::stage_repost( $start );
 328+ $this->assertEquals( $expected, $result, "Staged Repost with no defined fields should be exactly all the default values." );
 329+
 330+ $additional = array(
 331+ 'gateway' => 'testgateway',
 332+ 'recurring_paypal' => 'raspberries',
 333+ 'amount' => '6.60'
 334+ );
 335+
 336+ $expected = array(
 337+ 'gateway' => 'testgateway',
 338+ 'tshirt' => false,
 339+ 'size' => false,
 340+ 'premium_language' => false,
 341+ 'currency_code' => 'USD',
 342+ 'return' => 'Donate-thanks/en',
 343+ 'fname' => '',
 344+ 'lname' => '',
 345+ 'email' => '',
 346+ 'recurring_paypal' => '1',
 347+ 'amount' => '6.60',
 348+ 'amount_given' => '',
 349+ 'contribution_tracking_id' => '',
 350+ 'notify_url' => 'https://civicrm.wikimedia.org/fundcore_gateway/paypal',
 351+ 'item_name' => 'Recurring monthly donation'
 352+ );
 353+ $result = ContributionTrackingProcessor::stage_repost( $additional );
 354+ $this->assertEquals( $expected, $result, "Repost not staging properly." );
 355+
 356+
 357+ unset( $additional['recurring_paypal'] );
 358+ $expected['recurring_paypal'] = 0;
 359+ $expected['item_name'] = 'One-time donation';
 360+ $result = ContributionTrackingProcessor::stage_repost( $additional );
 361+ $this->assertEquals( $expected, $result, "Repost not staging properly." );
 362+ }
 363+
 364+ /**
 365+ * tests the get_owa_ref_id function
 366+ * Assertions:
 367+ * The unique add comes back with a numeric id.
 368+ * The second call also comes back with a numeric id.
 369+ * The insert and the lookup come back with the same numeric id.
 370+ */
 371+ function testGetOWARefID() {
 372+ $testRef = "test_ref_" . time();
 373+ $id_1 = ContributionTrackingProcessor::get_owa_ref_id( $testRef ); //add
 374+ $id_2 = ContributionTrackingProcessor::get_owa_ref_id( $testRef ); //get
 375+ $this->assertTrue( is_numeric( $id_1 ), "First id is not numeric: Problem adding OWA Ref URL" );
 376+ $this->assertTrue( is_numeric( $id_2 ), "Second id is not numeric: Problem retrieving OWA Ref ID" );
 377+ $this->assertEquals( $id_1, $id_2, "IDs do not match." );
 378+ }
 379+
 380+ /**
 381+ * tests the getLanguage function.
 382+ * NOTE: Static vars are involved here.
 383+ * Assertions:
 384+ * getLanguage with no parameters returns english (if none of the
 385+ * previous tests set the var differently. Static vars have tricky initial
 386+ * conditions...)
 387+ * Passing getLanguage a different language than the one previously in
 388+ * use will cause the var to reset to the explicit language. Messages should
 389+ * be sent in the new language.
 390+ */
 391+ function testGetLanguage() {
 392+ $messageKey = 'contributiontracking';
 393+ $messageBG = 'Проследяване на дарението';
 394+ $messageEN = 'Contribution tracking';
 395+
 396+ $code = ContributionTrackingProcessor::getLanguage();
 397+ $this->assertEquals( $code, 'en', "Default language is not US (or your test has a hangover)" );
 398+
 399+ $params['language'] = 'bg';
 400+ $code = ContributionTrackingProcessor::getLanguage( $params );
 401+ $this->assertEquals( $params['language'], $code, "Returned language is not the one we just sent." );
 402+ $message = ContributionTrackingProcessor::msg( $messageKey );
 403+ $this->assertEquals( $message, $messageBG, "Returned language is not the one we just sent." );
 404+
 405+ $params['language'] = 'en';
 406+ $code = ContributionTrackingProcessor::getLanguage( $params );
 407+ $this->assertEquals( $params['language'], $code, "Returned language is not the one we just sent." );
 408+ $message = ContributionTrackingProcessor::msg( $messageKey );
 409+ $this->assertEquals( $message, $messageEN, "Returned language is not the one we just sent." );
 410+ }
 411+
 412+ /**
 413+ * Helper function that recursively sorts arrays by key. Nice for debugging
 414+ * failed assertEquals, where you're comparing large arrays.
 415+ * @param array $array The array you want to recursively ksort.
 416+ * @return array The ksorted array.
 417+ */
 418+ function deepKSort( $array ) {
 419+ foreach ( $array as $key => $value ) {
 420+ if ( is_array( $value ) ) {
 421+ $array[$key] = $this->deepKSort( $value );
 422+ }
 423+ }
 424+ ksort( $array );
 425+ return $array;
 426+ }
 427+
 428+}
 429+
 430+?>
Property changes on: trunk/extensions/ContributionTracking/tests/ContributionTrackingProcessorTest.php
___________________________________________________________________
Added: svn:eol-style
1431 + native
Index: trunk/extensions/ContributionTracking/tests/ContributionTrackingTest.php
@@ -0,0 +1,330 @@
 2+<?php
 3+
 4+/**
 5+ * Yes, I realize this whole test class is full of things that are more
 6+ * regression run by phpunit, than actual unit tests. For the sake of coverage,
 7+ * it's going to stay that way until we can completely refactor
 8+ * ContributionTracking_body.php (beyond splitting its newly-shared
 9+ * functionality out into something the new API can also reach).
 10+ * //TODO: Refactor ContributionTracking_body.php, and clean up this whole mess.
 11+ * //TODO: Add tests to make sure that garbage requests fail gracefully.
 12+ *
 13+ * //FIXME: Yes, this test class and ContributionTrackingAPITest are nearly
 14+ * exactly the same. They should probably be combined into a thing that tests
 15+ * both entry methods simultaneously with the same requests.
 16+ * @group Fundraising
 17+ * @group Splunge
 18+ * @author Katie Horn <khorn@wikimedia.org>
 19+ */
 20+class ContributionTrackingTest extends MediaWikiTestCase {
 21+
 22+ /**
 23+ * Takes $request parameters and checks them against $expected parameters in
 24+ * the hidden form that comes back from the ContributionTracking page.
 25+ * All assert failures will start with the $message_prefix so we know which
 26+ * test actually failed.
 27+ * @param array $request The request parameters
 28+ * @param array $expected Expected contents of the hidden form about to be
 29+ * reposted to the gateway.
 30+ * @param string $message_prefix A readable string that identifies the test
 31+ * on failed assert.
 32+ */
 33+ function assertExecute_repostFormAsExpected( $request, $expected, $message_prefix ) {
 34+ $page_xml = $this->getPageHTML( $request );
 35+
 36+ $reposters = array( );
 37+ foreach ( $page_xml->getElementsByTagName( 'input' ) as $node ) {
 38+ $attributes = $this->getNodeAttributes( $node );
 39+ if ( $attributes['type'] == 'hidden' ) {
 40+ $reposters[$attributes['name']] = $attributes['value'];
 41+ }
 42+ }
 43+
 44+ foreach ( $expected['fields'] as $name => $value ) {
 45+ if ( $name === 'custom' ) {
 46+ $this->assertTrue( is_numeric( $reposters[$name] ), $message_prefix . ": 'custom' should be a number." );
 47+ } elseif ( $name === 'item_name' && array_key_exists( 'language', $request ) && $request['language'] !== 'en' ) {
 48+ //TODO: Actually deal with the encoding mismatch here. Urgh.
 49+ $this->assertTrue( ($reposters[$name] != 'One-time Donation' ), $message_prefix . ": Alternate language is coming up English." );
 50+ } else {
 51+ $this->assertEquals( $value, $reposters[$name], $message_prefix . ": Field $name was not reposted as expected by the interstitial page" );
 52+ }
 53+ }
 54+
 55+ //and don't forget to check it's the proper action!
 56+ foreach ( $page_xml->getElementsByTagName( 'form' ) as $node ) {
 57+ $attributes = $this->getNodeAttributes( $node );
 58+ if ( $attributes['name'] == 'contributiontracking' ) {
 59+ $this->assertEquals( $attributes['action'], $expected['action'], $message_prefix . ": Form action was incorrect!" );
 60+ }
 61+ }
 62+ }
 63+
 64+ /**
 65+ * Gets the ContributionTracking page's HTML and loads it into a DomDocument
 66+ * @global FauxRequest $wgRequest used to shoehorn in our own request vars.
 67+ * @global <type> $wgOut Needed so I can grab the resultant HTML.
 68+ * @global <type> $wgTitle Needed to solve a totally weird bug. (See below)
 69+ * @param <type> $request Request vars we are sending to the
 70+ * ContributionTracking page
 71+ * @return DomDocument Loaded up with the generated page's html.
 72+ */
 73+ function getPageHTML( $request ) {
 74+ global $wgRequest, $wgOut, $wgTitle;
 75+
 76+ //The next line addresses a totally weird bug I found. Uncomment the next line and run the test to see it.
 77+ $wgTitle = Title::newFromText( 'whatever' );
 78+
 79+ $ctpage = new ContributionTracking();
 80+ $wgRequest = new FauxRequest( $request );
 81+ if ( array_key_exists( 'language', $request ) ) {
 82+ $language = $request['language'];
 83+ } else {
 84+ $language = 'en';
 85+ }
 86+ $ctpage->execute( $language );
 87+ $page_xml = new DomDocument( '1.0' );
 88+
 89+ $page_xml->loadHTML( trim( $wgOut->getHTML() ) );
 90+
 91+ return $page_xml;
 92+
 93+ //echo "Hidden form: " . print_r($reposters, true);
 94+ //echo "wgOut: ##" . print_r($wgOut->getHTML(), true) . "##\n";
 95+ }
 96+
 97+ /**
 98+ * Sets up a bare-bones request to send to the interstitial page, and the
 99+ * values we expect to see in the page's hidden repost form after
 100+ * processing. Then calls assertExecute_repostFormAsExpected for the actual
 101+ * processing and assertions.
 102+ */
 103+ function testExecuteforRepostFields_minimal() {
 104+ $minimal = array(
 105+ 'referrer' => 'phpunit_interstitial',
 106+ 'gateway' => 'paypal',
 107+ 'amount' => '8.80'
 108+ );
 109+
 110+ $returnTitle = Title::newFromText( 'Donate-thanks/en' );
 111+ $expected = array(
 112+ 'action' => 'https://www.paypal.com/cgi-bin/webscr',
 113+ 'fields' => array(
 114+ 'business' => 'donations@wikimedia.org',
 115+ 'item_number' => 'DONATE',
 116+ 'no_note' => 0,
 117+ 'return' => $returnTitle->getFullUrl(),
 118+ 'currency_code' => 'USD',
 119+ 'cmd' => '_xclick',
 120+ 'notify_url' => 'https://civicrm.wikimedia.org/fundcore_gateway/paypal',
 121+ 'item_name' => 'One-time donation',
 122+ 'amount' => '8.80',
 123+ 'custom' => '', //this is overridden later. Should be the id of the inserted transaction.
 124+ )
 125+ );
 126+
 127+ $this->assertExecute_repostFormAsExpected( $minimal, $expected, "Minimal Repost Test" );
 128+ }
 129+
 130+ /**
 131+ * Sets up a recurring payment type request to send to the interstitial
 132+ * page, and the values we expect to see in the page's hidden repost form
 133+ * after processing. Then calls assertExecute_repostFormAsExpected for the
 134+ * actual processing and assertions.
 135+ */
 136+ function testExecuteforRepostFields_recurring() {
 137+ //test paypal recurring
 138+ $recurring = array(
 139+ 'referrer' => 'phpunit_interstitial',
 140+ 'gateway' => 'paypal',
 141+ 'amount' => '8.80',
 142+ 'recurring_paypal' => true
 143+ );
 144+ $returnTitle = Title::newFromText( 'Donate-thanks/en' );
 145+ $expected = array(
 146+ 'action' => 'https://www.paypal.com/cgi-bin/webscr',
 147+ 'fields' => array(
 148+ 'business' => 'donations@wikimedia.org',
 149+ 'item_number' => 'DONATE',
 150+ 'no_note' => 0,
 151+ 'return' => $returnTitle->getFullUrl(),
 152+ 'currency_code' => 'USD',
 153+ 'cmd' => '_xclick',
 154+ 'notify_url' => 'https://civicrm.wikimedia.org/fundcore_gateway/paypal',
 155+ 'item_name' => 'One-time donation',
 156+ 'a3' => '8.80',
 157+ 'custom' => '', //this is overridden later. Should be the id of the inserted transaction.
 158+ 't3' => 'M',
 159+ 'p3' => '1',
 160+ 'srt' => '12',
 161+ 'src' => '1',
 162+ 'sra' => '1',
 163+ 'cmd' => '_xclick-subscriptions',
 164+ 'item_name' => 'Recurring monthly donation',
 165+ )
 166+ );
 167+
 168+
 169+ $this->assertExecute_repostFormAsExpected( $recurring, $expected, "Paypal Recurring Test" );
 170+ }
 171+
 172+ /**
 173+ * Sets up a non-english request (in a language that has a translation) to
 174+ * send to the interstitial page, and the values we expect to see in the
 175+ * page's hidden repost form after processing. Then calls
 176+ * assertExecute_repostFormAsExpected for the actual processing and
 177+ * assertions.
 178+ * FIXME: something about the encoding makes this not work as expected.
 179+ */
 180+ function testExecuteforRepostFields_language() {
 181+
 182+ //test alternate language
 183+ $language = array(
 184+ 'referrer' => 'phpunit_interstitial',
 185+ 'gateway' => 'paypal',
 186+ 'amount' => '8.80',
 187+ 'language' => 'ja'
 188+ );
 189+
 190+
 191+ $returnTitle = Title::newFromText( 'Donate-thanks/ja' );
 192+ $expected = array(
 193+ 'action' => 'https://www.paypal.com/cgi-bin/webscr',
 194+ 'fields' => array(
 195+ 'business' => 'donations@wikimedia.org',
 196+ 'item_number' => 'DONATE',
 197+ 'no_note' => 0,
 198+ 'return' => $returnTitle->getFullUrl(), //Important to the language test.
 199+ 'currency_code' => 'USD',
 200+ 'cmd' => '_xclick',
 201+ 'notify_url' => 'https://civicrm.wikimedia.org/fundcore_gateway/paypal',
 202+ 'item_name' => '1回だけ寄付', //This should be translated.
 203+ 'amount' => '8.80',
 204+ 'custom' => '',
 205+ )
 206+ );
 207+
 208+ $this->assertExecute_repostFormAsExpected( $language, $expected, "Translation Test" );
 209+ }
 210+
 211+ /**
 212+ * Sets up a "premium" request to send to the interstitial page, and the
 213+ * values we expect to see in the page's hidden repost form after
 214+ * processing. Then calls assertExecute_repostFormAsExpected for the actual
 215+ * processing and assertions.
 216+ */
 217+ function testExecuteforRepostFields_tshirts() {
 218+
 219+ //test T-shirtness
 220+ $tshirts = array(
 221+ 'referrer' => 'phpunit_interstitial',
 222+ 'gateway' => 'paypal',
 223+ 'amount' => '8.80',
 224+ 'language' => 'en',
 225+ 'tshirt' => 'true',
 226+ 'size' => 'medium',
 227+ 'premium_language' => 'ja'
 228+ );
 229+
 230+ $returnTitle = Title::newFromText( 'Donate-thanks/en' );
 231+ $expected = array(
 232+ 'action' => 'https://www.paypal.com/cgi-bin/webscr',
 233+ 'fields' => array(
 234+ 'business' => 'donations@wikimedia.org',
 235+ 'item_number' => 'DONATE',
 236+ 'no_note' => 0,
 237+ 'return' => $returnTitle->getFullUrl(),
 238+ 'currency_code' => 'USD',
 239+ 'cmd' => '_xclick',
 240+ 'notify_url' => 'https://civicrm.wikimedia.org/fundcore_gateway/paypal',
 241+ 'item_name' => 'One-time donation',
 242+ 'amount' => '8.80',
 243+ 'custom' => '',
 244+ 'on0' => 'Shirt size',
 245+ 'os0' => 'medium',
 246+ 'on1' => 'Shirt language',
 247+ 'os1' => 'ja',
 248+ 'no_shipping' => 2
 249+ )
 250+ );
 251+
 252+ $this->assertExecute_repostFormAsExpected( $tshirts, $expected, "T-shirt Test" );
 253+ }
 254+
 255+ /**
 256+ * Tests to make sure the contribution was saved in the database properly.
 257+ * Assertions:
 258+ * The saved contribution ID is reposted to paypal
 259+ * Each parameter saved to the contribution_tracking table is identical
 260+ * to the value we were trying to save, in the row matching the ID passed to
 261+ * paypal
 262+ * The owa_ref URL value is stored in the owa_ref table, and referenced
 263+ * by the correct id in the owa_ref column
 264+ *
 265+ */
 266+ function testExecuteForContributionSave() {
 267+ //TODO: Test inserting pure garbage.
 268+ $complete = array(
 269+ 'comment' => 'Interstitial Save',
 270+ 'referrer' => 'phpunit_interstitial',
 271+ 'comment-option' => 'yep',
 272+ 'utm_source' => 'here',
 273+ 'utm_medium' => 'large',
 274+ 'utm_campaign' => 'testy01',
 275+ 'language' => 'en',
 276+ 'owa_session' => 'foo2',
 277+ 'owa_ref' => 'execute_save',
 278+ 'gateway' => 'paypal',
 279+ 'amount' => '6.60'
 280+ );
 281+ $table1_check = $complete;
 282+ $table1_check['anonymous'] = 0;
 283+ $table1_check['optout'] = 1;
 284+ $table1_check['note'] = $complete['comment'];
 285+ unset( $table1_check['owa_ref'] );
 286+ unset( $table1_check['comment'] );
 287+ unset( $table1_check['comment-option'] );
 288+ unset( $table1_check['gateway'] );
 289+ unset( $table1_check['amount'] );
 290+
 291+ $page_xml = $this->getPageHTML( $complete );
 292+
 293+ //We're using paypal, one-time, so the ID will come back in the hidden "custom" field
 294+
 295+ $reposters = array( );
 296+ foreach ( $page_xml->getElementsByTagName( 'input' ) as $node ) {
 297+ $attributes = $this->getNodeAttributes( $node );
 298+ if ( $attributes['type'] == 'hidden' ) {
 299+ $reposters[$attributes['name']] = $attributes['value'];
 300+ }
 301+ }
 302+
 303+ $this->assertTrue( is_numeric( $reposters['custom'] ), "The saved transaction ID was not found" );
 304+
 305+ $db = ContributionTrackingProcessor::contributionTrackingConnection();
 306+ $row = $db->selectRow( 'contribution_tracking', '*', array( 'id' => $reposters['custom'] ) );
 307+
 308+ foreach ( $table1_check as $key => $value ) {
 309+ $this->assertEquals( $value, $row->$key, "$key does not match in the database." );
 310+ }
 311+
 312+ $row = $db->selectRow( 'contribution_tracking_owa_ref', '*', array( 'id' => $row->owa_ref ) );
 313+ $this->assertEquals( $complete['owa_ref'], $row->url, "OWA Reference lookup does not match" );
 314+ }
 315+
 316+ /**
 317+ *
 318+ * @param DOMNode $node A DOMNode that ostensibly has attributes we need to retrieve.
 319+ * @return array All of $node's attributes in a key/value array.
 320+ */
 321+ function getNodeAttributes( $node ) {
 322+ $attributes = array( );
 323+ foreach ( $node->attributes as $name => $attrNode ) {
 324+ $attributes[$name] = $attrNode->value;
 325+ }
 326+ return $attributes;
 327+ }
 328+
 329+}
 330+
 331+?>
Property changes on: trunk/extensions/ContributionTracking/tests/ContributionTrackingTest.php
___________________________________________________________________
Added: svn:eol-style
1332 + native
Index: trunk/extensions/ContributionTracking/ContributionTracking_body.php
@@ -1,177 +1,87 @@
22 <?php
33
44 class ContributionTracking extends UnlistedSpecialPage {
 5+
56 function __construct() {
67 parent::__construct( 'ContributionTracking' );
78 }
89
9 - function get_owa_ref_id($ref){
10 - // Replication lag means sometimes a new event will not exist in the table yet
11 - $dbw = contributionTrackingConnection(); //wfGetDB( DB_MASTER );
12 - $id_num = $dbw->selectField(
13 - 'contribution_tracking_owa_ref',
14 - 'id',
15 - array( 'url' => $ref ),
16 - __METHOD__
17 - );
18 - // Once we're on mysql 5, we can use replace() instead of this selectField --> insert or update hooey
19 - if ( $id_num === false ) {
20 - $dbw->insert(
21 - 'contribution_tracking_owa_ref',
22 - array( 'url' => (string) $event_name ),
23 - __METHOD__
24 - );
25 - $id_num = $dbw->insertId();
26 - }
27 - return $id_num === false ? 0 : $id_num;
28 - }
29 -
30 -
3110 function execute( $language ) {
32 - global $wgRequest, $wgOut, $wgContributionTrackingPayPalIPN, $wgContributionTrackingReturnToURLDefault,
33 - $wgContributionTrackingPayPalRecurringIPN, $wgContributionTrackingPayPalBusiness;
34 -
 11+ global $wgRequest, $wgOut, $wgContributionTrackingReturnToURLDefault;
 12+
3513 if ( !preg_match( '/^[a-z-]+$/', $language ) ) {
3614 $language = 'en';
3715 }
3816 $this->lang = Language::factory( $language );
39 -
 17+
4018 $this->setHeaders();
41 -
 19+
4220 $gateway = $wgRequest->getText( 'gateway' );
43 - if( !in_array( $gateway, array( 'paypal', 'moneybookers' ) ) ) {
 21+ if ( !in_array( $gateway, array( 'paypal', 'moneybookers' ) ) ) {
4422 $wgOut->showErrorPage( 'contrib-tracking-error', 'contrib-tracking-error-text' );
4523 return;
4624 }
47 -
48 - $db = contributionTrackingConnection();
4925
50 - $ts = $db->timestamp();
51 -
52 - $owa_ref = $wgRequest->getText('owa_ref', null);
53 - if($owa_ref != null && !is_numeric($owa_ref)){
54 - $owa_ref = $this->get_owa_ref_id($owa_ref);
 26+ // Store the contribution data
 27+ if ( $wgRequest->getVal( 'contribution_tracking_id' ) ) {
 28+ $contribution_tracking_id = $wgRequest->getVal( 'contribution_tracking_id', 0 );
 29+ } else {
 30+ $tracked_contribution = array(
 31+ 'note' => $wgRequest->getVal( 'comment' ),
 32+ 'referrer' => $wgRequest->getVal( 'referrer' ),
 33+ 'anonymous' => $wgRequest->getCheck( 'comment-option', false ) ? false : true, //yup: 'anonymous' = !comment-option
 34+ 'utm_source' => $wgRequest->getVal( 'utm_source' ),
 35+ 'utm_medium' => $wgRequest->getVal( 'utm_medium' ),
 36+ 'utm_campaign' => $wgRequest->getVal( 'utm_campaign' ),
 37+ 'optout' => $wgRequest->getCheck( 'email-opt', false ) ? false : true, //Also: 'optout' = !email-opt.
 38+ 'language' => $wgRequest->getVal( 'language' ),
 39+ 'owa_session' => $wgRequest->getVal( 'owa_session' ),
 40+ 'owa_ref' => $wgRequest->getVal( 'owa_ref', null ),
 41+ //'ts' => $ts,
 42+ );
 43+ $contribution_tracking_id = ContributionTrackingProcessor::saveNewContribution( $tracked_contribution );
5544 }
5645
57 - $tracked_contribution = array(
58 - 'note' => $wgRequest->getText('comment', null),
59 - 'referrer' => $wgRequest->getText('referrer', null),
60 - 'anonymous' => ($wgRequest->getCheck('comment-option', 0) ? 0 : 1),
61 - 'utm_source' => $wgRequest->getText('utm_source', null),
62 - 'utm_medium' => $wgRequest->getText('utm_medium', null),
63 - 'utm_campaign' => $wgRequest->getText('utm_campaign', null),
64 - 'optout' => ($wgRequest->getCheck('email-opt', 0) ? 0 : 1),
65 - 'language' => $wgRequest->getText('language', null),
66 - 'owa_session' => $wgRequest->getText('owa_session', null),
67 - 'owa_ref' => $owa_ref,
68 - 'ts' => $ts,
 46+ $params = array(
 47+ 'gateway' => $gateway,
 48+ 'tshirt' => $wgRequest->getVal( 'tshirt' ),
 49+ 'return' => $wgRequest->getText( 'returnto', "Donate-thanks/$language" ),
 50+ 'currency_code' => $wgRequest->getText( 'currency_code', 'USD' ),
 51+ 'fname' => $wgRequest->getText( 'fname', null ),
 52+ 'lname' => $wgRequest->getText( 'lname', null ),
 53+ 'email' => $wgRequest->getText( 'email', null ),
 54+ 'recurring_paypal' => $wgRequest->getText( 'recurring_paypal' ),
 55+ 'amount' => $wgRequest->getVal( 'amount' ),
 56+ 'amount_given' => $wgRequest->getVal( 'amountGiven' ),
 57+ 'contribution_tracking_id' => $contribution_tracking_id,
 58+ 'language' => $language,
6959 );
70 -
71 - // Make all empty strings NULL
72 - foreach ($tracked_contribution as $key => $value) {
73 - if ($value === '') {
74 - $tracked_contribution[$key] = null;
75 - }
 60+
 61+ if ( $params['tshirt'] ) {
 62+ $params['size'] = $wgRequest->getText( 'size' );
 63+ $params['premium_language'] = $wgRequest->getText( 'premium_language' );
7664 }
77 -
78 - // Store the contribution data
79 - if ( !$wgRequest->getVal( 'contribution_tracking_id', 0 )) {
80 - $db->insert( 'contribution_tracking', $tracked_contribution );
81 - }
82 - $contribution_tracking_id = $wgRequest->getVal( 'contribution_tracking_id', $db->insertId());
83 -
84 - $returnText = $wgRequest->getText( 'returnto', "Donate-thanks/$language" );
85 - $returnTitle = Title::newFromText( $returnText );
86 - if( $returnTitle ) {
87 - $returnto = $returnTitle->getFullUrl();
88 - } else {
89 - $returnto = $wgContributionTrackingReturnToURLDefault . "/$language";
90 - }
91 -
92 - // Set the action and tracking ID fields
93 - $repost = array();
94 - $action = 'http://wikimediafoundation.org/';
95 - $amount_field_name = 'amount'; // the amount fieldname may be different depending on the service
96 - if ( $gateway == 'paypal' ) {
97 -
98 - $action = 'https://www.paypal.com/cgi-bin/webscr';
9965
100 - // Premiums
101 - if ( $wgRequest->getVal( 'tshirt') == '1' ) {
102 - $repost['on0'] = 'Shirt size';
103 - $repost['os0'] = $wgRequest->getText( 'size' );
104 - $repost['on1'] = 'Shirt language';
105 - $repost['os1'] = $wgRequest->getText( 'premium_language' );
106 - $repost['no_shipping'] = 2;
 66+ foreach ( $params as $key => $value ) {
 67+ if ( $value === "" || $value === null ) {
 68+ unset( $params[$key] );
10769 }
108 -
109 - // PayPal
110 - $repost['business'] = $wgContributionTrackingPayPalBusiness;
111 - $repost['item_number'] = 'DONATE';
112 - $repost['no_note'] = '0';
113 - $repost['return'] = $returnto;
114 - $repost['currency_code'] = $wgRequest->getText( 'currency_code', 'USD' );
115 -
116 - // additional fields to pass to PayPal from single-step credit card form
117 - $repost[ 'first_name' ] = $wgRequest->getText( 'fname', null );
118 - $repost[ 'last_name' ] = $wgRequest->getText( 'lname', null );
119 - $repost[ 'email' ] = $wgRequest->getText( 'email', null );
120 -
121 - // if this is a recurring donation, we have add'l fields to send to paypal
122 - if ( $wgRequest->getText( 'recurring_paypal' ) == 'true' ) {
123 - $repost[ 't3' ] = "M"; // The unit of measurement for for p3 (M = month)
124 - $repost[ 'p3' ] = '1'; // Billing cycle duration
125 - $repost[ 'srt' ] = '12'; // # of billing cycles
126 - $repost[ 'src' ] = '1'; // Make this 'recurring'
127 - $repost[ 'sra' ] = '1'; // Turn on re-attempt on failure
128 - $repost[ 'cmd' ] = '_xclick-subscriptions';
129 - $amount_field_name = 'a3';
130 - $repost['notify_url'] = $wgContributionTrackingPayPalRecurringIPN;
131 - $repost['item_name'] = $this->msg( 'contrib-tracking-item-name-recurring' );
132 - } else {
133 - $repost['cmd'] = '_xclick';
134 - $repost['notify_url'] = $wgContributionTrackingPayPalIPN;
135 - $repost['item_name'] = $this->msg( 'contrib-tracking-item-name-onetime' );
136 - }
13770 }
138 - else if ( $gateway == 'moneybookers' ) {
139 - $action = 'https://www.moneybookers.com/app/payment.pl';
14071
141 - // Tracking
142 - $repost['merchant_fields'] = 'os0';
 72+ $repost = ContributionTrackingProcessor::getRepostFields( $params );
14373
144 - // Moneybookers
145 - $repost['pay_to_email'] = 'donation@wikipedia.org';
146 - $repost['status_url'] = 'https://civicrm.wikimedia.org/fundcore_gateway/moneybookers';
147 - $repost['language'] = 'en';
148 - $repost['detail1_description'] = 'One-time donation';
149 - $repost['detail1_text'] = 'DONATE';
150 - $repost['currency'] = $wgRequest->getText( 'currency_code', 'USD' );
151 - } else {
152 - throw new MWException( "This shouldn't happen, we validated the gateway earlier." );
153 - }
154 -
155 - // Normalized amount
156 - $repost[ $amount_field_name ] = $wgRequest->getVal( 'amount' );
157 - if ( $wgRequest->getVal( 'amountGiven' ) ) {
158 - $repost[ $amount_field_name ] = $wgRequest->getVal( 'amountGiven' );
159 - }
160 -
161 - // Tracking
162 - $repost['custom'] = $contribution_tracking_id;
163 -
16474 $wgOut->addWikiText( "{{2009/Donate-banner/$language}}" );
16575 $wgOut->addHTML( $this->msgWiki( 'contrib-tracking-submitting' ) );
166 -
 76+
16777 // Output the repost form
168 - $output = '<form method="post" name="contributiontracking" action="' . $action . '">';
 78+ $output = '<form method="post" name="contributiontracking" action="' . $repost['action'] . '">';
16979
170 - foreach ( $repost as $key => $value ) {
171 - $output .= '<input type="hidden" name="' . htmlspecialchars($key) . '" value="' . htmlspecialchars($value) . '" />';
 80+ foreach ( $repost['fields'] as $key => $value ) {
 81+ $output .= '<input type="hidden" name="' . htmlspecialchars( $key ) . '" value="' . htmlspecialchars( $value ) . '" />';
17282 }
173 -
 83+
17484 $output .= $this->msgWiki( 'contrib-tracking-redirect' );
175 -
 85+
17686 // Offer a button to post the form if the user has no Javascript support
17787 $output .= '<noscript>';
17888 $output .= $this->msgWiki( 'contrib-tracking-continue' );
@@ -184,14 +94,12 @@
18595
18696 // Automatically post the form if the user has Javascript support
18797 $wgOut->addHTML( '<script type="text/javascript">document.contributiontracking.submit();</script>' );
188 -
18998 }
19099
191 - function msg( $key ) {
192 - return wfMsgExt( $key, array( 'escape', 'language' => $this->lang ) );
 100+ function msg() {
 101+ return wfMsgExt( func_get_arg( 0 ), array( 'escape', 'language' => $this->lang ) );
193102 }
194103
195 -
196104 function msgWiki( $key ) {
197105 return wfMsgExt( $key, array( 'parse', 'language' => $this->lang ) );
198106 }
Index: trunk/extensions/ContributionTracking/ContributionTracking.processor.php
@@ -0,0 +1,422 @@
 2+<?php
 3+/**
 4+ * Centralized class used by both the old interstitial page, and the API to
 5+ * process transactions and send donors off to the correct gateway location.
 6+ * @author Katie Horn <khorn@wikimedia.org>
 7+ */
 8+class ContributionTrackingProcessor {
 9+
 10+ /**
 11+ * If a database connection has already been established, it returns that
 12+ * connection. Otherwise, it establishes one, and returns that.
 13+ * @global string $wgContributionTrackingDBserver : DB Server name, defined
 14+ * in ContributionTracking.php
 15+ * @global string $wgContributionTrackingDBname : Database name, defined in
 16+ * ContributionTracking.php
 17+ * @global string $wgContributionTrackingDBuser : Database user, defined in
 18+ * ContributionTracking.php
 19+ * @global string $wgContributionTrackingDBpassword : Database password,
 20+ * defined in ContributionTracking.php
 21+ * @staticvar DatabaseMysql $db
 22+ * @return DatabaseMysql The established database connection
 23+ */
 24+ static function contributionTrackingConnection() {
 25+ global $wgContributionTrackingDBserver, $wgContributionTrackingDBname;
 26+ global $wgContributionTrackingDBuser, $wgContributionTrackingDBpassword;
 27+
 28+ static $db;
 29+
 30+ if ( !$db ) {
 31+ $db = new DatabaseMysql(
 32+ $wgContributionTrackingDBserver,
 33+ $wgContributionTrackingDBuser,
 34+ $wgContributionTrackingDBpassword,
 35+ $wgContributionTrackingDBname );
 36+ $db->query( "SET names utf8" );
 37+ }
 38+
 39+ return $db;
 40+ }
 41+
 42+ /**
 43+ * Looks up the url specified in $ref. If it is known, the existing id is
 44+ * returned. If it is new, a row is added to contribution_owa_ref and the
 45+ * new id is returned.
 46+ * @param string $ref owa_ref URL
 47+ * @return integer ID of the URL in the contribution_tracking_owa_ref table,
 48+ * 0 if something went wrong.
 49+ */
 50+ static function get_owa_ref_id( $ref ) {
 51+ // Replication lag means sometimes a new event will not exist in the table yet
 52+ $dbw = ContributionTrackingProcessor::contributionTrackingConnection(); //wfGetDB( DB_MASTER );
 53+ $id_num = $dbw->selectField(
 54+ 'contribution_tracking_owa_ref',
 55+ 'id',
 56+ array( 'url' => $ref ),
 57+ __METHOD__
 58+ );
 59+ // Once we're on mysql 5, we can use replace() instead of this selectField --> insert or update hooey
 60+ if ( $id_num === false ) {
 61+ $dbw->insert(
 62+ 'contribution_tracking_owa_ref',
 63+ array( 'url' => ( string ) $ref ),
 64+ __METHOD__
 65+ );
 66+ $id_num = $dbw->insertId();
 67+ }
 68+ return $id_num === false ? 0 : $id_num;
 69+ }
 70+
 71+ /**
 72+ * Saves a record of a new contribution to the contribution_tracking_table
 73+ * @param array $params A staged array of parameters that can be processed
 74+ * by the ContributionTrackingProcessor.
 75+ * @return integer The id of the saved contribution in the
 76+ * contribution_tracking table
 77+ */
 78+ static function saveNewContribution( $params = array( ) ) {
 79+ $db = ContributionTrackingProcessor::contributionTrackingConnection();
 80+
 81+ $params['ts'] = $db->timestamp();
 82+
 83+ $owa_ref = null;
 84+ if ( array_key_exists( 'owa_ref', $params ) && $params['owa_ref'] != null ) {
 85+ if ( $params['owa_ref'] == null || is_numeric( $params['owa_ref'] ) ) {
 86+ $owa_ref = $params['owa_ref'];
 87+ } else {
 88+ $owa_ref = ContributionTrackingProcessor::get_owa_ref_id( $params['owa_ref'] );
 89+ }
 90+ }
 91+ $params['owa_ref'] = $owa_ref;
 92+
 93+ $tracked_contribution = ContributionTrackingProcessor::stage_contribution( $params );
 94+
 95+ $db->insert( 'contribution_tracking', $tracked_contribution );
 96+ $contribution_tracking_id = $db->insertId();
 97+
 98+ return $contribution_tracking_id;
 99+ }
 100+
 101+ /**
 102+ * Stages the contribution parameters
 103+ * @param array $params Key-value pairs of the contribution parameters we
 104+ * want to pass in.
 105+ * @return array Staged key-value pairs ready to be saved as a contribution.
 106+ */
 107+ static function stage_contribution( $params ) {
 108+
 109+ //change the posted names to match the db where necessary
 110+ ContributionTrackingProcessor::rekey( $params, 'comment', 'note' );
 111+ ContributionTrackingProcessor::rekey_invert_boolean( $params, 'comment-option', 'anonymous' );
 112+ ContributionTrackingProcessor::rekey_invert_boolean( $params, 'email-opt', 'optout' );
 113+
 114+ $tracked_contribution = ContributionTrackingProcessor::mergeArrayDefaults( $params, ContributionTrackingProcessor::getContributionDefaults(), true );
 115+
 116+ return $tracked_contribution;
 117+ }
 118+
 119+ /**
 120+ * Stages the relevent data that will be sent to the gateway
 121+ * @global string $wgContributionTrackingPayPalRecurringIPN URL for paypal
 122+ * recurring donations : Defined in ContributionTracking.php
 123+ * @global string $wgContributionTrackingPayPalIPN URL for paypal recurring
 124+ * donations : Defined in ContributionTracking.php
 125+ * @param array $params Parameters to post to the gateway
 126+ * @return array Staged array
 127+ */
 128+ static function stage_repost( $params ) {
 129+ global $wgContributionTrackingPayPalRecurringIPN, $wgContributionTrackingPayPalIPN;
 130+ //TODO: assert that gateway makes The Sense here.
 131+ //change the posted names to match the db where necessary
 132+ ContributionTrackingProcessor::rekey( $params, 'amountGiven', 'amount_given' );
 133+ ContributionTrackingProcessor::rekey( $params, 'returnto', 'return' );
 134+
 135+ //booleanize!
 136+ ContributionTrackingProcessor::stage_checkbox( $params, 'recurring_paypal' );
 137+
 138+ //poke our language function with the current parameters - this sets the static var correctly
 139+ $params['language'] = ContributionTrackingProcessor::getLanguage( $params );
 140+
 141+ if ( array_key_exists( 'recurring_paypal', $params ) && $params['recurring_paypal'] ) {
 142+ $params['notify_url'] = $wgContributionTrackingPayPalRecurringIPN;
 143+ $params['item_name'] = ContributionTrackingProcessor::msg( 'contrib-tracking-item-name-recurring' );
 144+ } else {
 145+ $params['notify_url'] = $wgContributionTrackingPayPalIPN;
 146+ $params['item_name'] = ContributionTrackingProcessor::msg( 'contrib-tracking-item-name-onetime' );
 147+ }
 148+
 149+ $repost_params = ContributionTrackingProcessor::mergeArrayDefaults( $params, ContributionTrackingProcessor::getRepostDefaults(), true );
 150+ return $repost_params;
 151+ }
 152+
 153+ /**
 154+ * Effectively changes the name of a key in an array. If the key does not
 155+ * exist, no change is made.
 156+ * @param array $array The array to rekey (by reference)
 157+ * @param string $oldkey The key to change
 158+ * @param string $newkey The new value for the key
 159+ */
 160+ static function rekey( &$array, $oldkey, $newkey ) {
 161+ if ( array_key_exists( $oldkey, $array ) ) {
 162+ $array[$newkey] = $array[$oldkey];
 163+ unset( $array[$oldkey] );
 164+ }
 165+ }
 166+
 167+ /**
 168+ * There are a few values that come in, which are both generated by
 169+ * checkboxes, and are the exact inverse of the way we save them in the
 170+ * table.
 171+ * For these values, if the key exists (and is not explicit false), it is
 172+ * received as "true". Therefore, the rekey'd value should be false.
 173+ * However, the old key not existing isn't exactly conclusive.
 174+ * @param array $array The array to rekey (by reference)
 175+ * @param string $oldkey The key to change
 176+ * @param string $invertedkey The key meant to contain the inverted boolean
 177+ * of the old key.
 178+ */
 179+ static function rekey_invert_boolean( &$array, $oldkey, $invertedkey ) {
 180+ if ( array_key_exists( $oldkey, $array ) ) {
 181+ if ( $array[$oldkey] !== false ) {
 182+ unset( $array[$oldkey] );
 183+ $array[$invertedkey] = false;
 184+ } else {
 185+ $array[$invertedkey] = 1;
 186+ }
 187+ return;
 188+ }
 189+
 190+ if ( array_key_exists( $invertedkey, $array ) ) {
 191+ ContributionTrackingProcessor::stage_checkbox( $array, $invertedkey );
 192+ return;
 193+ }
 194+
 195+ //at this point, neither key exists. We go with the default.
 196+ $default = ContributionTrackingProcessor::getContributionDefaults();
 197+ if ( array_key_exists( $invertedkey, $default ) ) {
 198+ $array[$invertedkey] = $default[$invertedkey];
 199+ }
 200+ }
 201+
 202+ /**
 203+ * Stages a value generated by a checkbox or similar control, for use in our
 204+ * database. If the key exists and has not been set to exactly false, it's
 205+ * "true".
 206+ * @param array $array The array containing the value to stage, by reference
 207+ * @param string $key The key of a checkbox-generated value
 208+ */
 209+ static function stage_checkbox( &$array, $key ) {
 210+ //apparently so far in the code, if the key exists, the value is considered true
 211+ //and is therefore set to "1"
 212+ if ( array_key_exists( $key, $array ) && $array[$key] !== false ) {
 213+ $array[$key] = 1;
 214+ }
 215+ }
 216+
 217+ /**
 218+ * Returns a default value for every relevent field in a new contribution.
 219+ * @return array Default values for a new contribution.
 220+ */
 221+ static function getContributionDefaults() {
 222+ return array( //defaults
 223+ 'note' => null,
 224+ 'referrer' => null,
 225+ 'anonymous' => 0,
 226+ 'utm_source' => null,
 227+ 'utm_medium' => null,
 228+ 'utm_campaign' => null,
 229+ 'optout' => 0,
 230+ 'language' => null,
 231+ 'owa_session' => null,
 232+ 'owa_ref' => null,
 233+ 'ts' => null,
 234+ );
 235+ }
 236+
 237+ /**
 238+ * Returns a default value for every relevent field in a repost to a gateway
 239+ * @return array Default values for a payment gateway repost
 240+ */
 241+ static function getRepostDefaults() {
 242+ return array( //defaults
 243+ 'gateway' => '',
 244+ 'tshirt' => false,
 245+ 'size' => false,
 246+ 'premium_language' => false,
 247+ 'currency_code' => 'USD',
 248+ 'return' => 'Donate-thanks/' . ContributionTrackingProcessor::getLanguage(),
 249+ 'fname' => '',
 250+ 'lname' => '',
 251+ 'email' => '',
 252+ 'recurring_paypal' => '0',
 253+ 'amount' => '',
 254+ 'amount_given' => '',
 255+ 'contribution_tracking_id' => '',
 256+ 'notify_url' => '',
 257+ 'item_name' => ''
 258+ );
 259+ }
 260+
 261+ /**
 262+ * Merges an array of parameters from a payment form, with an array of
 263+ * default values. Additionally: Values in the $params array will only be
 264+ * returned if there is a corresponding key in the $defaults array.
 265+ * @param array $params Form / API data
 266+ * @param array $defaults A set of default values for a particular
 267+ * transaction type
 268+ * @param boolean $nullify If true, keys with empty string values will be
 269+ * set to null in the return array.
 270+ * @return array
 271+ */
 272+ static function mergeArrayDefaults( $params, $defaults, $nullify=false ) {
 273+ foreach ( $defaults as $key => $value ) {
 274+ if ( array_key_exists( $key, $params ) ) {
 275+ $defaults[$key] = $params[$key];
 276+ }
 277+ if ( $nullify && $defaults[$key] === '' ) {
 278+ $defaults[$key] = null;
 279+ }
 280+ }
 281+ return $defaults;
 282+ }
 283+
 284+ /**
 285+ * Takes staged transaction data, and constructs the key/value pairs
 286+ * formatted to be reposted to the gateway specified in $input['gateway']
 287+ * @global string $wgContributionTrackingPayPalBusiness 'Business' string
 288+ * for PayPal: Defined in ContributionTracking.php
 289+ * @global string $wgContributionTrackingReturnToURLDefault Default URL to
 290+ * return to after the transaction was processed by the gateway. Used if
 291+ * none supplied.
 292+ * @param array $input The staged data to repost to a gateway.
 293+ * @return array Key/value pairs, ready to be reposted to the specified
 294+ * gateway to complete the transaction.
 295+ */
 296+ static function getRepostFields( $input ) {
 297+ global $wgContributionTrackingPayPalBusiness, $wgContributionTrackingReturnToURLDefault;
 298+ // Set the action and tracking ID fields
 299+ $input = ContributionTrackingProcessor::stage_repost( $input );
 300+
 301+ $repost = array( );
 302+ $repost['action'] = 'http://wikimediafoundation.org/';
 303+ $amount_field_name = 'amount'; // the amount fieldname may be different depending on the service
 304+ if ( $input['gateway'] == 'paypal' ) {
 305+
 306+ $repost['action'] = 'https://www.paypal.com/cgi-bin/webscr';
 307+
 308+ // Premiums
 309+ if ( array_key_exists( 'tshirt', $input ) && $input['tshirt'] ) {
 310+ $repost['fields']['on0'] = 'Shirt size';
 311+ $repost['fields']['os0'] = $input['size'];
 312+ $repost['fields']['on1'] = 'Shirt language';
 313+ $repost['fields']['os1'] = $input['premium_language'];
 314+ $repost['fields']['no_shipping'] = 2;
 315+ }
 316+
 317+ // PayPal
 318+ $repost['fields']['business'] = $wgContributionTrackingPayPalBusiness;
 319+ $repost['fields']['item_number'] = 'DONATE';
 320+ $repost['fields']['no_note'] = '0';
 321+
 322+ $returnText = $input['return'];
 323+ $returnTitle = Title::newFromText( $returnText );
 324+ if ( $returnTitle ) {
 325+ $returnto = $returnTitle->getFullUrl();
 326+ } else {
 327+ $returnto = $wgContributionTrackingReturnToURLDefault . "/$language";
 328+ }
 329+ $repost['fields']['return'] = $returnto;
 330+ $repost['fields']['currency_code'] = $input['currency_code'];
 331+
 332+ // additional fields to pass to PayPal from single-step credit card form
 333+ if ( array_key_exists( 'fname', $input ) && !empty( $input['fname'] ) ) {
 334+ $repost['fields']['first_name'] = $input['fname'];
 335+ }
 336+ if ( array_key_exists( 'lname', $input ) && !empty( $input['lname'] ) ) {
 337+ $repost['fields']['last_name'] = $input['lname'];
 338+ }
 339+ if ( array_key_exists( 'email', $input ) && !empty( $input['email'] ) ) {
 340+ $repost['fields']['email'] = $input['email'];
 341+ }
 342+
 343+ // if this is a recurring donation, we have add'l fields to send to paypal
 344+ if ( $input['recurring_paypal'] && $input['recurring_paypal'] != 0 ) {
 345+
 346+ $repost['fields']['t3'] = "M"; // The unit of measurement for for p3 (M = month)
 347+ $repost['fields']['p3'] = '1'; // Billing cycle duration
 348+ $repost['fields']['srt'] = '12'; // # of billing cycles
 349+ $repost['fields']['src'] = '1'; // Make this 'recurring'
 350+ $repost['fields']['sra'] = '1'; // Turn on re-attempt on failure
 351+ $repost['fields']['cmd'] = '_xclick-subscriptions';
 352+ $amount_field_name = 'a3';
 353+ $repost['fields']['notify_url'] = $input['notify_url'];
 354+ $repost['fields']['item_name'] = $input['item_name'];
 355+ } else {
 356+ $repost['fields']['cmd'] = '_xclick';
 357+ $repost['fields']['notify_url'] = $input['notify_url'];
 358+ $repost['fields']['item_name'] = $input['item_name'];
 359+ }
 360+ } else if ( $input['gateway'] == 'moneybookers' ) {
 361+ $repost['action'] = 'https://www.moneybookers.com/app/payment.pl';
 362+
 363+ // Tracking
 364+ $repost['fields']['merchant_fields'] = 'os0';
 365+
 366+ // Moneybookers
 367+ $repost['fields']['pay_to_email'] = 'donation@wikipedia.org';
 368+ $repost['fields']['status_url'] = 'https://civicrm.wikimedia.org/fundcore_gateway/moneybookers';
 369+ $repost['fields']['language'] = 'en';
 370+ $repost['fields']['detail1_description'] = 'One-time donation';
 371+ $repost['fields']['detail1_text'] = 'DONATE';
 372+ $repost['fields']['currency'] = $input['currency_code'];
 373+ } else {
 374+ throw new MWException( "Unknown payment gateway!" );
 375+ }
 376+
 377+ // Normalized amount
 378+ $repost['fields'][$amount_field_name] = $input['amount'];
 379+ if ( $input['amount_given'] ) {
 380+ $repost['fields'][$amount_field_name] = $input['amount_given'];
 381+ }
 382+
 383+ // Tracking
 384+ $repost['fields']['custom'] = $input['contribution_tracking_id'];
 385+
 386+ return $repost;
 387+ }
 388+
 389+ /**
 390+ * Sets any language that is expressly specified in the posted parameters.
 391+ * If no language is expressly set, it gets the global language code.
 392+ * @global Language $wgLang
 393+ * @staticvar string $language The language code currently in use
 394+ * @param array $params Request parameters that may or may not contain a
 395+ * 'language' key.
 396+ * @return string A valid language code
 397+ */
 398+ static function getLanguage( $params = '' ) {
 399+ static $language = '';
 400+
 401+ if ( is_array( $params ) && array_key_exists( 'language', $params ) && $params['language'] != null ) {
 402+ //set/reset if something inteligable got sent.
 403+ $language = $params['language'];
 404+ }
 405+
 406+ if ( $language == '' ) { //if we have nothing by this point...
 407+ global $wgLang;
 408+ $language = $wgLang->getCode();
 409+ }
 410+
 411+ return $language;
 412+ }
 413+
 414+ /**
 415+ * Gets a message in the local language
 416+ * @param string $key Message key
 417+ * @return string translated message
 418+ */
 419+ static function msg( $key ) {
 420+ return wfMsgExt( $key, array( 'escape', 'language' => ContributionTrackingProcessor::getLanguage() ) );
 421+ }
 422+
 423+}
Property changes on: trunk/extensions/ContributionTracking/ContributionTracking.processor.php
___________________________________________________________________
Added: svn:eol-style
1424 + native
Index: trunk/extensions/ContributionTracking/ContributionTracking.php
@@ -23,9 +23,31 @@
2424 $wgExtensionAliasesFiles['ContributionTracking'] = $dir . 'ContributionTracking.alias.php';
2525 $wgAutoloadClasses['ContributionTracking'] = $dir . 'ContributionTracking_body.php';
2626 $wgSpecialPages['ContributionTracking'] = 'ContributionTracking';
 27+
 28+$wgAutoloadClasses['ContributionTrackingTester'] = $dir . 'ContributionTracking_Tester.php';
 29+$wgSpecialPages['ContributionTrackingTester'] = 'ContributionTrackingTester';
 30+
 31+//give sysops access to the tracking tester form.
 32+$wgGroupPermissions['sysop']['ViewContributionTrackingTester'] = true;
 33+$wgAvailableRights[] = 'ViewContributionTrackingTester';
 34+
 35+$wgAutoloadClasses['ApiContributionTracking'] = $dir . 'ApiContributionTracking.php';
 36+$wgAutoloadClasses['ContributionTrackingProcessor'] = $dir . 'ContributionTracking.processor.php';
 37+
2738 //this only works if contribution tracking is inside a mediawiki DB, which typically it isn't.
28 -//$wgHooks['LoadExtensionSchemaUpdates'][] = 'efContributionTrackingLoadUpdates';
 39+//$wgHooks['LoadExtensionSchemaUpdates'][] = 'efContributionTrackingLoadUpdates';
2940
 41+// Resource modules
 42+$ctResourceTemplate = array(
 43+ 'localBasePath' => $dir . 'modules',
 44+ 'remoteExtPath' => 'ContributionTracking/modules',
 45+);
 46+$wgResourceModules['jquery.contributionTracking'] = array(
 47+ 'scripts' => 'jquery.contributionTracking.js',
 48+ 'dependencies' => 'jquery.json',
 49+) + $ctResourceTemplate;
 50+
 51+
3052 /**
3153 * The default 'return to' URL for a thank you page after posting to the contribution
3254 *
@@ -53,6 +75,19 @@
5476 */
5577 $wgContributionTrackingPayPalBusiness = 'donations@wikimedia.org';
5678
 79+# Unit tests
 80+$wgHooks['UnitTestsList'][] = 'efContributionTrackingUnitTests';
 81+
 82+function efContributionTrackingUnitTests( &$files ) {
 83+ $files[] = dirname( __FILE__ ) . '/tests/ContributionTrackingTest.php';
 84+ $files[] = dirname( __FILE__ ) . '/tests/ContributionTrackingProcessorTest.php';
 85+ $files[] = dirname( __FILE__ ) . '/tests/ContributionTrackingAPITest.php';
 86+ return true;
 87+}
 88+
 89+// api modules
 90+$wgAPIModules['contributiontracking'] = 'ApiContributionTracking';
 91+
5792 function efContributionTrackingLoadUpdates(){
5893 global $wgExtNewTables, $wgExtNewFields;
5994 $dir = dirname( __FILE__ ) . '/';
@@ -67,43 +102,3 @@
68103 return true;
69104
70105 }
71 -
72 - //convert a referrer URL to an index in the owa_ref table
73 -function ef_contribution_tracking_owa_get_ref_id($ref){
74 - // Replication lag means sometimes a new event will not exist in the table yet
75 - $dbw = contributionTrackingConnection();
76 - $id_num = $dbw->selectField(
77 - 'contribution_tracking_owa_ref',
78 - 'id',
79 - array( 'url' => $ref ),
80 - __METHOD__
81 - );
82 - // Once we're on mysql 5, we can use replace() instead of this selectField --> insert or update hooey
83 - if ( $id_num === false ) {
84 - $dbw->insert(
85 - 'contribution_tracking_owa_ref',
86 - array( 'url' => (string) $ref ),
87 - __METHOD__
88 - );
89 - $id_num = $dbw->insertId();
90 - }
91 - return $id_num === false ? 0 : $id_num;
92 - }
93 -
94 -function contributionTrackingConnection() {
95 - global $wgContributionTrackingDBserver, $wgContributionTrackingDBname;
96 - global $wgContributionTrackingDBuser, $wgContributionTrackingDBpassword;
97 -
98 - static $db;
99 -
100 - if ( !$db ) {
101 - $db = new DatabaseMysql(
102 - $wgContributionTrackingDBserver,
103 - $wgContributionTrackingDBuser,
104 - $wgContributionTrackingDBpassword,
105 - $wgContributionTrackingDBname );
106 - $db->query( "SET names utf8" );
107 - }
108 -
109 - return $db;
110 -}
Index: trunk/extensions/ContributionTracking/modules/jquery.contributionTracking.js
@@ -0,0 +1,115 @@
 2+/**
 3+ * Turns any form with the bare minimum "appropriate" fields into a form that
 4+ * can get a donor to a gateway with no interstitial page.
 5+ * To use:
 6+ * *) Install the ContributionTracking Extension.
 7+ * *) Include this module on a page.
 8+ * *) On that page, create a form that contains (at least) the fields
 9+ * required by ApiContributionTracking.
 10+ * *) Make sure that form's submit button has a unique ID.
 11+ * *) Assign that button the class of "ajax_me".
 12+ *
 13+ * @author Katie Horn <khorn@wikimedia.org>
 14+ */
 15+
 16+( function( $ ) {
 17+
 18+ /**
 19+ * Binds the onclick function to everything with a class of "ajax_me".
 20+ */
 21+ $.bindAjaxControls = function(){
 22+ $(".ajax_me:disabled").removeAttr("disabled");
 23+ $(".ajax_me").click(function() {
 24+ this.disabled = true;
 25+ $.goAjax(this.id);
 26+ return false; //prevent regular form submission.
 27+ //TODO: Think about the button disabling and enabling.
 28+ //TODO: also think about a barber pole. That would go here.
 29+ });
 30+ }
 31+
 32+ /**
 33+ * Turns the first parent form from the passing object, to an object we can
 34+ * pass to the API.
 35+ * Takes the button ID in string form.
 36+ */
 37+ $.serializeForm = function(buttonID){
 38+ buttonID = "#" + buttonID;
 39+ var form = $(buttonID).parents("form:first");
 40+
 41+ var serializedForm = form.serializeArray();
 42+ var finalObj = {};
 43+
 44+ for (key in serializedForm){
 45+ if(serializedForm[key]['value'] != ""){
 46+ finalObj[serializedForm[key]['name']] = serializedForm[key]['value'];
 47+ }
 48+ }
 49+ return finalObj;
 50+ }
 51+
 52+ /**
 53+ * Sends the formatted ajax request to the API, turns the result into a
 54+ * hidden form, and immediately posts that form on return.
 55+ * Takes the button ID in string form.
 56+ */
 57+ $.goAjax = function(buttonID) {
 58+
 59+ var postData = $.serializeForm(buttonID);
 60+ postData.action = "contributiontracking";
 61+ postData.format = "json";
 62+ //$.debugPostObjectWithAlert(postData);
 63+
 64+ var processAjaxReturn = function(data, status){
 65+ //TODO: Improve the language of the success and error dialogs.
 66+
 67+ if(status != "success"){
 68+ window.alert("Status: " + status);
 69+ $(buttonID).removeAttr("disabled");
 70+ $(".ajax_me:disabled").removeAttr("disabled");
 71+ return;
 72+ }
 73+
 74+ if(data["error"]){
 75+ //TODO: localization. And i18n. And stuff.
 76+ window.alert("The following error has occurred:\r\n" + data["error"]["info"]);
 77+ $(buttonID).removeAttr("disabled");
 78+ $(".ajax_me:disabled").removeAttr("disabled");
 79+ return;
 80+ }
 81+
 82+ if ($('#hideyform').length){
 83+ $('#hideyform').empty(); //just in case something is already hiding in the hideyform.
 84+ } else {
 85+ $('<div id="hideyform"></div>').appendTo('body');
 86+ }
 87+ $('<form id="immediate_repost" action="' + data["returns"]["action"]["url"] + '"></form>').appendTo('#hideyform');
 88+ for (key in data["returns"]["fields"]) {
 89+ $('<input type="hidden" id="' + key +'" name="' + key +'" value="' + data["returns"]["fields"][key] +'">').appendTo('#immediate_repost');
 90+ }
 91+ $('#immediate_repost').submit();
 92+
 93+ }
 94+
 95+ $.post(
 96+ mw.config.get('wgScriptPath') + '/api.php',
 97+ postData,
 98+ processAjaxReturn,
 99+ 'json');
 100+ }
 101+
 102+ /**
 103+ * Just for easy debugging. Should not actually be called anywhere.
 104+ * TODO: Take this out when we know we're done here.
 105+ */
 106+ $.debugPostObjectWithAlert = function(object){
 107+ var contents = "";
 108+ for (key in object){
 109+ contents += key + " = " + object[key] + "\r\n";
 110+ }
 111+ window.alert(contents);
 112+ }
 113+
 114+} )( jQuery );
 115+
 116+$.bindAjaxControls();
\ No newline at end of file
Property changes on: trunk/extensions/ContributionTracking/modules/jquery.contributionTracking.js
___________________________________________________________________
Added: svn:eol-style
1117 + native
Index: trunk/extensions/ContributionTracking/ContributionTracking_Tester.php
@@ -0,0 +1,67 @@
 2+<?php
 3+
 4+/**
 5+ * This is a page that exists solely for the purpose of manually testing all
 6+ * aspects of the ContributionTracking API: Both send (querystring) and receive
 7+ * (jquery processing and reposting). Could also be used for browser-based
 8+ * regression testing of these components.
 9+ * The form is built with all the fields the API will let through the filter.
 10+ * Required are marked with "***".
 11+ * This page is only visible to sysops.
 12+ */
 13+class ContributionTrackingTester extends UnlistedSpecialPage {
 14+
 15+ function __construct() {
 16+ parent::__construct( 'ContributionTrackingTester', 'ViewContributionTrackingTester' );
 17+ }
 18+
 19+ function execute( $language ) {
 20+ global $wgUser;
 21+ if ( !$this->userCanExecute( $wgUser ) ) {
 22+ $this->displayRestrictionError();
 23+ return;
 24+ }
 25+
 26+ global $wgRequest, $wgOut, $wgContributionTrackingReturnToURLDefault;
 27+
 28+ $wgOut->addModules( 'jquery.contributionTracking' );
 29+
 30+ if ( !preg_match( '/^[a-z-]+$/', $language ) ) {
 31+ $language = 'en';
 32+ }
 33+ $this->lang = Language::factory( $language );
 34+
 35+ $this->setHeaders();
 36+
 37+ $apiObj = new ApiContributionTracking( null, null );
 38+ $formfields = $apiObj->getFinalParams();
 39+
 40+ //$wgOut->addWikiText(print_r($formfields, true));
 41+ $wgOut->addHTML( '<form id="landingpage_submit"><table>' );
 42+
 43+ foreach ( $formfields as $name => $attribs ) {
 44+ if ( array_key_exists( 8, $attribs ) ) {
 45+ $required = "***";
 46+ } else {
 47+ $required = "";
 48+ }
 49+ $wgOut->addHTML( '<tr><td>' . $required . $name . '</td><td>' );
 50+ if ( $attribs[2] == 'string' ) {
 51+ $wgOut->addHTML( '<input type="text" id="' . $name . '" name="' . $name . '">' );
 52+ }
 53+ if ( $attribs[2] == 'boolean' ) {
 54+ $wgOut->addHTML( '<input type="checkbox" id="' . $name . '" name="' . $name . '">' );
 55+ }
 56+
 57+ $wgOut->addHTML( '</td></tr>' );
 58+ }
 59+
 60+ $wgOut->addHTML( '<tr><td colspan=2 align=center><button id="ajax_contribution" class="ajax_me">Fire away!</button></td></tr>' );
 61+ $wgOut->addHTML( '</table></form>' );
 62+ }
 63+
 64+ function msgWiki( $key ) {
 65+ return wfMsgExt( $key, array( 'parse', 'language' => $this->lang ) );
 66+ }
 67+
 68+}
Property changes on: trunk/extensions/ContributionTracking/ContributionTracking_Tester.php
___________________________________________________________________
Added: svn:eol-style
169 + native

Follow-up revisions

RevisionCommit summaryAuthorDate
r94744Followup r94720...reedy11:41, 17 August 2011
r94791r94720...khorn18:54, 17 August 2011
r94809r94720 r94771...khorn20:09, 17 August 2011
r95040Stylistic change to the mergeArrayDeafults function in ContributionTracking....khorn20:55, 19 August 2011
r95254r94809: This will now use the updater object for schema changes only if the e...khorn21:12, 22 August 2011

Comments

#Comment by Reedy (talk | contribs)   11:43, 17 August 2011
+	public function getVersion() {
+		return "1.0"; //Is this really _that_ arbitrary?
+	}

Not really, fixed it in r94744. We tend to use svn:keywords Id, and let svn do the rest of it. Not a big deal

+  `owa_session` varchar(255) default NULL,
+  `owa_ref` int(11) default NULL,

You'll want to create a database patch for that too, ie so existing tables can be updated without having to kill and rebuild

#Comment by Khorn (WMF) (talk | contribs)   16:27, 17 August 2011

Thanks, Reedy. I'll have a look at the database patching mechanisms and do a followup.

#Comment by Reedy (talk | contribs)   16:34, 17 August 2011

Cool.

It's literally just an "ALTER TABLE foo" type statement. Plenty of examples in the extensions directory if you need them :)

#Comment by Khorn (WMF) (talk | contribs)   16:38, 17 August 2011

Great! Doesn't sound too hard. :) I should probably mention that the live tables already have had these changes made: We only noticed there should be a schema change in the code when I started using ContributionTracking on my local instance and it kept throwing errors looking for columns that didn't exist... :/

#Comment by Awjrichards (talk | contribs)   00:57, 19 August 2011

Sylistic point:

static function mergeArrayDefaults( $params, $defaults, $nullify=false ) {
+		foreach ( $defaults as $key => $value ) {
+			if ( array_key_exists( $key, $params ) ) {
+				$defaults[$key] = $params[$key];
+			}
+			if ( $nullify && $defaults[$key] === '' ) {
+				$defaults[$key] = null;
+			}
+		}
+		return $defaults;
+	}

It would be clearer if you reassigned $defaults to a variable other than $defaults, since as soon as you start modifying the values in $defaults, they are no longer default values.

In ContributionTracking_body.php, you changed the msg() function to:

+	function msg() {
+		return wfMsgExt( func_get_arg( 0 ), array( 'escape', 'language' => $this->lang ) );
 	}

But left msg() in ContributionTracking.processor.php with the $key argument. Was this on purpose? Similarly, the msgWiki() functions also have the $key argument. Also on purpose, or should they be changed? Iirc, there was a good reason why you took the explicit arg out and started using func_get_arg().

Otherwise, this looks great - this is heading in a good direction!

#Comment by Khorn (WMF) (talk | contribs)   20:51, 19 August 2011

In reverse order: I took out the $key argument in ContributionTracking_body because the class's grandparent (SpecialPage) already has a msg() function defined with no args. Leaving it with the $key arg as it was in ContributionTracking_body causes strict error reporting to go nuts, to the point it occasionally doesn't report more serious errors at all. The other classes in the extension do not have that particular inheritance problem. However: Yes, it's totally inconsistent. :)

I will change the var name in the first point momentarily.

Status & tagging log