Index: trunk/extensions/ContributionTracking/ContributionTracking.sql |
— | — | @@ -10,6 +10,8 @@ |
11 | 11 | `optout` tinyint(1) unsigned NOT NULL, |
12 | 12 | `language` varchar(8) default NULL, |
13 | 13 | `ts` char(14) default NULL, |
| 14 | + `owa_session` varchar(255) default NULL, |
| 15 | + `owa_ref` int(11) default NULL, |
14 | 16 | PRIMARY KEY (`id`), |
15 | 17 | UNIQUE KEY `contribution_id` (`contribution_id`), |
16 | 18 | 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 |
1 | 199 | + 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 |
1 | 287 | + 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 |
1 | 431 | + 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 |
1 | 332 | + native |
Index: trunk/extensions/ContributionTracking/ContributionTracking_body.php |
— | — | @@ -1,177 +1,87 @@ |
2 | 2 | <?php |
3 | 3 | |
4 | 4 | class ContributionTracking extends UnlistedSpecialPage { |
| 5 | + |
5 | 6 | function __construct() { |
6 | 7 | parent::__construct( 'ContributionTracking' ); |
7 | 8 | } |
8 | 9 | |
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 | | - |
31 | 10 | function execute( $language ) { |
32 | | - global $wgRequest, $wgOut, $wgContributionTrackingPayPalIPN, $wgContributionTrackingReturnToURLDefault, |
33 | | - $wgContributionTrackingPayPalRecurringIPN, $wgContributionTrackingPayPalBusiness; |
34 | | - |
| 11 | + global $wgRequest, $wgOut, $wgContributionTrackingReturnToURLDefault; |
| 12 | + |
35 | 13 | if ( !preg_match( '/^[a-z-]+$/', $language ) ) { |
36 | 14 | $language = 'en'; |
37 | 15 | } |
38 | 16 | $this->lang = Language::factory( $language ); |
39 | | - |
| 17 | + |
40 | 18 | $this->setHeaders(); |
41 | | - |
| 19 | + |
42 | 20 | $gateway = $wgRequest->getText( 'gateway' ); |
43 | | - if( !in_array( $gateway, array( 'paypal', 'moneybookers' ) ) ) { |
| 21 | + if ( !in_array( $gateway, array( 'paypal', 'moneybookers' ) ) ) { |
44 | 22 | $wgOut->showErrorPage( 'contrib-tracking-error', 'contrib-tracking-error-text' ); |
45 | 23 | return; |
46 | 24 | } |
47 | | - |
48 | | - $db = contributionTrackingConnection(); |
49 | 25 | |
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 ); |
55 | 44 | } |
56 | 45 | |
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, |
69 | 59 | ); |
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' ); |
76 | 64 | } |
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'; |
99 | 65 | |
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] ); |
107 | 69 | } |
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 | | - } |
137 | 70 | } |
138 | | - else if ( $gateway == 'moneybookers' ) { |
139 | | - $action = 'https://www.moneybookers.com/app/payment.pl'; |
140 | 71 | |
141 | | - // Tracking |
142 | | - $repost['merchant_fields'] = 'os0'; |
| 72 | + $repost = ContributionTrackingProcessor::getRepostFields( $params ); |
143 | 73 | |
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 | | - |
164 | 74 | $wgOut->addWikiText( "{{2009/Donate-banner/$language}}" ); |
165 | 75 | $wgOut->addHTML( $this->msgWiki( 'contrib-tracking-submitting' ) ); |
166 | | - |
| 76 | + |
167 | 77 | // Output the repost form |
168 | | - $output = '<form method="post" name="contributiontracking" action="' . $action . '">'; |
| 78 | + $output = '<form method="post" name="contributiontracking" action="' . $repost['action'] . '">'; |
169 | 79 | |
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 ) . '" />'; |
172 | 82 | } |
173 | | - |
| 83 | + |
174 | 84 | $output .= $this->msgWiki( 'contrib-tracking-redirect' ); |
175 | | - |
| 85 | + |
176 | 86 | // Offer a button to post the form if the user has no Javascript support |
177 | 87 | $output .= '<noscript>'; |
178 | 88 | $output .= $this->msgWiki( 'contrib-tracking-continue' ); |
— | — | @@ -184,14 +94,12 @@ |
185 | 95 | |
186 | 96 | // Automatically post the form if the user has Javascript support |
187 | 97 | $wgOut->addHTML( '<script type="text/javascript">document.contributiontracking.submit();</script>' ); |
188 | | - |
189 | 98 | } |
190 | 99 | |
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 ) ); |
193 | 102 | } |
194 | 103 | |
195 | | - |
196 | 104 | function msgWiki( $key ) { |
197 | 105 | return wfMsgExt( $key, array( 'parse', 'language' => $this->lang ) ); |
198 | 106 | } |
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 |
1 | 424 | + native |
Index: trunk/extensions/ContributionTracking/ContributionTracking.php |
— | — | @@ -23,9 +23,31 @@ |
24 | 24 | $wgExtensionAliasesFiles['ContributionTracking'] = $dir . 'ContributionTracking.alias.php'; |
25 | 25 | $wgAutoloadClasses['ContributionTracking'] = $dir . 'ContributionTracking_body.php'; |
26 | 26 | $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 | + |
27 | 38 | //this only works if contribution tracking is inside a mediawiki DB, which typically it isn't. |
28 | | -//$wgHooks['LoadExtensionSchemaUpdates'][] = 'efContributionTrackingLoadUpdates'; |
| 39 | +//$wgHooks['LoadExtensionSchemaUpdates'][] = 'efContributionTrackingLoadUpdates'; |
29 | 40 | |
| 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 | + |
30 | 52 | /** |
31 | 53 | * The default 'return to' URL for a thank you page after posting to the contribution |
32 | 54 | * |
— | — | @@ -53,6 +75,19 @@ |
54 | 76 | */ |
55 | 77 | $wgContributionTrackingPayPalBusiness = 'donations@wikimedia.org'; |
56 | 78 | |
| 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 | + |
57 | 92 | function efContributionTrackingLoadUpdates(){ |
58 | 93 | global $wgExtNewTables, $wgExtNewFields; |
59 | 94 | $dir = dirname( __FILE__ ) . '/'; |
— | — | @@ -67,43 +102,3 @@ |
68 | 103 | return true; |
69 | 104 | |
70 | 105 | } |
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 |
1 | 117 | + 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 |
1 | 69 | + native |