├── .gitattributes ├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── Configs ├── redirector.cfg ├── sample.cfg └── scriptdefault.cfg ├── GenFunctions.php ├── HTTP.php ├── Includes ├── AutoUpdate.php ├── Autoloader.php ├── Exceptions.php ├── Hooks.php ├── Image.php ├── Page.php ├── Peachy.php ├── PeachyAWBFunctions.php ├── SSH.php ├── User.php ├── Wiki.php ├── XMLParse.php ├── lime.php ├── limeColorizer.php ├── limeCoverage.php ├── limeHarness.php ├── limeOutput.php ├── limeRegistration.php └── lime_test.php ├── Init.php ├── LICENSE ├── Plugins ├── AbuseFilter.php ├── CheckUser.php ├── CodeReview.php ├── Email.php ├── FlaggedRevs.php ├── GlobalBlocking.php ├── GlobalUserInfo.php ├── IRC.php ├── RFA.php ├── RPED.php ├── SiteMatrix.php └── Xml.php ├── README.md ├── Tests ├── GenFunctionsTest.php ├── Includes │ ├── AutoUpdateTest.php │ ├── PageTest.php │ └── PeachyAWBFunctionsTest.php ├── bootstrap.php └── phplint.sh ├── config.inc.php ├── phpdoc.dist.xml └── phpunit.xml.dist /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | */phpunit.phar 2 | 3 | 4 | ################# 5 | ## PhpStorm 6 | ################# 7 | .idea/ 8 | 9 | ################# 10 | ## Eclipse 11 | ################# 12 | 13 | *.pydevproject 14 | .project 15 | .metadata 16 | bin/ 17 | tmp/ 18 | *.tmp 19 | *.bak 20 | *.swp 21 | *~.nib 22 | local.properties 23 | .classpath 24 | .settings/ 25 | .loadpath 26 | 27 | # External tool builders 28 | .externalToolBuilders/ 29 | 30 | # Locally stored "Eclipse launch configurations" 31 | *.launch 32 | 33 | # CDT-specific 34 | .cproject 35 | 36 | # PDT-specific 37 | .buildpath 38 | 39 | 40 | ################# 41 | ## Visual Studio 42 | ################# 43 | 44 | ## Ignore Visual Studio temporary files, build results, and 45 | ## files generated by popular Visual Studio add-ons. 46 | 47 | # User-specific files 48 | *.suo 49 | *.user 50 | *.sln.docstates 51 | 52 | # Build results 53 | 54 | [Dd]ebug/ 55 | [Rr]elease/ 56 | x64/ 57 | build/ 58 | [Bb]in/ 59 | [Oo]bj/ 60 | 61 | # MSTest test Results 62 | [Tt]est[Rr]esult*/ 63 | [Bb]uild[Ll]og.* 64 | 65 | *_i.c 66 | *_p.c 67 | *.ilk 68 | *.meta 69 | *.obj 70 | *.pch 71 | *.pdb 72 | *.pgc 73 | *.pgd 74 | *.rsp 75 | *.sbr 76 | *.tlb 77 | *.tli 78 | *.tlh 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.scc 86 | 87 | # Visual C++ cache files 88 | ipch/ 89 | *.aps 90 | *.ncb 91 | *.opensdf 92 | *.sdf 93 | *.cachefile 94 | 95 | # Visual Studio profiler 96 | *.psess 97 | *.vsp 98 | *.vspx 99 | 100 | # Guidance Automation Toolkit 101 | *.gpState 102 | 103 | # ReSharper is a .NET coding add-in 104 | _ReSharper*/ 105 | *.[Rr]e[Ss]harper 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | *.ncrunch* 115 | .*crunch*.local.xml 116 | 117 | # Installshield output folder 118 | [Ee]xpress/ 119 | 120 | # DocProject is a documentation generator add-in 121 | DocProject/buildhelp/ 122 | DocProject/Help/*.HxT 123 | DocProject/Help/*.HxC 124 | DocProject/Help/*.hhc 125 | DocProject/Help/*.hhk 126 | DocProject/Help/*.hhp 127 | DocProject/Help/Html2 128 | DocProject/Help/html 129 | 130 | # Click-Once directory 131 | publish/ 132 | 133 | # Publish Web Output 134 | *.Publish.xml 135 | *.pubxml 136 | 137 | # NuGet Packages Directory 138 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 139 | #packages/ 140 | 141 | # Windows Azure Build Output 142 | csx 143 | *.build.csdef 144 | 145 | # Windows Store app package directory 146 | AppPackages/ 147 | 148 | # Others 149 | sql/ 150 | *.Cache 151 | ClientBin/ 152 | [Ss]tyle[Cc]op.* 153 | ~$* 154 | *~ 155 | *.dbmdl 156 | *.[Pp]ublish.xml 157 | *.pfx 158 | *.publishsettings 159 | 160 | # RIA/Silverlight projects 161 | Generated_Code/ 162 | 163 | # Backup & report files from converting an old project file to a newer 164 | # Visual Studio version. Backup files are not needed, because we have git ;-) 165 | _UpgradeReport_Files/ 166 | Backup*/ 167 | UpgradeLog*.XML 168 | UpgradeLog*.htm 169 | 170 | # SQL Server files 171 | App_Data/*.mdf 172 | App_Data/*.ldf 173 | 174 | ############# 175 | ## Windows detritus 176 | ############# 177 | 178 | # Windows image file caches 179 | Thumbs.db 180 | ehthumbs.db 181 | 182 | # Folder config file 183 | Desktop.ini 184 | 185 | # Recycle Bin used on file shares 186 | $RECYCLE.BIN/ 187 | 188 | # Mac crap 189 | .DS_Store 190 | 191 | 192 | ############# 193 | ## Python 194 | ############# 195 | 196 | *.py[co] 197 | 198 | # Packages 199 | *.egg 200 | *.egg-info 201 | dist/ 202 | eggs/ 203 | parts/ 204 | var/ 205 | sdist/ 206 | develop-eggs/ 207 | .installed.cfg 208 | 209 | # Installer logs 210 | pip-log.txt 211 | 212 | # Unit test / coverage reports 213 | .coverage 214 | .tox 215 | 216 | #Translations 217 | *.mo 218 | 219 | #Mr Developer 220 | .mr.developer.cfg 221 | 222 | #peachy stuff 223 | *.cfg 224 | test.php 225 | acc.php 226 | config.local.inc.php 227 | Includes/updateversion 228 | Includes/SSHCore 229 | Includes/phpseclibupdate 230 | 231 | !Configs/redirector.cfg 232 | !Configs/sample.cfg 233 | !Configs/scriptdefault.cfg -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | inherit: true 2 | 3 | tools: 4 | php_code_sniffer: true 5 | php_cpd: true 6 | php_cs_fixer: true 7 | php_loc: true 8 | php_mess_detector: true 9 | php_pdepend: true 10 | php_analyzer: true 11 | sensiolabs_security_checker: true 12 | external_code_coverage: true -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.5 5 | - 5.4 6 | - 5.3 7 | 8 | script: 9 | - ./Tests/phplint.sh 10 | - phpunit --coverage-clover=coverage.clover 11 | 12 | after_script: 13 | - wget https://scrutinizer-ci.com/ocular.phar 14 | - php ocular.phar code-coverage:upload --format=php-clover coverage.clover 15 | 16 | notifications: 17 | irc: 18 | channels: 19 | - "chat.freenode.net##add" 20 | on_success: change 21 | on_failure: always -------------------------------------------------------------------------------- /Configs/redirector.cfg: -------------------------------------------------------------------------------- 1 | [config] 2 | useconfig = "path/to/another/config.cfg" -------------------------------------------------------------------------------- /Configs/sample.cfg: -------------------------------------------------------------------------------- 1 | [config] 2 | baseurl = "https://en.wikipedia.org/w/api.php" 3 | username = "Example" 4 | password = "1234567890" 5 | maxlag = "0" 6 | editsperminute = "0" 7 | httpecho = "true" 8 | consumerkey = "foo" 9 | consumersecret = "foobar" 10 | accesstoken = "bla" 11 | accesssecret = "blabar" 12 | oauthurl = "https://en.wikipedia.org/w/index.php?title=Special:OAuth" 13 | method = "legacy" -------------------------------------------------------------------------------- /Configs/scriptdefault.cfg: -------------------------------------------------------------------------------- 1 | [config] 2 | useconfig = "admin" -------------------------------------------------------------------------------- /GenFunctions.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | /** 21 | * @file 22 | * Stores general functions that do not belong in a class 23 | */ 24 | 25 | /** 26 | * Case insensitive in_array function 27 | * 28 | * @param mixed $needle What to search for 29 | * @param array $haystack Array to search in 30 | * @param bool $strict 31 | * @return bool True if $needle is found in $haystack, case insensitive 32 | * @link http://us3.php.net/in_array 33 | */ 34 | function iin_array( $needle, $haystack, $strict = false ) { 35 | return in_array_recursive( strtoupper_safe( $needle ), array_map( 'strtoupper_safe', $haystack ), $strict ); 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | function strtoupper_safe( $str ) { 42 | if( is_string( $str ) ) return strtoupper( $str ); 43 | if( is_array( $str ) ) $str = array_map( 'strtoupper_safe', $str ); 44 | return $str; 45 | } 46 | 47 | /** 48 | * Returns whether or not a string is found in another 49 | * Shortcut for strpos() 50 | * 51 | * @param string $needle What to search for 52 | * @param string $haystack What to search in 53 | * @param bool $insensitive Whether or not to do a case-insensitive search 54 | * @return bool True if $needle is found in $haystack 55 | * @link http://us3.php.net/strpos 56 | */ 57 | function in_string( $needle, $haystack, $insensitive = false ) { 58 | $fnc = 'strpos'; 59 | if( $insensitive ) $fnc = 'stripos'; 60 | 61 | return $fnc( $haystack, $needle ) !== false; 62 | } 63 | 64 | /** 65 | * Recursive in_array function 66 | * 67 | * @param string $needle What to search for 68 | * @param array $haystack What to search in 69 | * @param bool $insensitive Whether or not to do a case-insensitive search 70 | * @return bool True if $needle is found in $haystack 71 | * @link http://us3.php.net/in_array 72 | */ 73 | function in_array_recursive( $needle, $haystack, $insensitive = false ) { 74 | $fnc = 'in_array'; 75 | if( $insensitive ) $fnc = 'iin_array'; 76 | 77 | if( $fnc( $needle, $haystack ) ) return true; 78 | foreach( $haystack as $val ){ 79 | if( is_array( $val ) ) { 80 | return in_array_recursive( $needle, $val ); 81 | } 82 | } 83 | return false; 84 | } 85 | 86 | /** 87 | * Recursive glob() function. 88 | * 89 | * @access public 90 | * @param string $pattern . (default: '*') 91 | * @param int $flags . (default: 0) 92 | * @param string $path . (default: '') 93 | * @return array 94 | */ 95 | function rglob( $pattern = '*', $flags = 0, $path = '' ) { 96 | $paths = glob( $path . '*', GLOB_MARK | GLOB_ONLYDIR | GLOB_NOSORT ); 97 | $files = glob( $path . $pattern, $flags ); 98 | foreach( $paths as $path ){ 99 | $files = array_merge( $files, rglob( $pattern, $flags, $path ) ); 100 | } 101 | return $files; 102 | } 103 | 104 | /** 105 | * Detects the presence of a nobots template or one that denies editing by ours 106 | * 107 | * @access public 108 | * @param Wiki $wiki Wiki class 109 | * @param string $text Text of the page to check (default: '') 110 | * @param string $pgUsername Username to search for in the template (default: null) 111 | * @param string|null $optout Text to search for in the optout= parameter. (default: null) 112 | * @param string|null $taskname (default: null) 113 | * @return bool True on match of an appropriate nobots template 114 | */ 115 | function checkExclusion( Wiki $wiki, $text = '', $pgUsername = null, $optout = null, $taskname = null ) { 116 | if( !$wiki->get_nobots() ) return false; 117 | 118 | if( in_string( "{{nobots}}", $text ) ) return true; 119 | if( in_string( "{{bots}}", $text ) ) return false; 120 | 121 | if( preg_match( '/\{\{bots\s*\|\s*allow\s*=\s*(.*?)\s*\}\}/i', $text, $allow ) ) { 122 | if( $allow[1] == "all" ) return false; 123 | if( $allow[1] == "none" ) return true; 124 | $allow = array_map( 'trim', explode( ',', $allow[1] ) ); 125 | if( !is_null( $pgUsername ) && in_array( trim( $pgUsername ), $allow ) ) { 126 | return false; 127 | } 128 | return true; 129 | } 130 | 131 | if( preg_match( '/\{\{(no)?bots\s*\|\s*deny\s*=\s*(.*?)\s*\}\}/i', $text, $deny ) ) { 132 | if( $deny[2] == "all" ) return true; 133 | if( $deny[2] == "none" ) return false; 134 | $allow = array_map( 'trim', explode( ',', $deny[2] ) ); 135 | if( ( !is_null( $pgUsername ) && in_array( trim( $pgUsername ), $allow ) ) || ( !is_null( $taskname ) && in_array( trim( $taskname ), $allow ) ) ) { 136 | return true; 137 | } 138 | return false; 139 | } 140 | 141 | if( !is_null( $optout ) && preg_match( '/\{\{(no)?bots\s*\|\s*optout\s*=\s*(.*?)\s*\}\}/i', $text, $allow ) ) { 142 | if( $allow[1] == "all" ) return true; 143 | $allow = array_map( 'trim', explode( ',', $allow[2] ) ); 144 | if( in_array( trim( $optout ), $allow ) ) { 145 | return true; 146 | } 147 | return false; 148 | } 149 | return false; 150 | } 151 | 152 | /** 153 | * Outputs text if the given category is in the allowed types 154 | * 155 | * @param string $text Text to display 156 | * @param int $cat Category of text, such as PECHO_WARN, PECHO_NORMAL 157 | * @param string $func 158 | * @return void 159 | */ 160 | function outputText( $text, $cat = 0, $func = 'echo' ) { 161 | global $pgVerbose; 162 | 163 | Hooks::runHook( 'OutputText', array( &$text, &$cat, &$func ) ); 164 | 165 | if( in_array( $cat, $pgVerbose ) ) { 166 | if( $func == 'echo' ) { 167 | echo $text; 168 | } else { 169 | $func( $text ); 170 | } 171 | } 172 | } 173 | 174 | /** 175 | * Shortcut for {@link outputText} 176 | * 177 | * @param string $text Text to display 178 | * @param int $cat Category of text, such as PECHO_WARN, PECHO_NORMAL 179 | * @param string $func 180 | * @link outputText 181 | * @return void 182 | */ 183 | function pecho( $text, $cat = 0, $func = 'echo' ) { 184 | global $pgWebOutput; 185 | if( $pgWebOutput ) $text = str_replace( "\n", "
", $text ); 186 | outputText( $text, $cat, $func ); 187 | } 188 | 189 | /** 190 | * Echo function with color capabilities. 191 | * 192 | * Syntax: 193 | * 194 | * [Text to colorize|NAME] where NAME is the name of a defined style. For example: 195 | * 196 | * This text is standard terminal color. [This text will be yellow.|COMMENT] [This text will be white on red.|ERROR] 197 | * 198 | * Defined styles: 199 | * 209 | * 210 | * You can define your own styles by using this syntax: 211 | * 212 | * lime_colorizer::style('STYLE_NAME', array('bg' => 'red', 'fg' => 'white')); 213 | * 214 | * (Available colors: black, red, green, yellow, blue, magenta, cyan, white) 215 | * 216 | * You can also set options for how the text is formatted (not available on all systems): 217 | * 218 | * lime_colorizer::style('STYLE_NAME', array('bg' => 'red', 'fg' => 'white', 'bold' => true )); (sets bold text) 219 | * 220 | * Available options: bold, underscore, blink, reverse, conceal 221 | * 222 | * 223 | * @access public 224 | * @param string $text 225 | * @param bool $return 226 | * @return string 227 | */ 228 | function cecho( $text, $return = false ) { 229 | global $pgColorizer; 230 | 231 | if( !isset( $pgColorizer ) ) $pgColorizer = new lime_colorizer( true ); 232 | 233 | $text = preg_replace_callback( '/\[(.+?)\|(\w+)\]/s', function ($m) { 234 | global $pgColorizer; 235 | return $pgColorizer->colorize($m[1], $m[2]); 236 | }, $text ); 237 | if( $return ) return $text; 238 | 239 | echo $text; 240 | 241 | } 242 | 243 | 244 | /** 245 | * Generates a diff between two strings 246 | * 247 | * @package Text_Diff 248 | * @deprecated since 18 June 2013 249 | */ 250 | function getTextDiff() { 251 | Peachy::deprecatedWarn( 'getTextDiff()', 'Diff::load()' ); 252 | $args = func_get_args(); 253 | return call_user_func_array( array( 'Diff', 'load' ), $args ); 254 | } 255 | 256 | /** 257 | * Gets the first defined Wiki object 258 | * 259 | * @return Wiki|bool 260 | * @package initFunctions 261 | */ 262 | function &getSiteObject() { 263 | 264 | foreach( $GLOBALS as $var ){ 265 | if( is_object( $var ) ) { 266 | if( get_class( $var ) == "Wiki" ) { 267 | return $var; 268 | } 269 | } 270 | } 271 | 272 | return false; 273 | } 274 | 275 | /** 276 | * Returns an instance of the Page class as specified by $title or $pageid 277 | * 278 | * @param mixed $title Title of the page (default: null) 279 | * @param mixed $pageid ID of the page (default: null) 280 | * @param bool $followRedir Should it follow a redirect when retrieving the page (default: true) 281 | * @param bool $normalize Should the class automatically normalize the title (default: true) 282 | * @return Page 283 | * @package initFunctions 284 | */ 285 | function &initPage( $title = null, $pageid = null, $followRedir = true, $normalize = true ) { 286 | $wiki = getSiteObject(); 287 | if( !$wiki ) return false; 288 | 289 | $page = new Page( $wiki, $title, $pageid, $followRedir, $normalize ); 290 | return $page; 291 | } 292 | 293 | /** 294 | * Returns an instance of the User class as specified by $pgUsername 295 | * 296 | * @param mixed $pgUsername Username 297 | * @return User|false 298 | * @package initFunctions 299 | */ 300 | function &initUser( $pgUsername ) { 301 | $wiki = getSiteObject(); 302 | if( !$wiki ) return false; 303 | return new User( $wiki, $pgUsername ); 304 | } 305 | 306 | /** 307 | * Returns an instance of the Image class as specified by $filename or $pageid 308 | * 309 | * @param string $filename Filename 310 | * @return Image 311 | * @package initFunctions 312 | */ 313 | function &initImage( $filename = null ) { 314 | 315 | $wiki = getSiteObject(); 316 | if( !$wiki ) return false; 317 | 318 | $image = new Image( $wiki, $filename ); 319 | return $image; 320 | } 321 | 322 | if( !function_exists( 'mb_strlen' ) ) { 323 | 324 | /** 325 | * Fallback implementation of mb_strlen. 326 | * 327 | * @link http://svn.wikimedia.org/svnroot/mediawiki/trunk/phase3/includes/GlobalFunctions.php 328 | * @param string $str String to get 329 | * @return int 330 | * @package Fallback 331 | */ 332 | function mb_strlen( $str ) { 333 | $counts = count_chars( $str ); 334 | $total = 0; 335 | 336 | // Count ASCII bytes 337 | for( $i = 0; $i < 0x80; $i++ ){ 338 | $total += $counts[$i]; 339 | } 340 | 341 | // Count multibyte sequence heads 342 | for( $i = 0xc0; $i < 0xff; $i++ ){ 343 | $total += $counts[$i]; 344 | } 345 | 346 | return $total; 347 | } 348 | } 349 | 350 | if( !function_exists( 'mb_substr' ) ) { 351 | /** 352 | * Fallback implementation for mb_substr. This is VERY slow, from 5x to 100x slower. Use only if necessary. 353 | * @link http://svn.wikimedia.org/svnroot/mediawiki/trunk/phase3/includes/GlobalFunctions.php 354 | * @package Fallback 355 | */ 356 | function mb_substr( $str, $start, $count = 'end' ) { 357 | if( $start != 0 ) { 358 | $split = mb_substr_split_unicode( $str, intval( $start ) ); 359 | $str = substr( $str, $split ); 360 | } 361 | 362 | if( $count !== 'end' ) { 363 | $split = mb_substr_split_unicode( $str, intval( $count ) ); 364 | $str = substr( $str, 0, $split ); 365 | } 366 | 367 | return $str; 368 | } 369 | 370 | /** 371 | * Continuing support for mb_substr. Do not use. 372 | * @link http://svn.wikimedia.org/svnroot/mediawiki/trunk/phase3/includes/GlobalFunctions.php 373 | * @package Fallback 374 | * @param integer $splitPos 375 | * @return int 376 | */ 377 | function mb_substr_split_unicode( $str, $splitPos ) { 378 | if( $splitPos == 0 ) { 379 | return 0; 380 | } 381 | 382 | $byteLen = strlen( $str ); 383 | 384 | if( $splitPos > 0 ) { 385 | if( $splitPos > 256 ) { 386 | // Optimize large string offsets by skipping ahead N bytes. 387 | // This will cut out most of our slow time on Latin-based text, 388 | // and 1/2 to 1/3 on East European and Asian scripts. 389 | $bytePos = $splitPos; 390 | while( $bytePos < $byteLen && $str{$bytePos} >= "\x80" && $str{$bytePos} < "\xc0" ){ 391 | ++$bytePos; 392 | } 393 | $charPos = mb_strlen( substr( $str, 0, $bytePos ) ); 394 | } else { 395 | $charPos = 0; 396 | $bytePos = 0; 397 | } 398 | 399 | while( $charPos++ < $splitPos ){ 400 | ++$bytePos; 401 | // Move past any tail bytes 402 | while( $bytePos < $byteLen && $str{$bytePos} >= "\x80" && $str{$bytePos} < "\xc0" ){ 403 | ++$bytePos; 404 | } 405 | } 406 | } else { 407 | $splitPosX = $splitPos + 1; 408 | $charPos = 0; // relative to end of string; we don't care about the actual char position here 409 | $bytePos = $byteLen; 410 | while( $bytePos > 0 && $charPos-- >= $splitPosX ){ 411 | --$bytePos; 412 | // Move past any tail bytes 413 | while( $bytePos > 0 && $str{$bytePos} >= "\x80" && $str{$bytePos} < "\xc0" ){ 414 | --$bytePos; 415 | } 416 | } 417 | } 418 | 419 | return $bytePos; 420 | } 421 | } 422 | 423 | if( !function_exists( 'iconv' ) ) { 424 | /** 425 | * Fallback iconv function. 426 | * 427 | * iconv support is not in the default configuration and so may not be present. 428 | * Assume will only ever use utf-8 and iso-8859-1. 429 | * This will *not* work in all circumstances. 430 | * 431 | * @access public 432 | * @param mixed $from 433 | * @param mixed $to 434 | * @param mixed $string 435 | * @return void 436 | * @package Fallback 437 | */ 438 | function iconv( $from, $to, $string ) { 439 | if( substr( $to, -8 ) == '//IGNORE' ) $to = substr( $to, 0, strlen( $to ) - 8 ); 440 | if( strcasecmp( $from, $to ) == 0 ) return $string; 441 | if( strcasecmp( $from, 'utf-8' ) == 0 ) return utf8_decode( $string ); 442 | if( strcasecmp( $to, 'utf-8' ) == 0 ) return utf8_encode( $string ); 443 | return $string; 444 | } 445 | } 446 | 447 | if( !function_exists( 'istainted' ) ) { 448 | 449 | /** 450 | * Fallback istainted function. 451 | * 452 | * @access public 453 | * @param mixed $var 454 | * @return integer 455 | * @package Fallback 456 | */ 457 | function istainted( $var ) { 458 | return 0; 459 | } 460 | 461 | /** 462 | * Fallback taint function. 463 | * 464 | * @access public 465 | * @param mixed $var 466 | * @param int $level 467 | * @return void 468 | * @package Fallback 469 | */ 470 | function taint( $var, $level = 0 ) { } 471 | 472 | /** 473 | * Fallback untaint function. 474 | * 475 | * @access public 476 | * @param mixed $var 477 | * @param int $level 478 | * @return void 479 | * @package Fallback 480 | */ 481 | function untaint( $var, $level = 0 ) { } 482 | 483 | /** 484 | * @package Fallback 485 | */ 486 | define( 'TC_HTML', 1 ); 487 | 488 | /** 489 | * @package Fallback 490 | */ 491 | define( 'TC_SHELL', 1 ); 492 | 493 | /** 494 | * @package Fallback 495 | */ 496 | define( 'TC_MYSQL', 1 ); 497 | 498 | /** 499 | * @package Fallback 500 | */ 501 | define( 'TC_PCRE', 1 ); 502 | 503 | /** 504 | * @package Fallback 505 | */ 506 | define( 'TC_SELF', 1 ); 507 | } 508 | -------------------------------------------------------------------------------- /HTTP.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | /** 21 | * @file 22 | * HTTP object 23 | * Stores all cURL functions 24 | */ 25 | 26 | /** 27 | * HTTP Class, stores cURL functions 28 | */ 29 | class HTTP { 30 | 31 | /** 32 | * Curl object 33 | * 34 | * @var resource a cURL handle 35 | * @access private 36 | */ 37 | private $curl_instance; 38 | 39 | /** 40 | * Hash to use for cookies 41 | * 42 | * @var string 43 | * @access private 44 | */ 45 | private $cookie_hash; 46 | 47 | /** 48 | * Whether or not to enable GET:, POST:, and DLOAD: messages being sent to the terminal. 49 | * 50 | * @var bool 51 | * @access private 52 | */ 53 | private $echo; 54 | 55 | /** 56 | * Useragent 57 | * 58 | * @var mixed 59 | * @access private 60 | */ 61 | private $user_agent; 62 | 63 | /** 64 | * Temporary file where cookies are stored 65 | * 66 | * @var mixed 67 | * @access private 68 | */ 69 | private $cookie_jar; 70 | 71 | /** 72 | * @var string|null 73 | */ 74 | private $lastHeader = null; 75 | 76 | /** 77 | * Construction method for the HTTP class 78 | * 79 | * @access public 80 | * 81 | * @param bool $echo Whether or not to enable GET:, POST:, and DLOAD: messages being sent to the terminal. Default false; 82 | * 83 | * @note please consider using HTTP::getDefaultInstance() instead 84 | * 85 | * @throws RuntimeException 86 | * @throws DependencyError 87 | * 88 | * @return HTTP 89 | */ 90 | public function __construct($echo = false) 91 | { 92 | if( !function_exists( 'curl_init' ) ) { 93 | throw new DependencyError( "cURL", "http://us2.php.net/manual/en/curl.requirements.php" ); 94 | } 95 | 96 | $this->echo = $echo; 97 | $this->curl_instance = curl_init(); 98 | if( $this->curl_instance === false ) { 99 | throw new RuntimeException( 'Failed to initialize curl' ); 100 | } 101 | $this->cookie_hash = md5( time() . '-' . rand( 0, 999 ) ); 102 | $this->cookie_jar = sys_get_temp_dir() . 'peachy.cookies.' . $this->cookie_hash . '.dat'; 103 | 104 | $userAgent = 'Peachy MediaWiki Bot API'; 105 | if( defined( 'PEACHYVERSION' ) ) $userAgent .= ' Version ' . PEACHYVERSION; 106 | $this->setUserAgent( $userAgent ); 107 | 108 | Hooks::runHook( 'HTTPNewCURLInstance', array( &$this, &$echo ) ); 109 | 110 | $this->setCookieJar( $this->cookie_jar ); 111 | 112 | curl_setopt( $this->curl_instance, CURLOPT_MAXCONNECTS, 100 ); 113 | curl_setopt( $this->curl_instance, CURLOPT_MAXREDIRS, 10 ); 114 | $this->setCurlHeaders(); 115 | curl_setopt( $this->curl_instance, CURLOPT_ENCODING, 'gzip' ); 116 | curl_setopt( $this->curl_instance, CURLOPT_RETURNTRANSFER, 1 ); 117 | curl_setopt( $this->curl_instance, CURLOPT_HEADER, 1 ); 118 | curl_setopt( $this->curl_instance, CURLOPT_TIMEOUT, 10 ); 119 | curl_setopt( $this->curl_instance, CURLOPT_CONNECTTIMEOUT, 10 ); 120 | 121 | global $pgProxy; 122 | if( isset( $pgProxy ) && count( $pgProxy ) ) { 123 | curl_setopt( $this->curl_instance, CURLOPT_PROXY, $pgProxy['addr'] ); 124 | if( isset( $pgProxy['type'] ) ) { 125 | curl_setopt( $this->curl_instance, CURLOPT_PROXYTYPE, $pgProxy['type'] ); 126 | } 127 | if( isset( $pgProxy['userpass'] ) ) { 128 | curl_setopt( $this->curl_instance, CURLOPT_PROXYUSERPWD, $pgProxy['userpass'] ); 129 | } 130 | if( isset( $pgProxy['port'] ) ) { 131 | curl_setopt( $this->curl_instance, CURLOPT_PROXYPORT, $pgProxy['port'] ); 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * @param array $extraHeaders 138 | */ 139 | private function setCurlHeaders( $extraHeaders = array() ) { 140 | curl_setopt( $this->curl_instance, CURLOPT_HTTPHEADER, array_merge( array( 'Expect:' ), $extraHeaders ) ); 141 | } 142 | 143 | /** 144 | * @param boolean $verifyssl 145 | */ 146 | private function setVerifySSL( $verifyssl = null ) { 147 | if( is_null( $verifyssl ) ) { 148 | global $verifyssl; 149 | } 150 | if( !$verifyssl ) { 151 | curl_setopt( $this->curl_instance, CURLOPT_SSL_VERIFYPEER, false ); 152 | curl_setopt( $this->curl_instance, CURLOPT_SSL_VERIFYHOST, 0 ); 153 | } else { 154 | curl_setopt( $this->curl_instance, CURLOPT_SSL_VERIFYPEER, true ); 155 | //support for value of 1 will be removed in cURL 7.28.1 156 | curl_setopt( $this->curl_instance, CURLOPT_SSL_VERIFYHOST, 2 ); 157 | } 158 | } 159 | 160 | /** 161 | * @param string $cookie_file 162 | */ 163 | public function setCookieJar($cookie_file) 164 | { 165 | $this->cookie_jar = $cookie_file; 166 | 167 | Hooks::runHook( 'HTTPSetCookieJar', array( &$cookie_file ) ); 168 | 169 | curl_setopt( $this->curl_instance, CURLOPT_COOKIEJAR, $cookie_file ); 170 | curl_setopt( $this->curl_instance, CURLOPT_COOKIEFILE, $cookie_file ); 171 | } 172 | 173 | /** 174 | * @param null $user_agent 175 | * @throws BadEntryError 176 | * @throws HookError 177 | */ 178 | public function setUserAgent($user_agent = null) 179 | { 180 | $this->user_agent = $user_agent; 181 | 182 | Hooks::runHook( 'HTTPSetUserAgent', array( &$user_agent ) ); 183 | 184 | curl_setopt( $this->curl_instance, CURLOPT_USERAGENT, $user_agent ); 185 | } 186 | 187 | /** 188 | * @return string|bool Data. False on failure. 189 | * @throws CURLError 190 | */ 191 | private function doCurlExecWithRetrys() { 192 | $data = false; 193 | for( $i = 0; $i <= 20; $i++ ){ 194 | try{ 195 | $response = curl_exec( $this->curl_instance ); 196 | $header_size = curl_getinfo( $this->curl_instance, CURLINFO_HEADER_SIZE ); 197 | $this->lastHeader = substr( $response, 0, $header_size ); 198 | $data = substr( $response, $header_size ); 199 | } catch( Exception $e ){ 200 | if( curl_errno( $this->curl_instance ) != 0 ) { 201 | throw new CURLError( curl_errno( $this->curl_instance ), curl_error( $this->curl_instance ) ); 202 | } 203 | if( $i == 20 ) { 204 | pecho( "Warning: A CURL error occurred. Attempted 20 times. Terminating attempts.", PECHO_WARN ); 205 | return false; 206 | } else { 207 | pecho( "Warning: A CURL error occurred. Details can be found in the PHP error log. Retrying...", PECHO_WARN ); 208 | } 209 | continue; 210 | } 211 | if( !is_null( $data ) && $data !== false ) { 212 | break; 213 | } 214 | } 215 | return $data; 216 | } 217 | 218 | /** 219 | * Get an url with HTTP GET 220 | * 221 | * @access public 222 | * 223 | * @param string $url URL to get 224 | * @param array|null $data Array of data to pass. Gets transformed into the URL inside the function. Default null. 225 | * @param array $headers Array of headers to pass to curl 226 | * @param bool $verifyssl override for the global verifyssl value 227 | * 228 | * @return bool|string Result 229 | */ 230 | public function get( $url, $data = null, $headers = array(), $verifyssl = null ) { 231 | global $argv, $displayGetOutData; 232 | 233 | if( is_string( $headers ) ) curl_setopt( $this->curl_instance, CURLOPT_HTTPHEADER, array( $headers ) ); 234 | else $this->setCurlHeaders( $headers ); 235 | $this->setVerifySSL( $verifyssl ); 236 | 237 | curl_setopt( $this->curl_instance, CURLOPT_FOLLOWLOCATION, 1 ); 238 | curl_setopt( $this->curl_instance, CURLOPT_HTTPGET, 1 ); 239 | curl_setopt( $this->curl_instance, CURLOPT_POST, 0 ); 240 | 241 | /*if( !is_null( $this->use_cookie ) ) { 242 | curl_setopt($this->curl_instance,CURLOPT_COOKIE, $this->use_cookie); 243 | }*/ 244 | 245 | if( !is_null( $data ) && is_array( $data ) && !empty( $data ) ) { 246 | $url .= '?' . http_build_query( $data ); 247 | } 248 | 249 | curl_setopt( $this->curl_instance, CURLOPT_URL, $url ); 250 | 251 | if( ( !is_null( $argv ) && in_array( 'peachyecho', $argv ) ) || $this->echo ) { 252 | if( $displayGetOutData ) { 253 | pecho( "GET: $url\n", PECHO_NORMAL ); 254 | } 255 | } 256 | 257 | Hooks::runHook( 'HTTPGet', array( &$this, &$url, &$data ) ); 258 | 259 | return $this->doCurlExecWithRetrys(); 260 | 261 | } 262 | 263 | /** 264 | * Returns the HTTP code of the last request 265 | * 266 | * @access public 267 | * @return int HTTP code 268 | */ 269 | public function get_HTTP_code() 270 | { 271 | $ci = curl_getinfo( $this->curl_instance ); 272 | return $ci['http_code']; 273 | } 274 | 275 | /** 276 | * Sends data via HTTP POST 277 | * 278 | * @access public 279 | * 280 | * @param string $url URL to send 281 | * @param array $data Array of data to pass. 282 | * @param array $headers Array of headers to pass to curl 283 | * 284 | * @param bool|null $verifyssl override for global verifyssl value 285 | * 286 | * @return bool|string Result 287 | */ 288 | public function post($url, $data, $headers = array(), $verifyssl = null) 289 | { 290 | global $argv, $displayPostOutData; 291 | 292 | if( is_string( $headers ) ) curl_setopt( $this->curl_instance, CURLOPT_HTTPHEADER, array( $headers ) ); 293 | else $this->setCurlHeaders( $headers ); 294 | $this->setVerifySSL( $verifyssl ); 295 | 296 | curl_setopt( $this->curl_instance, CURLOPT_FOLLOWLOCATION, 0 ); 297 | curl_setopt( $this->curl_instance, CURLOPT_HTTPGET, 0 ); 298 | curl_setopt( $this->curl_instance, CURLOPT_POST, 1 ); 299 | curl_setopt( $this->curl_instance, CURLOPT_POSTFIELDS, $data ); 300 | 301 | /*if( !is_null( $this->use_cookie ) ) { 302 | curl_setopt($this->curl_instance,CURLOPT_COOKIE, $this->use_cookie); 303 | }*/ 304 | 305 | curl_setopt( $this->curl_instance, CURLOPT_URL, $url ); 306 | 307 | if( ( !is_null( $argv ) && in_array( 'peachyecho', $argv ) ) || $this->echo ) { 308 | if( $displayPostOutData ) { 309 | pecho( "POST: $url\n", PECHO_NORMAL ); 310 | } 311 | } 312 | 313 | Hooks::runHook( 'HTTPPost', array( &$this, &$url, &$data ) ); 314 | 315 | return $this->doCurlExecWithRetrys(); 316 | } 317 | 318 | /** 319 | * Downloads an URL to the local disk 320 | * 321 | * @access public 322 | * 323 | * @param string $url URL to get 324 | * @param string $local Local filename to download to 325 | * @param array $headers Array of headers to pass to curl 326 | * @param bool|null $verifyssl 327 | * 328 | * @return bool 329 | */ 330 | function download( $url, $local, $headers = array(), $verifyssl = null ) { 331 | global $argv; 332 | 333 | $out = fopen( $local, 'wb' ); 334 | 335 | if( is_string( $headers ) ) curl_setopt( $this->curl_instance, CURLOPT_HTTPHEADER, array( $headers ) ); 336 | else $this->setCurlHeaders( $headers ); 337 | $this->setVerifySSL( $verifyssl ); 338 | 339 | // curl_setopt($this->curl_instance, CURLOPT_FILE, $out); 340 | curl_setopt( $this->curl_instance, CURLOPT_HTTPGET, 1 ); 341 | curl_setopt( $this->curl_instance, CURLOPT_POST, 0 ); 342 | curl_setopt( $this->curl_instance, CURLOPT_URL, $url ); 343 | curl_setopt( $this->curl_instance, CURLOPT_HEADER, 0 ); 344 | 345 | if( ( !is_null( $argv ) && in_array( 'peachyecho', $argv ) ) || $this->echo ) { 346 | pecho( "DLOAD: $url\n", PECHO_NORMAL ); 347 | } 348 | 349 | Hooks::runHook( 'HTTPDownload', array( &$this, &$url, &$local ) ); 350 | 351 | fwrite( $out, $this->doCurlExecWithRetrys() ); 352 | fclose( $out ); 353 | 354 | return true; 355 | 356 | } 357 | 358 | /** 359 | * Gets the Header for the last request made 360 | * @return null|string 361 | */ 362 | public function getLastHeader() { 363 | return $this->lastHeader; 364 | } 365 | 366 | /** 367 | * Destructor, deletes cookies and closes cURL class 368 | * 369 | * @access public 370 | * @return void 371 | */ 372 | public function __destruct() 373 | { 374 | Hooks::runHook( 'HTTPClose', array( &$this ) ); 375 | 376 | curl_close( $this->curl_instance ); 377 | 378 | //@unlink($this->cookie_jar); 379 | } 380 | 381 | /** 382 | * The below allows us to only have one instance of this class 383 | */ 384 | private static $defaultInstance = null; 385 | private static $defaultInstanceWithEcho = null; 386 | 387 | /** 388 | * @param bool|false $echo 389 | * @return HTTP|null 390 | */ 391 | public static function getDefaultInstance($echo = false) 392 | { 393 | if( $echo ) { 394 | if( is_null( self::$defaultInstanceWithEcho ) ) { 395 | self::$defaultInstanceWithEcho = new Http( $echo ); 396 | } 397 | return self::$defaultInstanceWithEcho; 398 | } else { 399 | if( is_null( self::$defaultInstance ) ) { 400 | self::$defaultInstance = new Http( $echo ); 401 | } 402 | return self::$defaultInstance; 403 | } 404 | } 405 | 406 | } -------------------------------------------------------------------------------- /Includes/AutoUpdate.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | /** 21 | * Checks for a new version of Peachy and installs it if there is one. 22 | */ 23 | Class AutoUpdate { 24 | 25 | /** 26 | * @var Http 27 | */ 28 | protected $http; 29 | protected $repository; 30 | protected $logfile; 31 | protected $lastused; 32 | protected $commits; 33 | 34 | function __construct( $http ) { 35 | global $pgIP, $pgExperimentalupdates; 36 | $this->http = $http; 37 | $this->repository = ( $pgExperimentalupdates ? 'master' : 'stable' ); 38 | $this->logfile = ( $pgExperimentalupdates ? 'Update.log' : 'StableUpdate.log' ); 39 | $this->lastused = ( file_exists( $pgIP . 'Includes/updateversion' ) ? unserialize( file_get_contents( $pgIP . 'Includes/updateversion' ) ) : 'Unknown' ); 40 | } 41 | 42 | /** 43 | * Scans the GitHub repository for any updates and returns false if there are. 44 | * 45 | * @access public 46 | * @return bool 47 | */ 48 | public function Checkforupdate() { 49 | global $pgIP, $pgExperimentalupdates; 50 | pecho( "Checking for updates...\n\n", PECHO_NORMAL ); 51 | if( $pgExperimentalupdates ) pecho( "Warning: You have experimental updates switched on.\nExperimental updates are not fully tested and can cause problems,\nsuch as, bot misbehaviors up to complete crashes.\nUse at your own risk.\nPeachy will not revert back to a stable release until switched off.\n\n", PECHO_NOTICE ); 52 | $data = json_decode( $this->http->get( 'https://api.github.com/repos/MW-Peachy/Peachy/branches/' . $this->repository, null, array(), false ), true ); 53 | $this->commits = $data; 54 | /*if( strstr( $this->http->getLastHeader(), 'Status: 304 Not Modified') ) { 55 | pecho( "Peachy is up to date.\n\n", PECHO_NORMAL ); 56 | return true; 57 | }*/ 58 | if( is_array( $data ) && array_key_exists( 'message', $data ) && strpos( $data['message'], 'API rate limit exceeded' ) === 0 ) { 59 | pecho( "Cant check for updates right now, next window in " . $this->getTimeToNextLimitWindow() . "\n\n", PECHO_NOTICE ); 60 | return true; 61 | } 62 | $this->cacheLastGithubETag(); 63 | if( $this->lastused !== $this->repository ) { 64 | pecho( "Changing Peachy version to run using " . ( $pgExperimentalupdates ? "experimental" : "stable" ) . " updates.\n\n", PECHO_NOTICE ); 65 | return false; 66 | } 67 | if( file_exists( $pgIP . 'Includes' . DIRECTORY_SEPARATOR . $this->logfile ) ) { 68 | $log = unserialize( file_get_contents( $pgIP . 'Includes' . DIRECTORY_SEPARATOR . $this->logfile ) ); 69 | if( isset( $data['commit']['sha'] ) && $log['commit']['sha'] != $data['commit']['sha'] ) { 70 | pecho( "Update available!\n\n", PECHO_NOTICE ); 71 | return false; 72 | } else { 73 | pecho( "Peachy is up to date.\n\n", PECHO_NORMAL ); 74 | return true; 75 | } 76 | } else { 77 | pecho( "No update log found.\n\n", PECHO_WARN ); 78 | return false; 79 | } 80 | } 81 | 82 | /** 83 | * Caches the last Etag from github in a tmp file 84 | */ 85 | private function cacheLastGithubETag() { 86 | global $pgIP; 87 | if( preg_match( '/ETag\: \"([^\"]*)\"/', $this->http->getLastHeader(), $matches ) ) { 88 | file_put_contents( $pgIP . 'tmp' . DIRECTORY_SEPARATOR . 'github-ETag.tmp', $matches[1] ); 89 | } 90 | } 91 | 92 | /** 93 | * @return string representing time to next github api request window 94 | */ 95 | private function getTimeToNextLimitWindow() { 96 | if( preg_match( '/X-RateLimit-Reset: (\d*)/', $this->http->getLastHeader(), $matches ) ) { 97 | return gmdate( "i \m\i\\n\u\\t\\e\s s \s\\e\c\o\\n\d\s", $matches[1] - time() ); 98 | } 99 | return 'Unknown'; 100 | } 101 | 102 | private function getLocalPath( $fullUpdatePath ) { 103 | global $pgIP; 104 | $xplodesAt = DIRECTORY_SEPARATOR . 'gitUpdate' . DIRECTORY_SEPARATOR . 'Peachy-' . $this->repository . DIRECTORY_SEPARATOR; 105 | $parts = explode( $xplodesAt, $fullUpdatePath, 2 ); 106 | return $pgIP . $parts[1]; 107 | } 108 | 109 | /** 110 | * Updates the Peachy framework 111 | * 112 | * @access public 113 | * @return boolean|null 114 | */ 115 | public function updatePeachy() { 116 | global $pgIP, $pgExperimentalupdates; 117 | $gitZip = $pgIP . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR . 'gitUpdate.zip'; 118 | if( file_exists( $gitZip ) ) { 119 | unlink( $gitZip ); 120 | } 121 | file_put_contents( $gitZip, file_get_contents( 'http://github.com/MW-Peachy/Peachy/archive/' . $this->repository . '.zip' ) ); 122 | $zip = new ZipArchive(); 123 | $res = $zip->open( $gitZip ); 124 | if( $res === true ) { 125 | $gitFolder = $pgIP . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR . 'gitUpdate'; 126 | if( file_exists( $gitFolder ) ) { 127 | $this->rrmdir( $gitFolder ); 128 | } 129 | mkdir( $gitFolder, 02775 ); 130 | $zip->extractTo( $gitFolder ); 131 | $zip->close(); 132 | 133 | $this->copyOverGitFiles( $gitFolder . DIRECTORY_SEPARATOR . 'Peachy-' . $this->repository ); 134 | 135 | file_put_contents( $pgIP . 'Includes' . DIRECTORY_SEPARATOR . 'updateversion', serialize( ( $pgExperimentalupdates ? 'master' : 'stable' ) ) ); 136 | 137 | pecho( "Peachy Updated! Changes will go into effect on the next run.\n\n", PECHO_NOTICE ); 138 | 139 | file_put_contents( $pgIP . 'Includes' . DIRECTORY_SEPARATOR . $this->logfile, serialize( $this->commits ) ); 140 | } else { 141 | pecho( "Update failed! Peachy could not retrieve all contents from GitHub.\n\n", PECHO_WARN ); 142 | } 143 | } 144 | 145 | /** 146 | * @param string $gitFolder 147 | */ 148 | private function copyOverGitFiles( $gitFolder ) { 149 | /** @var $fileInfo DirectoryIterator */ 150 | foreach( new DirectoryIterator( $gitFolder ) as $fileInfo ){ 151 | if( $fileInfo->isDot() ) continue; 152 | $gitPath = $fileInfo->getRealPath(); 153 | $lclPatch = $this->getLocalPath( $gitPath ); 154 | 155 | if( $fileInfo->isDir() ) { 156 | if( !file_exists( $lclPatch ) ) { 157 | mkdir( $lclPatch ); 158 | } 159 | $this->copyOverGitFiles( $gitPath ); 160 | } elseif( $fileInfo->isFile() ) { 161 | file_put_contents( $lclPatch, file_get_contents( $gitPath ) ); 162 | } 163 | } 164 | } 165 | 166 | /** 167 | * recursively remove a directory 168 | * @param string $dir 169 | */ 170 | private function rrmdir( $dir ) { 171 | if( is_dir( $dir ) ) { 172 | $objects = scandir( $dir ); 173 | foreach( $objects as $object ){ 174 | if( $object != "." && $object != ".." ) { 175 | if( filetype( $dir . "/" . $object ) == "dir" ) $this->rrmdir( $dir . "/" . $object ); else unlink( $dir . "/" . $object ); 176 | } 177 | } 178 | reset( $objects ); 179 | rmdir( $dir ); 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Includes/Autoloader.php: -------------------------------------------------------------------------------- 1 | 'Includes/Wiki.php', 14 | 'User' => 'Includes/User.php', 15 | 'Page' => 'Includes/Page.php', 16 | 'Image' => 'Includes/Image.php', 17 | 'XMLParse' => 'Includes/XMLParse.php', 18 | 'Script' => 'Script.php', 19 | 'UtfNormal' => 'Plugins/normalize/UtfNormal.php', 20 | 'DatabaseMySQL' => 'Plugins/database/MySQL.php', 21 | 'DatabaseMySQLi' => 'Plugins/database/MySQLi.php', 22 | 'DatabasePgSQL' => 'Plugins/database/PgSQL.php', 23 | 'DatabaseBase' => 'Plugins/database.php', 24 | 'ResultWrapper' => 'Plugins/database.php', 25 | 'lime_test' => 'Includes/lime_test.php', 26 | 'lime_output' => 'Includes/limeOutput.php', 27 | 'lime_output_color' => 'Includes/limeOutputColor.php', 28 | 'lime_colorizer' => 'Includes/limeColorizer.php', 29 | 'lime_harness' => 'Includes/limeHarness.php', 30 | 'lime_coverage' => 'Includes/limeCoverage.php', 31 | 'lime_registration' => 'Includes/limeRegistration.php', 32 | 'sfYaml' => 'Plugins/yaml/sfYaml.php', 33 | 'sfYamlDumper' => 'Plugins/yaml/sfYamlDumper.php', 34 | 'sfYamlInline' => 'Plugins/yaml/sfYamlInline.php', 35 | 'sfYamlParser' => 'Plugins/yaml/sfYamlParser.php', 36 | 'Text_Diff' => 'Plugins/diff/textdiff/Diff.php', 37 | 'Text_MappedDiff' => 'Plugins/diff/textdiff/Diff.php', 38 | 'Text_Diff_Op' => 'Plugins/diff/textdiff/Diff.php', 39 | 'Text_Diff_Op_copy' => 'Plugins/diff/textdiff/Diff.php', 40 | 'Text_Diff_Op_delete' => 'Plugins/diff/textdiff/Diff.php', 41 | 'Text_Diff_Op_add' => 'Plugins/diff/textdiff/Diff.php', 42 | 'Text_Diff_Op_change' => 'Plugins/diff/textdiff/Diff.php', 43 | 'Text_Diff3' => 'Plugins/diff/textdiff/Diff3.php', 44 | 'Text_Diff3_Op' => 'Plugins/diff/textdiff/Diff3.php', 45 | 'Text_Diff3_Op_copy' => 'Plugins/diff/textdiff/Diff3.php', 46 | 'Text_Diff3_BlockBuilder' => 'Plugins/diff/textdiff/Diff3.php', 47 | 'Text_Diff_ThreeWay' => 'Plugins/diff/textdiff/Diff/ThreeWay.php', 48 | 'Text_Diff_ThreeWay_Op' => 'Plugins/diff/textdiff/Diff/ThreeWay.php', 49 | 'Text_Diff_ThreeWay_Op_copy' => 'Plugins/diff/textdiff/Diff/ThreeWay.php', 50 | 'Text_Diff_ThreeWay_BlockBuilder' => 'Plugins/diff/textdiff/Diff/ThreeWay.php', 51 | 'Text_Diff_Renderer' => 'Plugins/diff/textdiff/Diff/Renderer.php', 52 | 'Text_Diff_Mapped' => 'Plugins/diff/textdiff/Diff/Mapped.php', 53 | 'Text_Diff_Renderer_unified' => 'Plugins/diff/textdiff/Diff/Renderer/unified.php', 54 | 'Text_Diff_Renderer_inline' => 'Plugins/diff/textdiff/Diff/Renderer/inline.php', 55 | 'Text_Diff_Renderer_context' => 'Plugins/diff/textdiff/Diff/Renderer/context.php', 56 | 'Text_Diff_Renderer_colorized' => 'Plugins/diff/textdiff/Diff/Renderer/colorized.php', 57 | 'Text_Diff_Renderer_dualview' => 'Plugins/diff/textdiff/Diff/Renderer/dualview.php', 58 | 'Text_Diff_Engine_xdiff' => 'Plugins/diff/textdiff/Diff/Engine/xdiff.php', 59 | 'Text_Diff_Engine_string' => 'Plugins/diff/textdiff/Diff/Engine/string.php', 60 | 'Text_Diff_Engine_shell' => 'Plugins/diff/textdiff/Diff/Engine/shell.php', 61 | 'Text_Diff_Engine_native' => 'Plugins/diff/textdiff/Diff/Engine/native.php', 62 | 'PeachyAWBFunctions' => 'Includes/PeachyAWBFunctions.php', 63 | ); 64 | 65 | /** 66 | * Takes a class name and attempt to load it. Class name must be found in $pgAutoLoader. 67 | * 68 | * @param string $class_name Name of class we're looking for. 69 | * @return boolean|null Returns true if the function successfully loads the class. Otherwise returns false. 70 | * Returning false is important on failure as it allows Zend to try and look in other registered autoloaders as well. 71 | */ 72 | public static function autoload($class_name) 73 | { 74 | global $pgIP; 75 | 76 | if (isset(self::$pgAutoLoader[$class_name]) && is_file($pgIP . self::$pgAutoLoader[$class_name])) { 77 | require_once($pgIP . self::$pgAutoLoader[$class_name]); 78 | 79 | return true; 80 | } 81 | 82 | if (is_file($pgIP . 'Plugins/' . $class_name . '.php')) { 83 | Hooks::runHook('LoadPlugin', array(&$class_name)); 84 | require_once($pgIP . 'Plugins/' . $class_name . '.php'); 85 | 86 | return true; 87 | } 88 | } 89 | } 90 | 91 | spl_autoload_register(array('AutoLoader', 'autoload')); 92 | -------------------------------------------------------------------------------- /Includes/Exceptions.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | /** 21 | * @file 22 | * Stores all the subclasses of Exception 23 | */ 24 | 25 | /** 26 | * Generic Peachy Error 27 | * 28 | * @package Exceptions 29 | */ 30 | class PeachyError extends Exception { 31 | 32 | public function __construct( $code, $text ) { 33 | parent::__construct( 34 | "API Error: " . $code . " (" . $text . ")" 35 | ); 36 | } 37 | } 38 | 39 | /** 40 | * Assertation error bot 41 | * 42 | * @package Exceptions 43 | */ 44 | class AssertFailure extends Exception { 45 | 46 | public function __contstruct( $type ) { 47 | parent::__construct( 48 | "Assert Failure: ".( $type == "bot" ? "Bot is no longer flagged as a bot" : "User is logged out" ) 49 | ); 50 | } 51 | } 52 | 53 | /** 54 | * Generic API Error 55 | * 56 | * @package Exceptions 57 | */ 58 | class MWAPIError extends Exception { 59 | 60 | public function __construct( $error ) { 61 | parent::__construct( 62 | "API Error: " . $error['code'] . " (" . $error['text'] . $error['info'] . ")" 63 | ); 64 | } 65 | } 66 | 67 | /** 68 | * Error with user permissions 69 | * 70 | * @package Exceptions 71 | */ 72 | class PermissionsError extends Exception { 73 | public function __construct( $error ) { 74 | parent::__construct( 75 | "Permissions Error: " . $error 76 | ); 77 | } 78 | } 79 | 80 | /** 81 | * Generic cURL Error 82 | * 83 | * @package Exceptions 84 | */ 85 | class CURLError extends Exception { 86 | private $errno; 87 | private $error; 88 | 89 | /** 90 | * @param integer $errno 91 | * @param string $error 92 | */ 93 | public function __construct( $errno, $error ) { 94 | $this->errno = $errno; 95 | $this->error = $error; 96 | 97 | parent::__construct( 98 | "cURL Error (" . $this->errno . "): " . $this->error 99 | ); 100 | } 101 | 102 | public function get_errno() { 103 | return $this->errno; 104 | } 105 | 106 | public function get_error() { 107 | return $this->error; 108 | } 109 | 110 | } 111 | 112 | /** 113 | * Invalid Title Error 114 | * 115 | * @package Exceptions 116 | */ 117 | class BadTitle extends Exception { 118 | 119 | private $title; 120 | 121 | public function __construct( $title ) { 122 | $this->title = $title; 123 | parent::__construct( 124 | "Invalid title: $title" 125 | ); 126 | 127 | } 128 | 129 | public function getTitle() { 130 | return $this->title; 131 | } 132 | } 133 | 134 | /** 135 | * No Title Error 136 | * 137 | * @package Exceptions 138 | */ 139 | class NoTitle extends Exception { 140 | 141 | public function __construct() { 142 | parent::__construct( 143 | "No title or pageid stated when instantiating Page class" 144 | ); 145 | 146 | } 147 | } 148 | 149 | /** 150 | * No User Error 151 | * 152 | * @package Exceptions 153 | */ 154 | class NoUser extends Exception { 155 | 156 | public function __construct( $title ) { 157 | parent::__construct( 158 | "Non-existant user: $title" 159 | ); 160 | 161 | } 162 | } 163 | 164 | /** 165 | * Blocked User Error 166 | * 167 | * @package Exceptions 168 | */ 169 | class UserBlocked extends Exception { 170 | 171 | public function __construct( $pgUsername = "User" ) { 172 | parent::__construct( 173 | $pgUsername . " is currently blocked." 174 | ); 175 | 176 | } 177 | 178 | } 179 | 180 | /** 181 | * Logged Out Error 182 | * 183 | * @package Exceptions 184 | */ 185 | class LoggedOut extends Exception { 186 | 187 | public function __construct() { 188 | parent::__construct( 189 | "User is not logged in." 190 | ); 191 | 192 | } 193 | 194 | } 195 | 196 | /** 197 | * Missing DependencyError Error 198 | * 199 | * @package Exceptions 200 | */ 201 | class DependencyError extends Exception { 202 | 203 | public function __construct( $software, $url = false ) { 204 | $message = "Missing dependency: \`" . $software . "\`. "; 205 | if( $url ) $message .= "Download from <$url>"; 206 | parent::__construct( 207 | $message 208 | ); 209 | 210 | } 211 | 212 | } 213 | 214 | /** 215 | * Misspelling of "dependency", used for backwards compatibility 216 | * 217 | * @package Exceptions 218 | * @deprecated since 31 Jan 2014 219 | */ 220 | class DependancyError extends DependencyError { 221 | public function __construct( $software, $url = false ) { 222 | parent::__construct( $software, $url ); 223 | } 224 | } 225 | 226 | /** 227 | * Login Error 228 | * 229 | * @package Exceptions 230 | */ 231 | class LoginError extends Exception { 232 | 233 | /** 234 | * @param string[] $error 235 | */ 236 | public function __construct( $error ) { 237 | parent::__construct( 238 | "Login Error: " . $error[0] . " (" . $error[1] . ")" 239 | ); 240 | } 241 | } 242 | 243 | /** 244 | * Peachy Hook Error 245 | * 246 | * @package Exceptions 247 | * @package Peachy_Hooks 248 | */ 249 | class HookError extends Exception { 250 | public function __construct( $error ) { 251 | parent::__construct( 252 | "Hook Error: " . $error 253 | ); 254 | } 255 | } 256 | 257 | /** 258 | * Generic Database Error 259 | * 260 | * @package Exceptions 261 | * @package Peachy_Database 262 | */ 263 | class DBError extends Exception { 264 | 265 | /** 266 | * @param string $sql 267 | */ 268 | public function __construct( $error, $errno, $sql = null ) { 269 | parent::__construct( 270 | "Database Error: " . $error . " (code $errno) " . $sql 271 | ); 272 | } 273 | } 274 | 275 | /** 276 | * Generic Edit Error 277 | * 278 | * @package Exceptions 279 | */ 280 | class EditError extends Exception { 281 | 282 | /** 283 | * @param string $error 284 | * @param string $text 285 | */ 286 | public function __construct( $error, $text ) { 287 | parent::__construct( 288 | "Edit Error: " . $error . " ($text)" 289 | ); 290 | } 291 | } 292 | 293 | /** 294 | * Generic Move Error 295 | * 296 | * @package Exceptions 297 | */ 298 | class MoveError extends Exception { 299 | public function __construct( $error, $text ) { 300 | parent::__construct( 301 | "Move Error: " . $error . " ($text)" 302 | ); 303 | } 304 | } 305 | 306 | /** 307 | * Generic Delete Error 308 | * 309 | * @package Exceptions 310 | */ 311 | class DeleteError extends Exception { 312 | public function __construct( $error, $text ) { 313 | parent::__construct( 314 | "Delete Error: " . $error . " ($text)" 315 | ); 316 | } 317 | } 318 | 319 | /** 320 | * Generic Undelete Error 321 | * 322 | * @package Exceptions 323 | */ 324 | class UndeleteError extends Exception { 325 | public function __construct( $error, $text ) { 326 | parent::__construct( 327 | "Undelete Error: " . $error . " ($text)" 328 | ); 329 | } 330 | } 331 | 332 | /** 333 | * Generic Protect Error 334 | * 335 | * @package Exceptions 336 | */ 337 | class ProtectError extends Exception { 338 | public function __construct( $error, $text ) { 339 | parent::__construct( 340 | "Protect Error: " . $error . " ($text)" 341 | ); 342 | } 343 | } 344 | 345 | /** 346 | * Generic Email Error 347 | * 348 | * @package Exceptions 349 | */ 350 | class EmailError extends Exception { 351 | public function __construct( $error, $text ) { 352 | parent::__construct( 353 | "Email Error: " . $error . " ($text)" 354 | ); 355 | } 356 | } 357 | 358 | /** 359 | * Generic Image Error 360 | * 361 | * @package Exceptions 362 | */ 363 | class ImageError extends Exception { 364 | public function __construct( $error ) { 365 | parent::__construct( 366 | "Image Error: " . $error 367 | ); 368 | } 369 | } 370 | 371 | /** 372 | * Error for wrong parameters in a function 373 | * 374 | * @package Exceptions 375 | */ 376 | class BadEntryError extends Exception { 377 | 378 | /** 379 | * @param string $error 380 | */ 381 | public function __construct( $error, $text ) { 382 | parent::__construct( 383 | "Bad Entry Error: " . $error . " ($text)" 384 | ); 385 | } 386 | } 387 | 388 | /** 389 | * Generic XML Error 390 | * 391 | * @package Exceptions 392 | * @package XML 393 | */ 394 | class XMLError extends Exception { 395 | public function __construct( $error ) { 396 | parent::__construct( 397 | "XML Error: " . $error 398 | ); 399 | } 400 | } 401 | 402 | -------------------------------------------------------------------------------- /Includes/Hooks.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | /** 21 | * @file 22 | * Hooks object 23 | * Stores the runHook function, which runs all hook functions 24 | */ 25 | 26 | /** 27 | * Hooks class 28 | * Stores and runs {@link http://wiki.peachy.compwhizii.net/wiki/Manual/Hooks hooks} 29 | * 30 | */ 31 | class Hooks { 32 | 33 | /** 34 | * Search for hook functions and run them if defined 35 | * 36 | * @param string $hook_name Name of hook to search for 37 | * @param array $args Arguments to pass to the hook function 38 | * @throws HookError 39 | * @throws BadEntryError 40 | * @return mixed Output of hook function 41 | */ 42 | public static function runHook( $hook_name, $args = array() ) { 43 | global $pgHooks; 44 | 45 | if( !isset( $pgHooks[$hook_name] ) ) return null; 46 | 47 | if( !is_array( $pgHooks[$hook_name] ) ) { 48 | throw new HookError( "Hook assignment for event `$hook_name` is not an array. Syntax is " . '$pgHooks[\'hookname\'][] = "hook_function";' ); 49 | } 50 | 51 | $method = null; 52 | foreach( $pgHooks[$hook_name] as $function ){ 53 | if( is_array( $function ) ) { 54 | if( count( $function ) < 2 ) { 55 | throw new HookError( "Not enough parameters in array specified for `$hook_name` hook" ); 56 | } elseif( is_object( $function[0] ) ) { 57 | $object = $function[0]; 58 | $method = $function[1]; 59 | if( count( $function ) > 2 ) { 60 | $data = $function[2]; 61 | } 62 | } elseif( is_string( $function[0] ) ) { 63 | $method = $function[0]; 64 | if( count( $function ) > 1 ) { 65 | $data = $function[1]; 66 | } 67 | } 68 | 69 | } elseif( is_string( $function ) ) { 70 | $method = $function; 71 | } 72 | 73 | if( isset( $data ) ) { 74 | $args = array_merge( array( $data ), $args ); 75 | } 76 | 77 | if( isset( $object ) ) { 78 | $fncarr = array( $object, $method ); 79 | } elseif( is_string( $method ) && in_string( "::", $method ) ) { 80 | $fncarr = explode( "::", $method ); 81 | } else { 82 | $fncarr = $method; 83 | } 84 | 85 | //is_callable( $fncarr ); //Apparently this is a bug. Thanks, MW! 86 | 87 | if( !is_callable( $fncarr ) ) { 88 | throw new BadEntryError( "MissingFunction", "Hook function $fncarr was not defined" ); 89 | } 90 | 91 | $hookRet = call_user_func_array( $fncarr, $args ); 92 | 93 | if( !is_null( $hookRet ) ) return $hookRet; 94 | 95 | } 96 | 97 | } 98 | 99 | } -------------------------------------------------------------------------------- /Includes/Peachy.php: -------------------------------------------------------------------------------- 1 | Checkforupdate(); 27 | if( !$Uptodate ) $updater->updatePeachy();*/ 28 | 29 | if( !is_null( $config_name ) ) { 30 | $config_params = self::parse_config( $config_name ); 31 | 32 | } else { 33 | $config_params = array( 34 | 'username' => $pgUsername, 35 | 'password' => $password, 36 | 'baseurl' => $base_url, 37 | 'method' => 'legacy' 38 | ); 39 | 40 | } 41 | 42 | if( is_null( $config_params['baseurl'] ) || !isset( $config_params['baseurl'] ) ) { 43 | throw new LoginError( array( "MissingParam", "The baseurl parameter was not set." ) ); 44 | } 45 | 46 | if( !isset( $config_params['username'] ) ) { 47 | $config_params['nologin'] = true; 48 | } 49 | if( !isset( $config_params['password'] ) && isset( $config_params['method'] ) && $config_params['method'] == "legacy" ) { 50 | $config_params['nologin'] = true; 51 | } 52 | 53 | list( $version, $extensions ) = self::wikiChecks( $config_params['baseurl'] ); 54 | 55 | Hooks::runHook( 'StartLogin', array( &$config_params, &$extensions ) ); 56 | 57 | $w = new $classname( $config_params, $extensions, false, null ); 58 | $w->mwversion = $version; 59 | 60 | return $w; 61 | } 62 | 63 | /** 64 | * Performs various checks and settings 65 | * Checks if MW version is at least {@link MINMW} 66 | * 67 | * @static 68 | * @access public 69 | * @param string $base_url URL to api.php 70 | * @throws DependencyError|string 71 | * @return array Installed extensions 72 | */ 73 | public static function wikiChecks( $base_url ) { 74 | $http = HTTP::getDefaultInstance(); 75 | 76 | $siteInfo = unserialize( 77 | $http->get( 78 | $base_url, 79 | array( 80 | 'action' => 'query', 81 | 'meta' => 'siteinfo', 82 | 'format' => 'php', 83 | 'siprop' => 'extensions|general', 84 | ) 85 | ) 86 | ); 87 | 88 | if (isset($siteInfo['error']) && $siteInfo['error']['code'] == 'readapidenied') { 89 | global $pgHooks; 90 | $pgHooks['PostLogin'][] = array( 'Peachy::wikiChecks', $base_url ); 91 | return array( MINMW, array() ); 92 | } 93 | 94 | $version = preg_replace('/[^0-9\.]/', '', $siteInfo['query']['general']['generator']); 95 | 96 | if( version_compare( $version, MINMW ) < 0 ) { 97 | throw new DependencyError( "MediaWiki " . MINMW, "http://mediawiki.org" ); 98 | } 99 | 100 | $extensions = array(); 101 | 102 | foreach ($siteInfo['query']['extensions'] as $ext) { 103 | if( isset( $ext['version'] ) ) { 104 | $extensions[$ext['name']] = $ext['version']; 105 | } else { 106 | $extensions[$ext['name']] = ''; 107 | } 108 | } 109 | 110 | return array( $version, $extensions ); 111 | } 112 | 113 | /** 114 | * Checks for config files, parses them. 115 | * 116 | * @access private 117 | * @static 118 | * @param string $config_name Name of config file 119 | * @throws BadEntryError 120 | * @return array Config params 121 | */ 122 | private static function parse_config( $config_name ) { 123 | global $pgIP; 124 | if( !is_file( $config_name ) ) { 125 | if( !is_file( $pgIP . 'Configs/' . $config_name . '.cfg' ) ) { 126 | throw new BadEntryError( "BadConfig", "A non-existent configuration file was specified." ); 127 | } else { 128 | $config_name = $pgIP . 'Configs/' . $config_name . '.cfg'; 129 | } 130 | } 131 | 132 | $config_params = parse_ini_file( $config_name ); 133 | 134 | if( isset( $config_params['useconfig'] ) ) { 135 | $config_params = $config_params + self::parse_config( $config_params['useconfig'] ); 136 | } 137 | 138 | return $config_params; 139 | } 140 | 141 | /** 142 | * Function that displays an error message when an End-User attempts to use a function no longer included in Peachy 143 | * 144 | * @param null|string $method 145 | * @param null|string $newFunction 146 | * @param string $message 147 | */ 148 | public static function deprecatedWarn($method, $newFunction, $message = null) 149 | { 150 | if( is_null( $message ) ) { 151 | $message = "Warning: $method is deprecated. Please use $newFunction instead."; 152 | } 153 | 154 | $message = "[$message|YELLOW_BAR]\n\n"; 155 | 156 | pecho( $message, PECHO_WARN, 'cecho' ); 157 | } 158 | 159 | /** 160 | * Checks for and returns an SVN repository version. 161 | * 162 | * @return array|bool 163 | */ 164 | public static function getSvnInfo() { 165 | global $pgIP; 166 | 167 | // http://svnbook.red-bean.com/nightly/en/svn.developer.insidewc.html 168 | $entries = $pgIP . '/.svn/entries'; 169 | 170 | if( !file_exists( $entries ) ) { 171 | return false; 172 | } 173 | 174 | $lines = file( $entries ); 175 | if( !count( $lines ) ) { 176 | return false; 177 | } 178 | 179 | // check if file is xml (subversion release <= 1.3) or not (subversion release = 1.4) 180 | if( preg_match( '/^<\?xml/', $lines[0] ) ) { 181 | return false; 182 | } 183 | 184 | // Subversion is release 1.4 or above. 185 | if( count( $lines ) < 11 ) { 186 | return false; 187 | } 188 | 189 | $info = array( 190 | 'checkout-rev' => intval( trim( $lines[3] ) ), 191 | 'url' => trim( $lines[4] ), 192 | 'repo-url' => trim( $lines[5] ), 193 | 'directory-rev' => intval( trim( $lines[10] ) ) 194 | ); 195 | 196 | return $info; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /Includes/PeachyAWBFunctions.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | /** 21 | * PeachyAWBFunctions class. 22 | * 23 | * It consists of various static functions used for the PeachyAWB script 24 | * Much of the code is derived from Pywikipedia and AWB, both under the GPL 25 | * 26 | */ 27 | class PeachyAWBFunctions { 28 | 29 | public static $html_tags = array( 30 | # Tags that must be closed 31 | 'b', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1', 32 | 'h2', 'h3', 'h4', 'h5', 'h6', 'cite', 'code', 'em', 's', 33 | 'strike', 'strong', 'tt', 'var', 'div', 'center', 34 | 'blockquote', 'ol', 'ul', 'dl', 'table', 'caption', 'pre', 35 | 'ruby', 'rt', 'rb', 'rp', 'p', 'span', 'u', 'abbr', 36 | # Single 37 | 'br', 'hr', 'li', 'dt', 'dd', 38 | # Elements that cannot have close tags 39 | 'br', 'hr', 40 | # Tags that can be nested--?? 41 | 'table', 'tr', 'td', 'th', 'div', 'blockquote', 'ol', 'ul', 42 | 'dl', 'font', 'big', 'small', 'sub', 'sup', 'span', 43 | # Can only appear inside table, we will close them 44 | 'td', 'th', 'tr', 45 | # Tags used by list 46 | 'ul', 'ol', 47 | # Tags that can appear in a list 48 | 'li', 49 | ## pairs 50 | # "b", "i", "u", "font", "big", "small", "sub", "sup", "h1", 51 | # "h2", "h3", "h4", "h5", "h6", "cite", "code", "em", "s", "span", 52 | # "strike", "strong", "tt", "var", "div", "center", 53 | # "blockquote", "ol", "ul", "dl", "table", "caption", "pre", 54 | # "ruby", "rt" , "rb" , "rp", 55 | ## single 56 | # "br", "p", "hr", "li", "dt", "dd", 57 | ## nest 58 | # "table", "tr", "td", "th", "div", "blockquote", "ol", "ul", 59 | # "dl", "font", "big", "small", "sub", "sup", 60 | ## table tags 61 | # "td", "th", "tr", 62 | 63 | ); 64 | 65 | public static $html_attrs = array( 66 | "title", "align", "lang", "dir", "width", "height", 67 | "bgcolor", "clear", "noshade", 68 | "cite", "size", "face", "color", 69 | "type", "start", "value", "compact", 70 | #/* For various lists, mostly deprecated but safe */ 71 | "summary", "width", "border", "frame", "rules", 72 | "cellspacing", "cellpadding", "valign", "char", 73 | "charoff", "colgroup", "col", "span", "abbr", "axis", 74 | "headers", "scope", "rowspan", "colspan", 75 | "id", "class", "name", "style" 76 | ); 77 | 78 | public static $html_colors = array( 79 | '#F0FFFF' => 'azure', '#F5F5DC' => 'beige', '#FFE4C4' => 'bisque', '#000000' => 'black', '#0000FF' => 'blue', 80 | '#A52A2A' => 'brown', '#FF7F50' => 'coral', '#FFF8DC' => 'cornsilk', '#DC143C' => 'crimson', 81 | '#00FFFF' => 'cyan', '#00008B' => 'darkBlue', '#008B8B' => 'darkCyan', '#A9A9A9' => 'darkGray', 82 | '#8B0000' => 'darkRed', '#FF1493' => 'deepPink', '#696969' => 'dimGray', '#FF00FF' => 'fuchsia', 83 | '#FFD700' => 'gold', '#808080' => 'gray', '#008000' => 'green', '#F0FFF0' => 'honeyDew', '#FF69B4' => 'hotPink', 84 | '#4B0082' => 'indigo', '#FFFFF0' => 'ivory', '#F0E68C' => 'khaki', '#E6E6FA' => 'lavender', '#00FF00' => 'lime', 85 | '#FAF0E6' => 'linen', '#800000' => 'maroon', '#FFE4B5' => 'moccasin', '#000080' => 'navy', 86 | '#FDF5E6' => 'oldLace', '#808000' => 'olive', '#FFA500' => 'orange', '#DA70D6' => 'orchid', '#CD853F' => 'peru', 87 | '#FFC0CB' => 'pink', '#DDA0DD' => 'plum', '#800080' => 'purple', '#FF0000' => 'red', '#FA8072' => 'salmon', 88 | '#2E8B57' => 'seaGreen', '#FFF5EE' => 'seaShell', '#A0522D' => 'sienna', '#C0C0C0' => 'silver', 89 | '#87CEEB' => 'skyBlue', '#FFFAFA' => 'snow', '#D2B48C' => 'tan', '#008080' => 'teal', '#D8BFD8' => 'thistle', 90 | '#FF6347' => 'tomato', '#EE82EE' => 'violet', '#F5DEB3' => 'wheat', '#FFFFFF' => 'white', '#FFFF00' => 'yellow', 91 | ); 92 | 93 | public static $stub_search = '[Ss]tub'; 94 | 95 | public static $interwiki_map = array(); 96 | 97 | public static $typo_list = array(); 98 | 99 | public static function fixVars( Wiki $wiki ) { 100 | $interwiki = $wiki->siteinfo( array( 'interwikimap' ) ); 101 | self::$interwiki_map = $interwiki['query']['interwikimap']; 102 | } 103 | 104 | public static function fixCitations( $text ) { 105 | 106 | //merge all variant of cite web 107 | $text = preg_replace( '/\{\{\s*(cite[_ \-]*(url|web|website)|Web[_ \-]*(citation|cite|reference|reference[_ ]4))(?=\s*\|)/i', '{{cite web', $text ); 108 | 109 | //Remove formatting on certian parameters 110 | $text = preg_replace( "/(\|\s*(?:agency|author|first|format|language|last|location|month|publisher|work|year)\s*=\s*)(''|'''|''''')((?:\[\[[^][|]+|\[\[|)[][\w\s,.~!`\"]+)(''+)(?=\s*\|[\w\s]+=|\s*\}\})/", '$1$3', $text ); 111 | 112 | //Unlink PDF in format parameters 113 | $text = preg_replace( '/(\|\s*format\s*=\s*)\[\[(adobe|portable|document|file|format|pdf|\.|\s|\(|\)|\|)+\]\]/i', '$1PDF', $text ); 114 | $text = preg_replace( '/(\|\s*format\s*=\s*)(\s*\.?(adobe|portable|document|file|format|pdf|\(|\)))+?(\s*[|}])/i', '$1PDF$4', $text ); 115 | 116 | //No |format=HTML says {{cite web/doc}} 117 | $text = preg_replace( '/(\{\{cite[^{}]+)\|\s*format\s*=\s*(\[\[[^][|]+\||\[\[|)(\]\]| |html?|world|wide|web)+\s*(?=\||\}\})/i', '$1', $text ); 118 | 119 | //Fix accessdate tags [[WP:AWB/FR#Fix accessdate tags]] 120 | $text = preg_replace( 121 | array( 122 | '/(\|\s*)a[ces]{3,8}date(\s*=\s*)(?=[^{|}]*20\d\d|\}\})/', 123 | '/accessdate(\s*=\s*)\[*(200\d)[/_\-](\d{2})[/_\-](\d{2})\]*/', 124 | '/(\|\s*)a[cs]*es*mou*nthday(\s*=\s*)/', 125 | '/(\|\s*)a[cs]*es*daymou*nth(\s*=\s*)/', 126 | '/(\|\s*)accessdate(\s*=\s*[0-3]?[0-9] +(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w*)([^][<>}{]*accessyear[\s=]+20\d\d)/', 127 | '/(\|\s*)accessdate(\s*=\s*(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w* +[0-3]?[0-9])([^][<>}{]*accessyear[\s=]+20\d\d)/', 128 | '/(\|\s*)accessdaymonth(\s*=\s*)\s*([^{|}<>]+?)\s*(\|[^][<>}{]*accessyear[\s=]+)(20\d\d)/', 129 | '/(\|\s*)accessmonthday(\s*=\s*)\s*([^{|}<>]+?)\s*(\|[^][<>}{]*accessyear[\s=]+)(20\d\d)/', 130 | ), 131 | array( 132 | '$1accessdate$2', 133 | 'accessdate$1$2-$3-$4', 134 | '$1accessmonthday$2', 135 | '$1accessdaymonth$2', 136 | '$1accessdaymonth$2$3', 137 | '$1accessmonthday$2$3', 138 | '$1accessdate$2$3 $5', 139 | '$1accessdate$2$3, $5', 140 | ), 141 | $text 142 | ); 143 | 144 | //Fix improper dates 145 | $text = preg_replace( 146 | array( 147 | '/(\{\{cit[ea][^{}]+\|\s*date\s*=\s*\d{2}[/\-.]\d{2}[/\-.])([5-9]\d)(?=\s*[|}])/i', 148 | '/(\{\{cit[ea][^{}]+\|\s*date\s*=\s*)(0[1-9]|1[012])[/\-.](1[3-9]|2\d|3[01])[/\-.](19\d\d|20\d\d)(?=\s*[|}])/i', 149 | '/(\{\{cit[ea][^{}]+\|\s*date\s*=\s*)(1[3-9]|2\d|3[01])[/\-.](0[1-9]|1[012])[/\-.](19\d\d|20\d\d)(?=\s*[|}])/i', 150 | ), 151 | array( 152 | '${1}19$2', 153 | '$1$4-$2-$3', 154 | '$1$4-$3-$2', 155 | ), 156 | 157 | $text 158 | ); 159 | 160 | //Fix URLS lacking http:// 161 | $text = preg_replace( '/(\|\s*url\s*=\s*)([0-9a-z.\-]+\.[a-z]{2,4}/[^][{|}:\s"]\s*[|}])/', '$1http://$2', $text ); 162 | 163 | //Fix {{citation|title=[url title]}} 164 | $text = preg_replace( '/(\{\{cit[ea][^{}]*?)(\s*\|\s*)(?:url|title)(\s*=\s*)\[([^][<>\s"]*) +([^]\n]+)\](?=[|}])/i', '$1$2url$3$4$2title$3$5', $text ); 165 | 166 | return $text; 167 | 168 | } 169 | 170 | public static function fixDateTags( $text ) { 171 | 172 | $text = preg_replace( '/\{\{\s*(?:template:)?\s*(?:wikify(?:-date)?|wfy|wiki)(\s*\|\s*section)?\s*\}\}/iS', "{{Wikify$1|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}", $text ); 173 | $text = preg_replace( '/\{\{(template:)?(Clean( ?up)?|CU|Tidy)\}\}/iS', "{{Cleanup|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}", $text ); 174 | $text = preg_replace( '/\{\{(template:)?(Linkless|Orphan)\}\}/iS', "{{Orphan|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}", $text ); 175 | $text = preg_replace( '/\{\{(template:)?(Unreferenced(sect)?|add references|cite[ -]sources?|cleanup-sources?|needs? references|no sources|no references?|not referenced|references|unref|unsourced)\}\}/iS', "{{Unreferenced|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}", $text ); 176 | $text = preg_replace( '/\{\{(template:)?(Uncategori[sz]ed|Uncat|Classify|Category needed|Catneeded|categori[zs]e|nocats?)\}\}/iS', "{{Uncategorized|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}", $text ); 177 | $text = preg_replace( '/\{\{(template:)?(Trivia2?|Too ?much ?trivia|Trivia section|Cleanup-trivia)\}\}/iS', "{{Trivia|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}", $text ); 178 | $text = preg_replace( '/\{\{(template:)?(deadend|DEP)\}\}/iS', "{{Deadend|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}", $text ); 179 | $text = preg_replace( '/\{\{(template:)?(copyedit|g(rammar )?check|copy-edit|cleanup-copyedit|cleanup-english)\}\}/iS', "{{Copyedit|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}", $text ); 180 | $text = preg_replace( '/\{\{(template:)?(sources|refimprove|not verified)\}\}/iS', "{{Refimprove|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}", $text ); 181 | $text = preg_replace( '/\{\{(template:)?(Expand)\}\}/iS', "{{Expand|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}", $text ); 182 | //$text = preg_replace( '/\{\{(?:\s*[Tt]emplate:)?(\s*(?:[Cc]n|[Ff]act|[Pp]roveit|[Cc]iteneeded|[Uu]ncited|[Cc]itation needed)\s*(?:\|[^{}]+(?\]*>))+\w+[^\w\s<>]*)(?=\n[*#:;]|\n?]*>)\n?
/mi', 214 | '/<[/]?br([^{/}<>]*?/?)>/mi', 215 | '//i', 216 | '//', 217 | ), 218 | array( 219 | '$1', 220 | '', 221 | '{{-}}', 222 | '{{clear$1}}' 223 | ), 224 | $text 225 | ); 226 | 227 | $text = preg_replace( '/(]*)> *\n?]*>)((?:[^<]|<(?!/?font))*? *\n?)/mi', '$1$2$3', $text ); 228 | 229 | $text = preg_replace( '/]*)>\[\[([^[\]{|}]+)\|([^[\]\n]*?)\]\]/mi', '[[$2|$3]]', $text ); 230 | 231 | $text = preg_replace( '/(?!\[\[)((?:[^<]|<(?!/?font))*?)(?/mi', '$3', $text ); 232 | 233 | return $text; 234 | 235 | } 236 | 237 | public static function fixHyperlinking( $text ) { 238 | 239 | $text = preg_replace( '/(http:\/\/[^][<>\s"|])(&client=firefox-a|<=)(?=[][<>\s"|&])/', '$1', $text ); 240 | 241 | $text = str_replace( '[{{SERVER}}{{localurl:', '[{{fullurl:', $text ); 242 | 243 | $text = preg_replace( '/[(](?:see|) *(http:\/\/[^][<>"\s(|)]+[\w=\/&])\s?[)]/i', '<$1>', $text ); 244 | 245 | $text = preg_replace( '/\[\[(https?:\/\/[^\]\n]+?)\]\]/', '[$1]', $text ); 246 | $text = preg_replace( '/\[\[(https?:\/\/.+?)\]/', '[$1]', $text ); 247 | 248 | $text = preg_replace( '/\[\[(:?)Image:([^][{|}]+\.(pdf|midi?|ogg|ogv|xcf))(?=\||\]\])/i', '[[$1File:$2', $text ); 249 | 250 | $text = preg_replace( 251 | array( 252 | '/(http:\/* *){2,}(?=[a-z0-9:.\-]+\/)/i', 253 | "/(\[\w+:\/\/[^][<>\"\s]+?)''/i", 254 | '/\[\n*(\w+:\/\/[^][<>"\s]+ *(?:(?<= )[^\n\]<>]*?|))\n([^[\]<>{}\n=@\/]*?) *\n*\]/i', 255 | '/\[(\w+:\/\/[^][<>"\s]+) +([Cc]lick here|[Hh]ere|\W|→|[ -\/;-@]) *\]/i', 256 | ), 257 | array( 258 | 'http://', 259 | "$1 ''", 260 | '[$1 $2]', 261 | '$2 [$1]', 262 | ), 263 | $text 264 | ); 265 | 266 | $text = preg_replace( '/(\[\[(?:File|Image):[^][<>{|}]+)#(|filehistory|filelinks|file)(?=[\]|])/i', '$1', $text ); 267 | 268 | $text = preg_replace( '/\[http://(www\.toolserver\.org|toolserver\.org|tools\.wikimedia\.org|tools\.wikimedia\.de)/([^][<>"\s;?]*)\?? ([^]\n]+)\]/', '[[tools:$2|$3]]', $text ); 269 | 270 | return $text; 271 | 272 | } 273 | 274 | /** 275 | * @Fixme Method getWiki() not found. 276 | * 277 | * @see getWiki() 278 | * 279 | * @param string $text 280 | * @param string $title 281 | * @return mixed 282 | */ 283 | public static function fixTypos( $text, $title ) { 284 | 285 | if( !count( self::$typo_list ) ) { 286 | global $script; 287 | 288 | $str = $script->getWiki()->initPage( 'Wikipedia:AutoWikiBrowser/Typos' )->get_text(); 289 | 290 | foreach( explode( "\n", $str ) as $line ){ 291 | if( substr( $line, 0, 5 ) == "/', $line, $m ); 294 | 295 | if( !empty( $m[2] ) && !empty( $m[3] ) ) { 296 | self::$typo_list[] = array( 'word' => $m[1], 'find' => $m[2], 'replace' => $m[3] ); 297 | } 298 | // 299 | } 300 | } 301 | 302 | } 303 | 304 | $run_times = array(); 305 | 306 | shuffle( self::$typo_list ); //So that if it quits randomly, it will give equal prejudice to each typo. 307 | 308 | if( !count( self::$typo_list ) || preg_match( '/133t|-ology|\\(sic\\)|\\[sic\\]|\\[\'\'sic\'\'\\]|\\{\\{sic\\}\\}|spellfixno/', $text ) ) return $text; 309 | 310 | foreach( self::$typo_list as $typo ){ 311 | //Skip typos in links 312 | $time = microtime( 1 ); 313 | 314 | if( @preg_match( '/' . $typo['find'] . '/S', $title ) ) continue; //Skip if matches title 315 | 316 | if( @preg_match( "/(\{\{|\[\[)[^\[\]\r\n\|\{\}]*?" . $typo['find'] . "[^\[\]\r\n\|\{\}]*?(\]\]|\}\})/S", $text ) ) continue; 317 | 318 | $text2 = @preg_replace( '/' . $typo['find'] . '/S', $typo['replace'], $text ); 319 | if( !is_null( $text2 ) ) $text = $text2; 320 | $run_times[$typo['word']] = number_format( microtime( 1 ) - $time, 2 ); 321 | } 322 | return $text; 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /Includes/XMLParse.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | class XMLParse { 21 | 22 | /** 23 | * Converts an XML url or string to a PHP array format 24 | * 25 | * @static 26 | * @access public 27 | * @param string $data Either an url to an xml file, or a raw XML string. Peachy will autodetect which is which. 28 | * @return array Parsed XML 29 | * @throws BadEntryError 30 | * @throws DependencyError 31 | * @throws HookError 32 | * @throws XMLError 33 | */ 34 | public static function load( $data ) { 35 | $http = HTTP::getDefaultInstance(); 36 | 37 | if( !function_exists( 'simplexml_load_string' ) ) { 38 | throw new DependencyError( "SimpleXML", "http://us.php.net/manual/en/book.simplexml.php" ); 39 | } 40 | 41 | libxml_use_internal_errors( true ); 42 | 43 | if( in_string( "get( $data ); 47 | } 48 | 49 | Hooks::runHook( 'PreSimpleXMLLoad', array( &$xmlout ) ); 50 | 51 | $xml = simplexml_load_string( $xmlout ); 52 | 53 | Hooks::runHook( 'PostSimpleXMLLoad', array( &$xml ) ); 54 | 55 | if( !$xml ) { 56 | foreach( libxml_get_errors() as $error ){ 57 | throw new XMLError( $error ); 58 | } 59 | } 60 | 61 | $outArr = array(); 62 | 63 | $namespaces = $xml->getNamespaces( true ); 64 | $namespaces['default'] = ''; 65 | 66 | self::recurse( $xml, $outArr, $namespaces ); 67 | 68 | libxml_clear_errors(); 69 | 70 | return $outArr; 71 | } 72 | 73 | /** 74 | * @param SimpleXMLElement $xml 75 | * @param $arr 76 | * @param $namespaces 77 | */ 78 | private static function recurse( $xml, &$arr, $namespaces ) { 79 | 80 | foreach( $namespaces as $namespace ){ 81 | 82 | foreach( $xml->children( $namespace ) as $elementName => $node ){ 83 | 84 | $key = count( $arr ); 85 | 86 | if( isset( $arr['_name'] ) ) $key--; 87 | if( isset( $arr['_attributes'] ) ) $key--; 88 | 89 | $arr[$key] = array(); 90 | $arr[$key]['_name'] = $elementName; 91 | 92 | $arr[$key]['_attributes'] = array(); 93 | 94 | foreach( $node->attributes( $namespace ) as $aname => $avalue ){ 95 | $arr[$key]['_attributes'][$aname] = trim( $avalue ); 96 | } 97 | 98 | if( !count( $arr[$key]['_attributes'] ) ) { 99 | unset( $arr[$key]['_attributes'] ); 100 | } 101 | 102 | $text = trim( (string)$node ); 103 | 104 | if( strlen( $text ) > 0 ) $arr[$key]['_text'] = $text; 105 | 106 | self::recurse( $node, $arr[$key], $namespaces ); 107 | 108 | if( count( $arr[$key] ) == 1 && isset( $arr[$key]['_text'] ) ) { 109 | $arr[$key] = $arr[$key]['_text']; 110 | } 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Includes/lime.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | * For the full copyright and license information, please view the LICENSE 12 | * file that was distributed with this source code. 13 | */ 14 | 15 | lime_colorizer::style('ERROR', array('bg' => 'red', 'fg' => 'white', 'bold' => true)); 16 | lime_colorizer::style('INFO', array('fg' => 'green', 'bold' => true)); 17 | lime_colorizer::style('TRACE', array('fg' => 'green', 'bold' => true)); 18 | lime_colorizer::style('PARAMETER', array('fg' => 'cyan')); 19 | lime_colorizer::style('COMMENT', array('fg' => 'yellow')); 20 | 21 | lime_colorizer::style('GREEN_BAR', array('fg' => 'white', 'bg' => 'green', 'bold' => true)); 22 | lime_colorizer::style('RED_BAR', array('fg' => 'white', 'bg' => 'red', 'bold' => true)); 23 | lime_colorizer::style('YELLOW_BAR', array('fg' => 'black', 'bg' => 'yellow', 'bold' => true)); 24 | lime_colorizer::style('INFO_BAR', array('fg' => 'cyan', 'bold' => true)); 25 | 26 | lime_colorizer::style('DIFF_DELETED', array('fg' => 'red', 'bold' => true)); 27 | lime_colorizer::style('DIFF_ADDED', array('fg' => 'green', 'bold' => true)); 28 | -------------------------------------------------------------------------------- /Includes/limeColorizer.php: -------------------------------------------------------------------------------- 1 | colors_supported = true; 20 | } else { 21 | // colors are supported on windows with ansicon or on tty consoles 22 | if (DIRECTORY_SEPARATOR == '\\') { 23 | $this->colors_supported = false !== getenv('ANSICON'); 24 | } else { 25 | $this->colors_supported = function_exists('posix_isatty') && @posix_isatty(STDOUT); 26 | } 27 | } 28 | } 29 | 30 | /** 31 | * @param $name 32 | * @param array $options 33 | */ 34 | public static function style($name, $options = array()) 35 | { 36 | self::$styles[$name] = $options; 37 | } 38 | 39 | /** 40 | * @param string $text 41 | * @param array $parameters 42 | * @return string 43 | */ 44 | public function colorize($text = '', $parameters = array()) 45 | { 46 | 47 | if (!$this->colors_supported) { 48 | return $text; 49 | } 50 | 51 | static $options = array('bold' => 1, 'underscore' => 4, 'blink' => 5, 'reverse' => 7, 'conceal' => 8); 52 | static $foreground = array( 53 | 'black' => 30, 54 | 'red' => 31, 55 | 'green' => 32, 56 | 'yellow' => 33, 57 | 'blue' => 34, 58 | 'magenta' => 35, 59 | 'cyan' => 36, 60 | 'white' => 37 61 | ); 62 | static $background = array( 63 | 'black' => 40, 64 | 'red' => 41, 65 | 'green' => 42, 66 | 'yellow' => 43, 67 | 'blue' => 44, 68 | 'magenta' => 45, 69 | 'cyan' => 46, 70 | 'white' => 47 71 | ); 72 | 73 | !is_array($parameters) && isset(self::$styles[$parameters]) and $parameters = self::$styles[$parameters]; 74 | 75 | $codes = array(); 76 | isset($parameters['fg']) and $codes[] = $foreground[$parameters['fg']]; 77 | isset($parameters['bg']) and $codes[] = $background[$parameters['bg']]; 78 | foreach ($options as $option => $value) { 79 | isset($parameters[$option]) && $parameters[$option] and $codes[] = $value; 80 | } 81 | 82 | return "\033[" . implode(';', $codes) . 'm' . $text . "\033[0m"; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Includes/limeCoverage.php: -------------------------------------------------------------------------------- 1 | harness = $harness; 25 | 26 | if (!function_exists('xdebug_start_code_coverage')) { 27 | throw new Exception('You must install and enable xdebug before using lime coverage.'); 28 | } 29 | 30 | if (!ini_get('xdebug.extended_info')) { 31 | throw new Exception('You must set xdebug.extended_info to 1 in your php.ini to use lime coverage.'); 32 | } 33 | } 34 | 35 | /** 36 | * @throws Exception 37 | */ 38 | public function run() 39 | { 40 | if (!count($this->harness->files)) { 41 | throw new Exception('You must register some test files before running coverage!'); 42 | } 43 | 44 | if (!count($this->files)) { 45 | throw new Exception('You must register some files to cover!'); 46 | } 47 | 48 | $this->coverage = array(); 49 | 50 | $this->process($this->harness->files); 51 | 52 | $this->output($this->files); 53 | } 54 | 55 | /** 56 | * @param $files 57 | * @throws Exception 58 | */ 59 | public function process($files) 60 | { 61 | if (!is_array($files)) { 62 | $files = array($files); 63 | } 64 | 65 | $tmp_file = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'test.php'; 66 | foreach ($files as $file) { 67 | $tmp = <<'.serialize(xdebug_get_code_coverage()).''; 72 | EOF; 73 | file_put_contents($tmp_file, $tmp); 74 | ob_start(); 75 | // see http://trac.symfony-project.org/ticket/5437 for the explanation on the weird "cd" thing 76 | passthru(sprintf('cd & %s %s 2>&1', escapeshellarg($this->harness->php_cli), escapeshellarg($tmp_file)), 77 | $return); 78 | $retval = ob_get_clean(); 79 | 80 | if (0 != $return) // test exited without success 81 | { 82 | // something may have gone wrong, we should warn the user so they know 83 | // it's a bug in their code and not symfony's 84 | 85 | $this->harness->output->echoln(sprintf('Warning: %s returned status %d, results may be inaccurate', 86 | $file, $return), 'ERROR'); 87 | } 88 | 89 | if (false === $cov = @unserialize(substr($retval, strpos($retval, '') + 9, 90 | strpos($retval, '') - 9)) 91 | ) { 92 | if (0 == $return) { 93 | // failed to serialize, but PHP said it should of worked. 94 | // something is seriously wrong, so abort with exception 95 | throw new Exception(sprintf('Unable to unserialize coverage for file "%s"', $file)); 96 | } else { 97 | // failed to serialize, but PHP warned us that this might have happened. 98 | // so we should ignore and move on 99 | continue; // continue foreach loop through $this->harness->files 100 | } 101 | } 102 | 103 | foreach ($cov as $file => $lines) { 104 | if (!isset($this->coverage[$file])) { 105 | $this->coverage[$file] = $lines; 106 | continue; 107 | } 108 | 109 | foreach ($lines as $line => $flag) { 110 | if ($flag == 1) { 111 | $this->coverage[$file][$line] = 1; 112 | } 113 | } 114 | } 115 | } 116 | 117 | if (file_exists($tmp_file)) { 118 | unlink($tmp_file); 119 | } 120 | } 121 | 122 | /** 123 | * @param $files 124 | */ 125 | public function output($files) 126 | { 127 | ksort($this->coverage); 128 | $total_php_lines = 0; 129 | $total_covered_lines = 0; 130 | foreach ($files as $file) { 131 | $file = realpath($file); 132 | $is_covered = isset($this->coverage[$file]); 133 | $cov = isset($this->coverage[$file]) ? $this->coverage[$file] : array(); 134 | $covered_lines = array(); 135 | $missing_lines = array(); 136 | $output = null; 137 | 138 | foreach ($cov as $line => $flag) { 139 | switch ($flag) { 140 | case 1: 141 | $covered_lines[] = $line; 142 | break; 143 | case -1: 144 | $missing_lines[] = $line; 145 | break; 146 | } 147 | } 148 | 149 | $total_lines = count($covered_lines) + count($missing_lines); 150 | if (!$total_lines) { 151 | // probably means that the file is not covered at all! 152 | $total_lines = count($this->get_php_lines(file_get_contents($file))); 153 | } 154 | 155 | $output = $this->harness->output; 156 | $percent = $total_lines ? count($covered_lines) * 100 / $total_lines : 0; 157 | 158 | $total_php_lines += $total_lines; 159 | $total_covered_lines += count($covered_lines); 160 | 161 | $relative_file = $this->get_relative_file($file); 162 | $output->echoln(sprintf("%-70s %3.0f%%", substr($relative_file, -min(70, strlen($relative_file))), 163 | $percent), $percent == 100 ? 'INFO' : ($percent > 90 ? 'PARAMETER' : ($percent < 20 ? 'ERROR' : ''))); 164 | if ($this->verbose && $is_covered && $percent != 100) { 165 | $output->comment(sprintf("missing: %s", $this->format_range($missing_lines))); 166 | } 167 | } 168 | 169 | $output->echoln(sprintf("TOTAL COVERAGE: %3.0f%%", 170 | $total_php_lines ? $total_covered_lines * 100 / $total_php_lines : 0)); 171 | } 172 | 173 | /** 174 | * @param $content 175 | * @return array 176 | */ 177 | public static function get_php_lines($content) 178 | { 179 | if (is_readable($content)) { 180 | $content = file_get_contents($content); 181 | } 182 | 183 | $tokens = token_get_all($content); 184 | $php_lines = array(); 185 | $current_line = 1; 186 | $in_class = false; 187 | $in_function = false; 188 | $in_function_declaration = false; 189 | $end_of_current_expr = true; 190 | $open_braces = 0; 191 | foreach ($tokens as $token) { 192 | if (is_string($token)) { 193 | switch ($token) { 194 | case '=': 195 | if (false === $in_class || (false !== $in_function && !$in_function_declaration)) { 196 | $php_lines[$current_line] = true; 197 | } 198 | break; 199 | case '{': 200 | ++$open_braces; 201 | $in_function_declaration = false; 202 | break; 203 | case ';': 204 | $in_function_declaration = false; 205 | $end_of_current_expr = true; 206 | break; 207 | case '}': 208 | $end_of_current_expr = true; 209 | --$open_braces; 210 | if ($open_braces == $in_class) { 211 | $in_class = false; 212 | } 213 | if ($open_braces == $in_function) { 214 | $in_function = false; 215 | } 216 | break; 217 | } 218 | 219 | continue; 220 | } 221 | 222 | list($id, $text) = $token; 223 | 224 | switch ($id) { 225 | case T_CURLY_OPEN: 226 | case T_DOLLAR_OPEN_CURLY_BRACES: 227 | ++$open_braces; 228 | break; 229 | case T_WHITESPACE: 230 | case T_OPEN_TAG: 231 | case T_CLOSE_TAG: 232 | $end_of_current_expr = true; 233 | $current_line += count(explode("\n", $text)) - 1; 234 | break; 235 | case T_COMMENT: 236 | case T_DOC_COMMENT: 237 | $current_line += count(explode("\n", $text)) - 1; 238 | break; 239 | case T_CLASS: 240 | $in_class = $open_braces; 241 | break; 242 | case T_FUNCTION: 243 | $in_function = $open_braces; 244 | $in_function_declaration = true; 245 | break; 246 | case T_AND_EQUAL: 247 | case T_BREAK: 248 | case T_CASE: 249 | case T_CATCH: 250 | case T_CLONE: 251 | case T_CONCAT_EQUAL: 252 | case T_CONTINUE: 253 | case T_DEC: 254 | case T_DECLARE: 255 | case T_DEFAULT: 256 | case T_DIV_EQUAL: 257 | case T_DO: 258 | case T_ECHO: 259 | case T_ELSEIF: 260 | case T_EMPTY: 261 | case T_ENDDECLARE: 262 | case T_ENDFOR: 263 | case T_ENDFOREACH: 264 | case T_ENDIF: 265 | case T_ENDSWITCH: 266 | case T_ENDWHILE: 267 | case T_EVAL: 268 | case T_EXIT: 269 | case T_FOR: 270 | case T_FOREACH: 271 | case T_GLOBAL: 272 | case T_IF: 273 | case T_INC: 274 | case T_INCLUDE: 275 | case T_INCLUDE_ONCE: 276 | case T_INSTANCEOF: 277 | case T_ISSET: 278 | case T_IS_EQUAL: 279 | case T_IS_GREATER_OR_EQUAL: 280 | case T_IS_IDENTICAL: 281 | case T_IS_NOT_EQUAL: 282 | case T_IS_NOT_IDENTICAL: 283 | case T_IS_SMALLER_OR_EQUAL: 284 | case T_LIST: 285 | case T_LOGICAL_AND: 286 | case T_LOGICAL_OR: 287 | case T_LOGICAL_XOR: 288 | case T_MINUS_EQUAL: 289 | case T_MOD_EQUAL: 290 | case T_MUL_EQUAL: 291 | case T_NEW: 292 | case T_OBJECT_OPERATOR: 293 | case T_OR_EQUAL: 294 | case T_PLUS_EQUAL: 295 | case T_PRINT: 296 | case T_REQUIRE: 297 | case T_REQUIRE_ONCE: 298 | case T_RETURN: 299 | case T_SL: 300 | case T_SL_EQUAL: 301 | case T_SR: 302 | case T_SR_EQUAL: 303 | case T_SWITCH: 304 | case T_THROW: 305 | case T_TRY: 306 | case T_UNSET: 307 | case T_UNSET_CAST: 308 | case T_USE: 309 | case T_WHILE: 310 | case T_XOR_EQUAL: 311 | $php_lines[$current_line] = true; 312 | $end_of_current_expr = false; 313 | break; 314 | default: 315 | if (false === $end_of_current_expr) { 316 | $php_lines[$current_line] = true; 317 | } 318 | } 319 | } 320 | 321 | return $php_lines; 322 | } 323 | 324 | /** 325 | * @param $content 326 | * @param $cov 327 | * @return array 328 | */ 329 | public function compute($content, $cov) 330 | { 331 | $php_lines = self::get_php_lines($content); 332 | 333 | // we remove from $cov non php lines 334 | foreach (array_diff_key($cov, $php_lines) as $line => $tmp) { 335 | unset($cov[$line]); 336 | } 337 | 338 | return array($cov, $php_lines); 339 | } 340 | 341 | /** 342 | * @param $lines 343 | * @return string 344 | */ 345 | public function format_range($lines) 346 | { 347 | sort($lines); 348 | $formatted = ''; 349 | $first = -1; 350 | $last = -1; 351 | foreach ($lines as $line) { 352 | if ($last + 1 != $line) { 353 | if ($first != -1) { 354 | $formatted .= $first == $last ? "$first " : "[$first - $last] "; 355 | } 356 | $first = $line; 357 | $last = $line; 358 | } else { 359 | $last = $line; 360 | } 361 | } 362 | if ($first != -1) { 363 | $formatted .= $first == $last ? "$first " : "[$first - $last] "; 364 | } 365 | 366 | return $formatted; 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /Includes/limeHarness.php: -------------------------------------------------------------------------------- 1 | $options); 25 | } 26 | 27 | $this->options = array_merge(array( 28 | 'php_cli' => null, 29 | 'force_colors' => false, 30 | 'output' => null, 31 | 'verbose' => false, 32 | ), $options); 33 | 34 | $this->php_cli = $this->find_php_cli($this->options['php_cli']); 35 | $this->output = $this->options['output'] ? $this->options['output'] : new lime_output($this->options['force_colors']); 36 | } 37 | 38 | /** 39 | * @param null $php_cli 40 | * @return null|string 41 | * @throws Exception 42 | */ 43 | protected function find_php_cli($php_cli = null) 44 | { 45 | if (is_null($php_cli)) { 46 | if (getenv('PHP_PATH')) { 47 | $php_cli = getenv('PHP_PATH'); 48 | 49 | if (!is_executable($php_cli)) { 50 | throw new Exception('The defined PHP_PATH environment variable is not a valid PHP executable.'); 51 | } 52 | } else { 53 | $php_cli = PHP_BINDIR . DIRECTORY_SEPARATOR . 'php'; 54 | } 55 | } 56 | 57 | if (is_executable($php_cli)) { 58 | return $php_cli; 59 | } 60 | 61 | $path = getenv('PATH') ? getenv('PATH') : getenv('Path'); 62 | $exe_suffixes = DIRECTORY_SEPARATOR == '\\' ? (getenv('PATHEXT') ? explode(PATH_SEPARATOR, 63 | getenv('PATHEXT')) : array('.exe', '.bat', '.cmd', '.com')) : array(''); 64 | foreach (array('php5', 'php') as $php_cli) { 65 | foreach ($exe_suffixes as $suffix) { 66 | foreach (explode(PATH_SEPARATOR, $path) as $dir) { 67 | $file = $dir . DIRECTORY_SEPARATOR . $php_cli . $suffix; 68 | if (is_executable($file)) { 69 | return $file; 70 | } 71 | } 72 | } 73 | } 74 | 75 | throw new Exception("Unable to find PHP executable."); 76 | } 77 | 78 | /** 79 | * @return array 80 | */ 81 | public function to_array() 82 | { 83 | $results = array(); 84 | foreach ($this->stats['files'] as $stat) { 85 | $results = array_merge($results, $stat['output']); 86 | } 87 | 88 | return $results; 89 | } 90 | 91 | /** 92 | * @return string 93 | */ 94 | public function to_xml() 95 | { 96 | return lime_test::to_xml($this->to_array()); 97 | } 98 | 99 | /** 100 | * @return bool 101 | * @throws Exception 102 | */ 103 | public function run() 104 | { 105 | if (!count($this->files)) { 106 | throw new Exception('You must register some test files before running them!'); 107 | } 108 | 109 | // sort the files to be able to predict the order 110 | sort($this->files); 111 | 112 | $this->stats = array( 113 | 'files' => array(), 114 | 'failed_files' => array(), 115 | 'failed_tests' => 0, 116 | 'total' => 0, 117 | ); 118 | 119 | foreach ($this->files as $file) { 120 | $this->stats['files'][$file] = array(); 121 | $stats = &$this->stats['files'][$file]; 122 | 123 | $relative_file = $this->get_relative_file($file); 124 | 125 | $test_file = tempnam(sys_get_temp_dir(), 'lime'); 126 | $result_file = tempnam(sys_get_temp_dir(), 'lime'); 127 | file_put_contents($test_file, <<&1', escapeshellarg($this->php_cli), escapeshellarg($test_file)), $return); 141 | ob_end_clean(); 142 | unlink($test_file); 143 | 144 | $output = file_get_contents($result_file); 145 | $stats['output'] = $output ? unserialize($output) : ''; 146 | if (!$stats['output']) { 147 | $stats['output'] = array( 148 | array( 149 | 'file' => $file, 150 | 'tests' => array(), 151 | 'stats' => array( 152 | 'plan' => 1, 153 | 'total' => 1, 154 | 'failed' => array(0), 155 | 'passed' => array(), 156 | 'skipped' => array(), 157 | 'errors' => array() 158 | ) 159 | ) 160 | ); 161 | } 162 | unlink($result_file); 163 | 164 | $file_stats = &$stats['output'][0]['stats']; 165 | 166 | $delta = 0; 167 | if ($return > 0) { 168 | $stats['status'] = $file_stats['errors'] ? 'errors' : 'dubious'; 169 | $stats['status_code'] = $return; 170 | } else { 171 | $this->stats['total'] += $file_stats['total']; 172 | 173 | if (!$file_stats['plan']) { 174 | $file_stats['plan'] = $file_stats['total']; 175 | } 176 | 177 | $delta = $file_stats['plan'] - $file_stats['total']; 178 | if (0 != $delta) { 179 | $stats['status'] = $file_stats['errors'] ? 'errors' : 'dubious'; 180 | $stats['status_code'] = 255; 181 | } else { 182 | $stats['status'] = $file_stats['failed'] ? 'not ok' : ($file_stats['errors'] ? 'errors' : 'ok'); 183 | $stats['status_code'] = 0; 184 | } 185 | } 186 | 187 | $this->output->echoln(sprintf('%s%s%s', substr($relative_file, -min(67, strlen($relative_file))), 188 | str_repeat('.', 70 - min(67, strlen($relative_file))), $stats['status'])); 189 | 190 | if ('dubious' == $stats['status']) { 191 | $this->output->echoln(sprintf(' Test returned status %s', $stats['status_code'])); 192 | } 193 | 194 | if ('ok' != $stats['status']) { 195 | $this->stats['failed_files'][] = $file; 196 | } 197 | 198 | if ($delta > 0) { 199 | $this->output->echoln(sprintf(' Looks like you planned %d tests but only ran %d.', 200 | $file_stats['plan'], $file_stats['total'])); 201 | 202 | $this->stats['failed_tests'] += $delta; 203 | $this->stats['total'] += $delta; 204 | } else { 205 | if ($delta < 0) { 206 | $this->output->echoln(sprintf(' Looks like you planned %s test but ran %s extra.', 207 | $file_stats['plan'], $file_stats['total'] - $file_stats['plan'])); 208 | } 209 | } 210 | 211 | if (false !== $file_stats && $file_stats['failed']) { 212 | $this->stats['failed_tests'] += count($file_stats['failed']); 213 | 214 | $this->output->echoln(sprintf(" Failed tests: %s", implode(', ', $file_stats['failed']))); 215 | } 216 | 217 | if (false !== $file_stats && $file_stats['errors']) { 218 | $this->output->echoln(' Errors:'); 219 | 220 | $error_count = count($file_stats['errors']); 221 | for ($i = 0; $i < 3 && $i < $error_count; ++$i) { 222 | $this->output->echoln(' - ' . $file_stats['errors'][$i]['message'], null, false); 223 | } 224 | if ($error_count > 3) { 225 | $this->output->echoln(sprintf(' ... and %s more', $error_count - 3)); 226 | } 227 | } 228 | } 229 | 230 | if (count($this->stats['failed_files'])) { 231 | $format = "%-30s %4s %5s %5s %5s %s"; 232 | $this->output->echoln(sprintf($format, 'Failed Test', 'Stat', 'Total', 'Fail', 'Errors', 'List of Failed')); 233 | $this->output->echoln("--------------------------------------------------------------------------"); 234 | foreach ($this->stats['files'] as $file => $stat) { 235 | if (!in_array($file, $this->stats['failed_files'])) { 236 | continue; 237 | } 238 | $relative_file = $this->get_relative_file($file); 239 | 240 | if (isset($stat['output'][0])) { 241 | $this->output->echoln(sprintf($format, substr($relative_file, -min(30, strlen($relative_file))), 242 | $stat['status_code'], 243 | count($stat['output'][0]['stats']['failed']) + count($stat['output'][0]['stats']['passed']), 244 | count($stat['output'][0]['stats']['failed']), count($stat['output'][0]['stats']['errors']), 245 | implode(' ', $stat['output'][0]['stats']['failed']))); 246 | } else { 247 | $this->output->echoln(sprintf($format, substr($relative_file, -min(30, strlen($relative_file))), 248 | $stat['status_code'], '', '', '')); 249 | } 250 | } 251 | 252 | $this->output->red_bar(sprintf('Failed %d/%d test scripts, %.2f%% okay. %d/%d subtests failed, %.2f%% okay.', 253 | $nb_failed_files = count($this->stats['failed_files']), 254 | $nb_files = count($this->files), 255 | ($nb_files - $nb_failed_files) * 100 / $nb_files, 256 | $nb_failed_tests = $this->stats['failed_tests'], 257 | $nb_tests = $this->stats['total'], 258 | $nb_tests > 0 ? ($nb_tests - $nb_failed_tests) * 100 / $nb_tests : 0 259 | )); 260 | 261 | if ($this->options['verbose']) { 262 | foreach ($this->to_array() as $testsuite) { 263 | $first = true; 264 | foreach ($testsuite['stats']['failed'] as $testcase) { 265 | if (!isset($testsuite['tests'][$testcase]['file'])) { 266 | continue; 267 | } 268 | 269 | if ($first) { 270 | $this->output->echoln(''); 271 | $this->output->error($this->get_relative_file($testsuite['file']) . $this->extension); 272 | $first = false; 273 | } 274 | 275 | $this->output->comment(sprintf(' at %s line %s', 276 | $this->get_relative_file($testsuite['tests'][$testcase]['file']) . $this->extension, 277 | $testsuite['tests'][$testcase]['line'])); 278 | $this->output->info(' ' . $testsuite['tests'][$testcase]['message']); 279 | $this->output->echoln($testsuite['tests'][$testcase]['error'], null, false); 280 | } 281 | } 282 | } 283 | } else { 284 | $this->output->green_bar(' All tests successful.'); 285 | $this->output->green_bar(sprintf(' Files=%d, Tests=%d', count($this->files), $this->stats['total'])); 286 | } 287 | 288 | return $this->stats['failed_files'] ? false : true; 289 | } 290 | 291 | /** 292 | * @return array 293 | */ 294 | public function get_failed_files() 295 | { 296 | return isset($this->stats['failed_files']) ? $this->stats['failed_files'] : array(); 297 | } 298 | } -------------------------------------------------------------------------------- /Includes/limeOutput.php: -------------------------------------------------------------------------------- 1 | colorizer = new lime_colorizer($force_colors); 19 | $this->base_dir = $base_dir === null ? getcwd() : $base_dir; 20 | } 21 | 22 | /** 23 | * Produces an Echo 24 | */ 25 | public function diag() 26 | { 27 | $messages = func_get_args(); 28 | foreach ($messages as $message) { 29 | echo $this->colorizer->colorize('# ' . join("\n# ", (array)$message), 'COMMENT') . "\n"; 30 | } 31 | } 32 | 33 | /** 34 | * @param $message 35 | */ 36 | public function comment($message) 37 | { 38 | echo $this->colorizer->colorize(sprintf('# %s', $message), 'COMMENT') . "\n"; 39 | } 40 | 41 | /** 42 | * @param $message 43 | */ 44 | public function info($message) 45 | { 46 | echo $this->colorizer->colorize(sprintf('> %s', $message), 'INFO_BAR') . "\n"; 47 | } 48 | 49 | /** 50 | * @param $message 51 | * @param null $file 52 | * @param null $line 53 | * @param array $traces 54 | */ 55 | public function error($message, $file = null, $line = null, $traces = array()) 56 | { 57 | if ($file !== null) { 58 | $message .= sprintf("\n(in %s on line %s)", $file, $line); 59 | } 60 | 61 | // some error messages contain absolute file paths 62 | $message = $this->strip_base_dir($message); 63 | 64 | $space = $this->colorizer->colorize(str_repeat(' ', 71), 'RED_BAR') . "\n"; 65 | $message = trim($message); 66 | $message = wordwrap($message, 66, "\n"); 67 | 68 | echo "\n" . $space; 69 | foreach (explode("\n", $message) as $message_line) { 70 | echo $this->colorizer->colorize(str_pad(' ' . $message_line, 71, ' '), 'RED_BAR') . "\n"; 71 | } 72 | echo $space . "\n"; 73 | 74 | if (count($traces) > 0) { 75 | echo $this->colorizer->colorize('Exception trace:', 'COMMENT') . "\n"; 76 | 77 | $this->print_trace(null, $file, $line); 78 | 79 | foreach ($traces as $trace) { 80 | if (array_key_exists('class', $trace)) { 81 | $method = sprintf('%s%s%s()', $trace['class'], $trace['type'], $trace['function']); 82 | } else { 83 | $method = sprintf('%s()', $trace['function']); 84 | } 85 | 86 | if (array_key_exists('file', $trace)) { 87 | $this->print_trace($method, $trace['file'], $trace['line']); 88 | } else { 89 | $this->print_trace($method); 90 | } 91 | } 92 | 93 | echo "\n"; 94 | } 95 | } 96 | 97 | /** 98 | * @param string $method 99 | */ 100 | protected function print_trace($method = null, $file = null, $line = null) 101 | { 102 | if (!is_null($method)) { 103 | $method .= ' '; 104 | } 105 | 106 | echo ' ' . $method . 'at '; 107 | 108 | if (!is_null($file) && !is_null($line)) { 109 | printf("%s:%s\n", $this->colorizer->colorize($this->strip_base_dir($file), 'TRACE'), 110 | $this->colorizer->colorize($line, 'TRACE')); 111 | } else { 112 | echo "[internal function]\n"; 113 | } 114 | } 115 | 116 | /** 117 | * @param $message 118 | * @param null $colorizer_parameter 119 | * @param bool|true $colorize 120 | */ 121 | public function echoln($message, $colorizer_parameter = null, $colorize = true) 122 | { 123 | if ($colorize) { 124 | $message = preg_replace('/(?:^|\.)((?:not ok|dubious|errors) *\d*)\b/e', 125 | '$this->colorizer->colorize(\'$1\', \'ERROR\')', $message); 126 | $message = preg_replace('/(?:^|\.)(ok *\d*)\b/e', '$this->colorizer->colorize(\'$1\', \'INFO\')', $message); 127 | $message = preg_replace('/"(.+?)"/e', '$this->colorizer->colorize(\'$1\', \'PARAMETER\')', $message); 128 | $message = preg_replace('/(\->|\:\:)?([a-zA-Z0-9_]+?)\(\)/e', 129 | '$this->colorizer->colorize(\'$1$2()\', \'PARAMETER\')', $message); 130 | } 131 | 132 | echo ($colorizer_parameter ? $this->colorizer->colorize($message, $colorizer_parameter) : $message) . "\n"; 133 | } 134 | 135 | /** 136 | * @param string $message 137 | */ 138 | public function green_bar($message) 139 | { 140 | echo $this->colorizer->colorize($message . str_repeat(' ', 71 - min(71, strlen($message))), 'GREEN_BAR') . "\n"; 141 | } 142 | 143 | /** 144 | * @param string $message 145 | */ 146 | public function red_bar($message) 147 | { 148 | echo $this->colorizer->colorize($message . str_repeat(' ', 71 - min(71, strlen($message))), 'RED_BAR') . "\n"; 149 | } 150 | 151 | /** 152 | * @return string 153 | */ 154 | protected function strip_base_dir($text) 155 | { 156 | return str_replace(DIRECTORY_SEPARATOR, '/', 157 | str_replace(realpath($this->base_dir) . DIRECTORY_SEPARATOR, '', $text)); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Includes/limeRegistration.php: -------------------------------------------------------------------------------- 1 | files[] = realpath($f_or_d); 22 | } elseif (is_dir($f_or_d)) { 23 | $this->register_dir($f_or_d); 24 | } else { 25 | throw new Exception(sprintf('The file or directory "%s" does not exist.', $f_or_d)); 26 | } 27 | } 28 | } 29 | 30 | /** 31 | * @param $glob 32 | */ 33 | public function register_glob($glob) 34 | { 35 | if ($dirs = glob($glob)) { 36 | foreach ($dirs as $file) { 37 | $this->files[] = realpath($file); 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * @param $directory 44 | * @throws Exception 45 | */ 46 | public function register_dir($directory) 47 | { 48 | if (!is_dir($directory)) { 49 | throw new Exception(sprintf('The directory "%s" does not exist.', $directory)); 50 | } 51 | 52 | $files = array(); 53 | 54 | $current_dir = opendir($directory); 55 | while ($entry = readdir($current_dir)) { 56 | if ($entry == '.' || $entry == '..') { 57 | continue; 58 | } 59 | 60 | if (is_dir($entry)) { 61 | $this->register_dir($entry); 62 | } elseif (preg_match('#' . $this->extension . '$#', $entry)) { 63 | $files[] = realpath($directory . DIRECTORY_SEPARATOR . $entry); 64 | } 65 | } 66 | 67 | $this->files = array_merge($this->files, $files); 68 | } 69 | 70 | /** 71 | * @param $file 72 | * @return mixed 73 | */ 74 | protected function get_relative_file($file) 75 | { 76 | return str_replace(DIRECTORY_SEPARATOR, '/', 77 | str_replace(array(realpath($this->base_dir) . DIRECTORY_SEPARATOR, $this->extension), '', $file)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Includes/lime_test.php: -------------------------------------------------------------------------------- 1 | 8 | * @version SVN: $Id$ 9 | */ 10 | class lime_test 11 | { 12 | const EPSILON = 0.0000000001; 13 | 14 | protected $test_nb = 0; 15 | protected $output = null; 16 | protected $results = array(); 17 | protected $options = array(); 18 | 19 | static protected $all_results = array(); 20 | 21 | /** 22 | * Constructor for the lime.php class 23 | * 24 | * @param null $plan 25 | * @param array $options 26 | */ 27 | public function __construct($plan = null, $options = array()) 28 | { 29 | // for BC 30 | if (!is_array($options)) { 31 | $options = array('output' => $options); 32 | } 33 | 34 | $this->options = array_merge(array( 35 | 'force_colors' => false, 36 | 'output' => null, 37 | 'verbose' => false, 38 | 'error_reporting' => false, 39 | ), $options); 40 | 41 | $this->output = $this->options['output'] ? $this->options['output'] : new lime_output($this->options['force_colors']); 42 | 43 | $caller = $this->find_caller(debug_backtrace()); 44 | self::$all_results[] = array( 45 | 'file' => $caller[0], 46 | 'tests' => array(), 47 | 'stats' => array( 48 | 'plan' => $plan, 49 | 'total' => 0, 50 | 'failed' => array(), 51 | 'passed' => array(), 52 | 'skipped' => array(), 53 | 'errors' => array() 54 | ), 55 | ); 56 | 57 | $this->results = &self::$all_results[count(self::$all_results) - 1]; 58 | 59 | null !== $plan and $this->output->echoln(sprintf("1..%d", $plan)); 60 | 61 | set_error_handler(array($this, 'handle_error')); 62 | set_exception_handler(array($this, 'handle_exception')); 63 | } 64 | 65 | static public function reset() 66 | { 67 | self::$all_results = array(); 68 | } 69 | 70 | /** 71 | * @return array 72 | */ 73 | static public function to_array() 74 | { 75 | return self::$all_results; 76 | } 77 | 78 | /** 79 | * @param null $results 80 | * @return string 81 | */ 82 | static public function to_xml($results = null) 83 | { 84 | if (is_null($results)) { 85 | $results = self::$all_results; 86 | } 87 | 88 | $dom = new DOMDocument('1.0', 'UTF-8'); 89 | $dom->formatOutput = true; 90 | $dom->appendChild($testsuites = $dom->createElement('testsuites')); 91 | 92 | $failures = 0; 93 | $errors = 0; 94 | $skipped = 0; 95 | $assertions = 0; 96 | 97 | foreach ($results as $result) { 98 | $testsuites->appendChild($testsuite = $dom->createElement('testsuite')); 99 | $testsuite->setAttribute('name', basename($result['file'], '.php')); 100 | $testsuite->setAttribute('file', $result['file']); 101 | $testsuite->setAttribute('failures', count($result['stats']['failed'])); 102 | $testsuite->setAttribute('errors', count($result['stats']['errors'])); 103 | $testsuite->setAttribute('skipped', count($result['stats']['skipped'])); 104 | $testsuite->setAttribute('tests', $result['stats']['plan']); 105 | $testsuite->setAttribute('assertions', $result['stats']['plan']); 106 | 107 | $failures += count($result['stats']['failed']); 108 | $errors += count($result['stats']['errors']); 109 | $skipped += count($result['stats']['skipped']); 110 | $assertions += $result['stats']['plan']; 111 | 112 | foreach ($result['tests'] as $test) { 113 | $testsuite->appendChild($testcase = $dom->createElement('testcase')); 114 | $testcase->setAttribute('name', $test['message']); 115 | $testcase->setAttribute('file', $test['file']); 116 | $testcase->setAttribute('line', $test['line']); 117 | $testcase->setAttribute('assertions', 1); 118 | if (!$test['status']) { 119 | $testcase->appendChild($failure = $dom->createElement('failure')); 120 | $failure->setAttribute('type', 'lime'); 121 | if (isset($test['error'])) { 122 | $failure->appendChild($dom->createTextNode($test['error'])); 123 | } 124 | } 125 | } 126 | } 127 | 128 | $testsuites->setAttribute('failures', $failures); 129 | $testsuites->setAttribute('errors', $errors); 130 | $testsuites->setAttribute('tests', $assertions); 131 | $testsuites->setAttribute('assertions', $assertions); 132 | $testsuites->setAttribute('skipped', $skipped); 133 | 134 | return $dom->saveXml(); 135 | } 136 | 137 | /** 138 | * 139 | */ 140 | public function __destruct() 141 | { 142 | $plan = $this->results['stats']['plan']; 143 | $passed = count($this->results['stats']['passed']); 144 | $failed = count($this->results['stats']['failed']); 145 | $total = $this->results['stats']['total']; 146 | is_null($plan) and $plan = $total and $this->output->echoln(sprintf("1..%d", $plan)); 147 | 148 | if ($total > $plan) { 149 | $this->output->red_bar(sprintf("# Looks like you planned %d tests but ran %d extra.", $plan, 150 | $total - $plan)); 151 | } elseif ($total < $plan) { 152 | $this->output->red_bar(sprintf("# Looks like you planned %d tests but only ran %d.", $plan, $total)); 153 | } 154 | 155 | if ($failed) { 156 | $this->output->red_bar(sprintf("# Looks like you failed %d tests of %d.", $failed, $passed + $failed)); 157 | } else { 158 | if ($total == $plan) { 159 | $this->output->green_bar("# Looks like everything went fine."); 160 | } 161 | } 162 | 163 | flush(); 164 | } 165 | 166 | /** 167 | * Tests a condition and passes if it is true 168 | * 169 | * @param mixed $exp condition to test 170 | * @param string $message display output message when the test passes 171 | * 172 | * @return boolean 173 | */ 174 | public function ok($exp, $message = '') 175 | { 176 | $this->update_stats(); 177 | 178 | if ($result = (boolean)$exp) { 179 | $this->results['stats']['passed'][] = $this->test_nb; 180 | } else { 181 | $this->results['stats']['failed'][] = $this->test_nb; 182 | } 183 | $this->results['tests'][$this->test_nb]['message'] = $message; 184 | $this->results['tests'][$this->test_nb]['status'] = $result; 185 | $this->output->echoln(sprintf("%s %d%s", $result ? 'ok' : 'not ok', $this->test_nb, 186 | $message = $message ? sprintf('%s %s', 0 === strpos($message, '#') ? '' : ' -', $message) : '')); 187 | 188 | if (!$result) { 189 | $this->output->diag(sprintf(' Failed test (%s at line %d)', 190 | str_replace(getcwd(), '.', $this->results['tests'][$this->test_nb]['file']), 191 | $this->results['tests'][$this->test_nb]['line'])); 192 | } 193 | 194 | return $result; 195 | } 196 | 197 | /** 198 | * Compares two values and passes if they are equal (==) 199 | * 200 | * @param mixed $exp1 left value 201 | * @param mixed $exp2 right value 202 | * @param string $message display output message when the test passes 203 | * 204 | * @return boolean 205 | */ 206 | public function is($exp1, $exp2, $message = '') 207 | { 208 | if (is_object($exp1) || is_object($exp2)) { 209 | $value = $exp1 === $exp2; 210 | } else { 211 | if (is_float($exp1) && is_float($exp2)) { 212 | $value = abs($exp1 - $exp2) < self::EPSILON; 213 | } else { 214 | $value = $exp1 == $exp2; 215 | } 216 | } 217 | 218 | if (!$result = $this->ok($value, $message)) { 219 | $this->set_last_test_errors(array( 220 | sprintf(" got: %s", var_export($exp1, true)), 221 | sprintf(" expected: %s", var_export($exp2, true)) 222 | )); 223 | } 224 | 225 | return $result; 226 | } 227 | 228 | /** 229 | * Compares two values and passes if they are not equal 230 | * 231 | * @param mixed $exp1 left value 232 | * @param mixed $exp2 right value 233 | * @param string $message display output message when the test passes 234 | * 235 | * @return boolean 236 | */ 237 | public function isnt($exp1, $exp2, $message = '') 238 | { 239 | if (!$result = $this->ok($exp1 != $exp2, $message)) { 240 | $this->set_last_test_errors(array( 241 | sprintf(" %s", var_export($exp2, true)), 242 | ' ne', 243 | sprintf(" %s", var_export($exp2, true)) 244 | )); 245 | } 246 | 247 | return $result; 248 | } 249 | 250 | /** 251 | * Tests a string against a regular expression 252 | * 253 | * @param string $exp value to test 254 | * @param string $regex the pattern to search for, as a string 255 | * @param string $message display output message when the test passes 256 | * 257 | * @return boolean 258 | */ 259 | public function like($exp, $regex, $message = '') 260 | { 261 | if (!$result = $this->ok(preg_match($regex, $exp), $message)) { 262 | $this->set_last_test_errors(array( 263 | sprintf(" '%s'", $exp), 264 | sprintf(" doesn't match '%s'", $regex) 265 | )); 266 | } 267 | 268 | return $result; 269 | } 270 | 271 | /** 272 | * Checks that a string doesn't match a regular expression 273 | * 274 | * @param string $exp value to test 275 | * @param string $regex the pattern to search for, as a string 276 | * @param string $message display output message when the test passes 277 | * 278 | * @return boolean 279 | */ 280 | public function unlike($exp, $regex, $message = '') 281 | { 282 | if (!$result = $this->ok(!preg_match($regex, $exp), $message)) { 283 | $this->set_last_test_errors(array( 284 | sprintf(" '%s'", $exp), 285 | sprintf(" matches '%s'", $regex) 286 | )); 287 | } 288 | 289 | return $result; 290 | } 291 | 292 | /** 293 | * Compares two arguments with an operator 294 | * 295 | * @param mixed $exp1 left value 296 | * @param string $op operator 297 | * @param mixed $exp2 right value 298 | * @param string $message display output message when the test passes 299 | * 300 | * @return boolean 301 | */ 302 | public function cmp_ok($exp1, $op, $exp2, $message = '') 303 | { 304 | $result = false; 305 | $php = sprintf("\$result = \$exp1 $op \$exp2;"); 306 | // under some unknown conditions the sprintf() call causes a segmentation fault 307 | // when placed directly in the eval() call 308 | eval($php); 309 | 310 | if (!$this->ok($result, $message)) { 311 | $this->set_last_test_errors(array( 312 | sprintf(" %s", str_replace("\n", '', var_export($exp1, true))), 313 | sprintf(" %s", $op), 314 | sprintf(" %s", str_replace("\n", '', var_export($exp2, true))) 315 | )); 316 | } 317 | 318 | return $result; 319 | } 320 | 321 | /** 322 | * Checks the availability of a method for an object or a class 323 | * 324 | * @param mixed $object an object instance or a class name 325 | * @param string|array $methods one or more method names 326 | * @param string $message display output message when the test passes 327 | * 328 | * @return boolean 329 | */ 330 | public function can_ok($object, $methods, $message = '') 331 | { 332 | $result = true; 333 | $failed_messages = array(); 334 | foreach ((array)$methods as $method) { 335 | if (!method_exists($object, $method)) { 336 | $failed_messages[] = sprintf(" method '%s' does not exist", $method); 337 | $result = false; 338 | } 339 | } 340 | 341 | !$this->ok($result, $message); 342 | 343 | !$result and $this->set_last_test_errors($failed_messages); 344 | 345 | return $result; 346 | } 347 | 348 | /** 349 | * Checks the type of an argument 350 | * 351 | * @param mixed $var variable instance 352 | * @param string $class class or type name 353 | * @param string $message display output message when the test passes 354 | * 355 | * @return boolean 356 | */ 357 | public function isa_ok($var, $class, $message = '') 358 | { 359 | $type = is_object($var) ? get_class($var) : gettype($var); 360 | if (!$result = $this->ok($type == $class, $message)) { 361 | $this->set_last_test_errors(array(sprintf(" variable isn't a '%s' it's a '%s'", $class, $type))); 362 | } 363 | 364 | return $result; 365 | } 366 | 367 | /** 368 | * Checks that two arrays have the same values 369 | * 370 | * @param mixed $exp1 first variable 371 | * @param mixed $exp2 second variable 372 | * @param string $message display output message when the test passes 373 | * 374 | * @return boolean 375 | */ 376 | public function is_deeply($exp1, $exp2, $message = '') 377 | { 378 | if (!$result = $this->ok($this->test_is_deeply($exp1, $exp2), $message)) { 379 | $this->set_last_test_errors(array( 380 | sprintf(" got: %s", str_replace("\n", '', var_export($exp1, true))), 381 | sprintf(" expected: %s", str_replace("\n", '', var_export($exp2, true))) 382 | )); 383 | } 384 | 385 | return $result; 386 | } 387 | 388 | /** 389 | * is_ignore_nl function. 390 | * 391 | * @access public 392 | * @param mixed $exp1 393 | * @param mixed $exp2 394 | * @param string $message (default: '') 395 | * @return boolean 396 | */ 397 | public function is_ignore_nl($exp1, $exp2, $message = '') 398 | { 399 | return $this->is( 400 | str_replace("\n", '', $exp1), 401 | str_replace("\n", '', $exp2), 402 | $message 403 | ); 404 | } 405 | 406 | /** 407 | * Sortcut for {@link is}(), performs a strict check. 408 | * 409 | * @access public 410 | * @param mixed $exp1 411 | * @param mixed $exp2 412 | * @param string $message (default: '') 413 | * @return boolean 414 | */ 415 | public function is_strict($exp1, $exp2, $message = '') 416 | { 417 | if (is_float($exp1) && is_float($exp2)) { 418 | $value = abs($exp1 - $exp2) < self::EPSILON; 419 | } else { 420 | $value = $exp1 === $exp2; 421 | } 422 | 423 | if (!$result = $this->ok($value, $message)) { 424 | $this->set_last_test_errors(array( 425 | sprintf(" got: (%s) %s", gettype($exp1), var_export($exp1, true)), 426 | sprintf(" expected: (%s) %s", gettype($exp2), var_export($exp2, true)) 427 | )); 428 | } 429 | 430 | return $result; 431 | } 432 | 433 | /** 434 | * Sortcut for {@link isnt}(), performs a strict check. 435 | * 436 | * @access public 437 | * @param mixed $exp1 438 | * @param mixed $exp2 439 | * @param string $message . (default: '') 440 | * @return boolean 441 | */ 442 | public function isnt_strict($exp1, $exp2, $message = '') 443 | { 444 | if (!$result = $this->ok($exp1 !== $exp2, $message)) { 445 | $this->set_last_test_errors(array( 446 | sprintf(" (%s) %s", gettype($exp1), var_export($exp1, true)), 447 | ' ne', 448 | sprintf(" (%s) %s", gettype($exp2), var_export($exp2, true)) 449 | )); 450 | } 451 | 452 | return $result; 453 | } 454 | 455 | /** 456 | * Always passes--useful for testing exceptions 457 | * 458 | * @param string $message display output message 459 | * 460 | * @return boolean 461 | */ 462 | public function pass($message = '') 463 | { 464 | return $this->ok(true, $message); 465 | } 466 | 467 | /** 468 | * Always fails--useful for testing exceptions 469 | * 470 | * @param string $message display output message 471 | * 472 | * @return false 473 | */ 474 | public function fail($message = '') 475 | { 476 | return $this->ok(false, $message); 477 | } 478 | 479 | /** 480 | * Outputs a diag message but runs no test 481 | * 482 | * @param string $message display output message 483 | * 484 | * @return void 485 | */ 486 | public function diag($message) 487 | { 488 | $this->output->diag($message); 489 | } 490 | 491 | /** 492 | * Counts as $nb_tests tests--useful for conditional tests 493 | * 494 | * @param string $message display output message 495 | * @param integer $nb_tests number of tests to skip 496 | * 497 | * @return void 498 | */ 499 | public function skip($message = '', $nb_tests = 1) 500 | { 501 | for ($i = 0; $i < $nb_tests; $i++) { 502 | $this->pass(sprintf("# SKIP%s", $message ? ' ' . $message : '')); 503 | $this->results['stats']['skipped'][] = $this->test_nb; 504 | array_pop($this->results['stats']['passed']); 505 | } 506 | } 507 | 508 | /** 509 | * Counts as a test--useful for tests yet to be written 510 | * 511 | * @param string $message display output message 512 | * 513 | * @return void 514 | */ 515 | public function todo($message = '') 516 | { 517 | $this->pass(sprintf("# TODO%s", $message ? ' ' . $message : '')); 518 | $this->results['stats']['skipped'][] = $this->test_nb; 519 | array_pop($this->results['stats']['passed']); 520 | } 521 | 522 | /** 523 | * Validates that a file exists and that it is properly included 524 | * 525 | * @param string $file file path 526 | * @param string $message display output message when the test passes 527 | * 528 | * @return boolean 529 | */ 530 | public function include_ok($file, $message = '') 531 | { 532 | if (!$result = $this->ok((@include($file)) == 1, $message)) { 533 | $this->set_last_test_errors(array(sprintf(" Tried to include '%s'", $file))); 534 | } 535 | 536 | return $result; 537 | } 538 | 539 | /** 540 | * @param $var1 541 | * @param $var2 542 | * @return bool 543 | */ 544 | private function test_is_deeply($var1, $var2) 545 | { 546 | if (gettype($var1) != gettype($var2)) { 547 | return false; 548 | } 549 | 550 | if (is_array($var1)) { 551 | ksort($var1); 552 | ksort($var2); 553 | 554 | $keys1 = array_keys($var1); 555 | $keys2 = array_keys($var2); 556 | if (array_diff($keys1, $keys2) || array_diff($keys2, $keys1)) { 557 | return false; 558 | } 559 | $is_equal = true; 560 | foreach ($var1 as $key => $value) { 561 | $is_equal = $this->test_is_deeply($var1[$key], $var2[$key]); 562 | if ($is_equal === false) { 563 | break; 564 | } 565 | } 566 | 567 | return $is_equal; 568 | } else { 569 | return $var1 === $var2; 570 | } 571 | } 572 | 573 | /** 574 | * @param $message 575 | */ 576 | public function comment($message) 577 | { 578 | $this->output->comment($message); 579 | } 580 | 581 | /** 582 | * @param $message 583 | */ 584 | public function info($message) 585 | { 586 | $this->output->info($message); 587 | } 588 | 589 | /** 590 | * @param string $message 591 | */ 592 | public function error($message, $file = null, $line = null, array $traces = array()) 593 | { 594 | $this->output->error($message, $file, $line, $traces); 595 | 596 | $this->results['stats']['errors'][] = array( 597 | 'message' => $message, 598 | 'file' => $file, 599 | 'line' => $line, 600 | ); 601 | } 602 | 603 | protected function update_stats() 604 | { 605 | ++$this->test_nb; 606 | ++$this->results['stats']['total']; 607 | 608 | list($this->results['tests'][$this->test_nb]['file'], $this->results['tests'][$this->test_nb]['line']) = $this->find_caller(debug_backtrace()); 609 | } 610 | 611 | /** 612 | * @param array $errors 613 | */ 614 | protected function set_last_test_errors(array $errors) 615 | { 616 | $this->output->diag($errors); 617 | 618 | $this->results['tests'][$this->test_nb]['error'] = implode("\n", $errors); 619 | } 620 | 621 | /** 622 | * @param $traces 623 | * @return array 624 | */ 625 | protected function find_caller($traces) 626 | { 627 | // find the first call to a method of an object that is an instance of lime_test 628 | $t = array_reverse($traces); 629 | foreach ($t as $trace) { 630 | if (isset($trace['object']) && $trace['object'] instanceof lime_test) { 631 | return array($trace['file'], $trace['line']); 632 | } 633 | } 634 | 635 | // return the first call 636 | $last = count($traces) - 1; 637 | 638 | return array($traces[$last]['file'], $traces[$last]['line']); 639 | } 640 | 641 | /** 642 | * @param $code 643 | * @param $message 644 | * @param $file 645 | * @param $line 646 | * @param $context 647 | * @return bool 648 | * 649 | * @FIXME: This method has inconsistent return types 650 | */ 651 | public function handle_error($code, $message, $file, $line, $context) 652 | { 653 | if (!$this->options['error_reporting'] || ($code & error_reporting()) == 0) { 654 | return false; 655 | } 656 | 657 | switch ($code) { 658 | case E_WARNING: 659 | $type = 'Warning'; 660 | break; 661 | default: 662 | $type = 'Notice'; 663 | break; 664 | } 665 | 666 | $trace = debug_backtrace(); 667 | array_shift($trace); // remove the handle_error() call from the trace 668 | 669 | $this->error($type . ': ' . $message, $file, $line, $trace); 670 | } 671 | 672 | /** 673 | * @param Exception $exception 674 | * @return bool 675 | */ 676 | public function handle_exception(Exception $exception) 677 | { 678 | $this->error(get_class($exception) . ': ' . $exception->getMessage(), $exception->getFile(), 679 | $exception->getLine(), $exception->getTrace()); 680 | 681 | // exception was handled 682 | return true; 683 | } 684 | } -------------------------------------------------------------------------------- /Init.php: -------------------------------------------------------------------------------- 1 | . 18 | 19 | Peachy is not responsible for any damage caused to the system running it. 20 | */ 21 | 22 | /** 23 | * @file 24 | * Main Peachy file 25 | * Defines constants, initializes global variables 26 | * Stores Peachy 27 | * @license GPL 28 | */ 29 | 30 | /** 31 | * The version that Peachy is running 32 | */ 33 | define( 'PEACHYVERSION', '2.0 (alpha 8)' ); 34 | 35 | /** 36 | * Minimum MediaWiki version that is required for Peachy 37 | */ 38 | define( 'MINMW', '1.23' ); 39 | 40 | /** 41 | * Minimum PHP version that is required for Peachy 42 | */ 43 | define( 'MINPHP', '5.2.1' ); 44 | 45 | /** 46 | * PECHO constants, used for {@link outputText}() 47 | */ 48 | define( 'PECHO_VERBOSE', -1 ); 49 | 50 | /** 51 | * PECHO constants, used for {@link outputText}() 52 | */ 53 | define( 'PECHO_NORMAL', 0 ); 54 | 55 | /** 56 | * PECHO constants, used for {@link outputText}() 57 | */ 58 | define( 'PECHO_NOTICE', 1 ); 59 | 60 | /** 61 | * PECHO constants, used for {@link outputText}() 62 | */ 63 | define( 'PECHO_WARN', 2 ); 64 | 65 | /** 66 | * PECHO constants, used for {@link outputText}() 67 | */ 68 | define( 'PECHO_ERROR', 3 ); 69 | 70 | /** 71 | * PECHO constants, used for {@link outputText}() 72 | */ 73 | define( 'PECHO_FATAL', 4 ); 74 | 75 | $pgIP = dirname( __FILE__ ) . DIRECTORY_SEPARATOR; 76 | 77 | //If out /tmp directory doesnt exist, make it! 78 | if( !file_exists( __DIR__ . '/tmp' ) ) { 79 | mkdir(__DIR__ . '/tmp'); 80 | } 81 | 82 | require_once( $pgIP . 'Includes/Exceptions.php' ); 83 | require_once( $pgIP . 'Includes/AutoUpdate.php' ); 84 | 85 | peachyCheckPHPVersion( MINPHP ); 86 | 87 | $pgHooks = array(); 88 | $pgProxy = array(); 89 | $pgUA = 'Peachy MediaWiki Bot API Version ' . PEACHYVERSION; 90 | 91 | require_once( $pgIP . 'Includes/Hooks.php' ); 92 | require_once( $pgIP . 'HTTP.php' ); 93 | require_once( $pgIP . 'Includes/Autoloader.php' ); 94 | require_once( $pgIP . 'GenFunctions.php' ); 95 | require_once( $pgIP . 'config.inc.php' ); 96 | require_once( $pgIP . 'Includes/SSH.php' ); 97 | require_once($pgIP . 'Includes/lime.php'); 98 | 99 | $pgVerbose = array(); 100 | if( $pgDisplayPechoVerbose ) $pgVerbose[] = -1; 101 | if( $pgDisplayPechoNormal ) $pgVerbose[] = 0; 102 | if( $pgDisplayPechoNotice ) $pgVerbose[] = 1; 103 | if( $pgDisplayPechoWarn ) $pgVerbose[] = 2; 104 | if( $pgDisplayPechoError ) $pgVerbose[] = 3; 105 | if( $pgDisplayPechoFatal ) $pgVerbose[] = 4; 106 | 107 | $pgIRCTrigger = array( '!', '.' ); 108 | 109 | //Last version check 110 | $tmp = null; 111 | 112 | if( function_exists( 'mb_internal_encoding' ) ) { 113 | mb_internal_encoding("UTF-8"); 114 | } 115 | 116 | // Suppress warnings if timezone not set on server 117 | date_default_timezone_set( @date_default_timezone_get() ); 118 | 119 | //Check for updates before loading Peachy. 120 | if( !$pgDisableUpdates && !defined( 'PEACHY_PHPUNIT_TESTS' ) ) { 121 | //the below MUST have its own Http object or else things will break 122 | $updater = new AutoUpdate(new HTTP()); 123 | $Uptodate = $updater->Checkforupdate(); 124 | if (!$Uptodate) { 125 | $updater->updatePeachy(); 126 | } 127 | } 128 | 129 | require_once( $pgIP . 'Includes/Peachy.php' ); 130 | 131 | /** 132 | * Simple version_compare() wrapper 133 | * @param string $check_version string 134 | * @throws DependencyError 135 | * @return void 136 | */ 137 | function peachyCheckPHPVersion( $check_version ) 138 | { 139 | if (version_compare(PHP_VERSION, $check_version, '<')) { 140 | throw new DependencyError("PHP " . $check_version, "http://php.net/downloads.php"); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Plugins/AbuseFilter.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | class AbuseFilter { 21 | 22 | /** 23 | * Wiki class 24 | * 25 | * @var Wiki 26 | * @access private 27 | */ 28 | private $wiki; 29 | 30 | /** 31 | * Construction method for the AbuseFilter class 32 | * 33 | * @access public 34 | * @param Wiki &$wikiClass The Wiki class object 35 | * @throws DependencyError 36 | */ 37 | public function __construct(Wiki &$wikiClass) 38 | { 39 | $this->wiki = $wikiClass; 40 | 41 | if( !array_key_exists( 'Abuse Filter', $wikiClass->get_extensions() ) ) { 42 | throw new DependencyError( "AbuseFilter", "http://www.mediawiki.org/wiki/Extension:AbuseFilter" ); 43 | } 44 | } 45 | 46 | /** 47 | * Returns the abuse filter log 48 | * 49 | * @access public 50 | * @param int $filter Filter ID. Default null 51 | * @param string $user Show entries by this user. Default null 52 | * @param string $title Show entries to this title. Default null 53 | * @param int $limit Number of entries to retrieve. Defautl null 54 | * @param string $start Timestamp to start at. Default null 55 | * @param string $end Timestamp to end at. Default null 56 | * @param string $dir Direction to list. Default older 57 | * @param array $prop Properties to retrieve. Default array( 'ids', 'filter', 'user', 'ip', 'title', 'action', 'details', 'result', 'timestamp' ) 58 | * @return array 59 | */ 60 | public function abuselog( $filter = null, $user = null, $title = null, $limit = null, $start = null, $end = null, $dir = 'older', $prop = array( 61 | 'ids', 'filter', 'user', 'ip', 'title', 'action', 'details', 'result', 'timestamp' 62 | ) ) { 63 | 64 | $tArray = array( 65 | 'prop' => $prop, 66 | '_code' => 'afl', 67 | 'afldir' => $dir, 68 | '_limit' => $limit, 69 | 'list' => 'abuselog', 70 | ); 71 | 72 | if( !is_null( $filter ) ) $tArray['aflfilter'] = $filter; 73 | if( !is_null( $user ) ) $tArray['afluser'] = $user; 74 | if( !is_null( $title ) ) $tArray['afltitle'] = $title; 75 | if( !is_null( $start ) ) $tArray['aflstart'] = $start; 76 | if( !is_null( $end ) ) $tArray['aflend'] = $end; 77 | 78 | pecho( "Getting abuse log...\n\n", PECHO_NORMAL ); 79 | 80 | return $this->wiki->listHandler( $tArray ); 81 | } 82 | 83 | /** 84 | * Returns a list of all filters 85 | * 86 | * @access public 87 | * @param int $start Filter ID to start at. Default null 88 | * @param int $end Filter ID to end at. Default null 89 | * @param string $dir Direction to list. Default newer 90 | * @param bool $enabled Only list enabled filters. true => only enabled, false => only disabled, null => all 91 | * @param bool $deleted Only list deleted filters. true => only deleted, false => only non-deleted, null => all 92 | * @param bool $private Only list private filters. true => only private, false => only non-private, null => all 93 | * @param int $limit Number of filters to get. Default null 94 | * @param array $prop Properties to retrieve. Default array( 'id', 'description', 'pattern', 'actions', 'hits', 'comments', 'lasteditor', 'lastedittime', 'status', 'private' ) 95 | * @return array 96 | */ 97 | public function abusefilters( $start = null, $end = null, $dir = 'newer', $enabled = null, $deleted = false, $private = null, $limit = null, $prop = array( 98 | 'id', 'description', 'pattern', 'actions', 'hits', 'comments', 'lasteditor', 'lastedittime', 'status', 'private' 99 | ) ) { 100 | 101 | $tArray = array( 102 | 'prop' => $prop, 103 | '_code' => 'abf', 104 | 'abfdir' => $dir, 105 | '_limit' => $limit, 106 | 'abfshow' => array(), 107 | 'list' => 'abusefilters' 108 | ); 109 | 110 | if( !is_null( $enabled ) ) { 111 | if( $enabled ) { 112 | $tArray['abfshow'][] = 'enabled'; 113 | } else { 114 | $tArray['abfshow'][] = '!enabled'; 115 | } 116 | } 117 | 118 | if( !is_null( $deleted ) ) { 119 | if( $deleted ) { 120 | $tArray['abfshow'][] = 'deleted'; 121 | } else { 122 | $tArray['abfshow'][] = '!deleted'; 123 | } 124 | } 125 | 126 | if( !is_null( $private ) ) { 127 | if( $private ) { 128 | $tArray['abfshow'][] = 'private'; 129 | } else { 130 | $tArray['abfshow'][] = '!private'; 131 | } 132 | } 133 | 134 | $tArray['abfshow'] = implode( '|', $tArray['abfshow'] ); 135 | 136 | if( !is_null( $start ) ) $tArray['abfstartid'] = $start; 137 | if( !is_null( $end ) ) $tArray['abfendid'] = $end; 138 | 139 | pecho( "Getting abuse filter list...\n\n", PECHO_NORMAL ); 140 | 141 | return $this->wiki->listHandler( $tArray ); 142 | } 143 | 144 | 145 | } -------------------------------------------------------------------------------- /Plugins/CheckUser.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | class CheckUser { 23 | 24 | public function __construct(Wiki &$wikiclass = null) 25 | { 26 | 27 | $extensions = $wikiClass->get_extensions(); 28 | if( !array_key_exists( 'CheckUser', $extensions ) ) { 29 | throw new DependencyError( "CheckUser", "http://www.mediawiki.org/wiki/Extension:CheckUser" ); 30 | } elseif( $extensions['CheckUser'] < 3 ) { 31 | throw new DependencyError( "CheckUser version 3.0 or up", "http://www.mediawiki.org/wiki/Extension:CheckUser" ); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /Plugins/CodeReview.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | class CodeReview { 21 | 22 | private $wiki; 23 | private $repo; 24 | 25 | public function __construct(Wiki &$wikiClass, $repo) 26 | { 27 | $this->wiki = $wikiClass; 28 | $this->repo = $repo; 29 | 30 | if( !array_key_exists( 'CodeReview', $wikiClass->get_extensions() ) ) { 31 | throw new DependencyError( "CodeReview", "http://www.mediawiki.org/wiki/Extension:CodeReview" ); 32 | } 33 | } 34 | 35 | public function update( $rev ) { 36 | 37 | Hooks::runHook( 'StartCodeReviewUpdate', array( &$rev ) ); 38 | 39 | $apiRes = $this->wiki->apiQuery( 40 | array( 41 | 'action' => 'codeupdate', 42 | 'repo' => $this->repo, 43 | 'rev' => $rev 44 | ), true 45 | ); 46 | 47 | print_r( $apiRes ); 48 | } 49 | 50 | public function diff() { } 51 | 52 | public function testupload() { } 53 | 54 | public function comments() { } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Plugins/Email.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | class Email { 21 | /** 22 | * Who to send to 23 | * @var array 24 | */ 25 | private $mTo = array(); 26 | 27 | /** 28 | * Who the message is from 29 | * @var array 30 | */ 31 | private $mFrom = array(); 32 | 33 | /** 34 | * Subject 35 | * @var string 36 | */ 37 | private $mSubject; 38 | 39 | /** 40 | * Message to send 41 | * @var string 42 | */ 43 | private $mMessage; 44 | 45 | /** 46 | * CC field 47 | * @var array 48 | */ 49 | private $mCC = array(); 50 | 51 | /** 52 | * BCC field 53 | * @var array 54 | */ 55 | private $mBCC = array(); 56 | 57 | /** 58 | * Reply-to field 59 | * @var array 60 | */ 61 | private $mRT = array(); 62 | 63 | /** 64 | * Construct function, adds the From: field, subject, and message 65 | * @param string $fromEmail Email address of sender 66 | * @param string $fromName Name of sender. 67 | * @param string $subject Subject of email 68 | * @param string $message Message to send 69 | * @throws DependencyError 70 | */ 71 | function __construct( $fromEmail, $fromName, $subject, $message ) { 72 | 73 | if( !function_exists( 'mail' ) ) { 74 | throw new DependencyError( "Mail", "http://us4.php.net/manual/en/book.mail.php" ); 75 | } 76 | 77 | if( !is_null( $fromName ) ) { 78 | $this->mFrom[$fromName] = $fromEmail; 79 | } else { 80 | $this->mFrom[] = $fromEmail; 81 | } 82 | $this->mSubject = $subject; 83 | $this->mMessage = $message; 84 | } 85 | 86 | /** 87 | * Adds another email to the To: field. 88 | * @param string $toEmail Email address of recipient 89 | * @param string $toName Name of recipient. Default null 90 | */ 91 | public function addTarget( $toEmail, $toName = null ) { 92 | if( !is_null( $toName ) ) { 93 | $this->mTo[$toName] = $toEmail; 94 | } else { 95 | $this->mTo[] = $toEmail; 96 | } 97 | } 98 | 99 | /** 100 | * Adds another email to the CC: field. 101 | * @param string $ccEmail Email address of cc 102 | * @param string $ccName Name of cc. Default null 103 | */ 104 | public function addCC( $ccEmail, $ccName = null ) { 105 | if( !is_null( $ccName ) ) { 106 | $this->mCC[$ccName] = $ccEmail; 107 | } else { 108 | $this->mCC[] = $ccEmail; 109 | } 110 | } 111 | 112 | /** 113 | * Adds another email to the BCC: field. 114 | * @param string $bccEmail Email address of bcc 115 | * @param string $bccName Name of bcc. Default null 116 | */ 117 | public function addBCC( $bccEmail, $bccName = null ) { 118 | if( !is_null( $bccName ) ) { 119 | $this->mBCC[$bccName] = $bccEmail; 120 | } else { 121 | $this->mBCC[] = $bccEmail; 122 | } 123 | } 124 | 125 | /** 126 | * Adds another email to the Reply-to: field. 127 | * @param string $rtEmail Email address to reply to 128 | * @param string $rtName Name to reply to. Default null 129 | */ 130 | public function addReplyTo( $rtEmail, $rtName = null ) { 131 | if( !is_null( $rtName ) ) { 132 | $this->mRT[$rtName] = $rtEmail; 133 | } else { 134 | $this->mRT[] = $rtEmail; 135 | } 136 | } 137 | 138 | /** 139 | * Sends the email. 140 | */ 141 | public function send() { 142 | $msg = array(); 143 | 144 | $msg_to = array(); 145 | foreach( $this->mTo as $name => $target ){ 146 | if( !is_string( $name ) ) { 147 | $msg_to[] = $target; 148 | } else { 149 | $msg_to[] = "$name <$target>"; 150 | } 151 | } 152 | $msg['to'] = implode( ', ', $msg_to ); 153 | 154 | $msg_from = array(); 155 | foreach( $this->mFrom as $name => $target ){ 156 | if( !is_string( $name ) ) { 157 | $msg_from[] = $target; 158 | } else { 159 | $msg_from[] = "$name <$target>"; 160 | } 161 | } 162 | $msg['from'] = null; 163 | if( count( $msg_from ) > 0 ) $msg['from'] = "From: " . implode( ', ', $msg_from ); 164 | 165 | $msg_cc = array(); 166 | foreach( $this->mCC as $name => $target ){ 167 | if( !is_string( $name ) ) { 168 | $msg_cc[] = $target; 169 | } else { 170 | $msg_cc[] = "$name <$target>"; 171 | } 172 | } 173 | $msg['cc'] = null; 174 | if( count( $msg_cc ) > 0 ) $msg['cc'] = "CC: " . implode( ', ', $msg_cc ); 175 | 176 | $msg_bcc = array(); 177 | foreach( $this->mBCC as $name => $target ){ 178 | if( !is_string( $name ) ) { 179 | $msg_bcc[] = $target; 180 | } else { 181 | $msg_bcc[] = "$name"; 182 | } 183 | } 184 | $msg['bcc'] = null; 185 | if( count( $msg_bcc ) > 0 ) $msg['bcc'] = "BCC: " . implode( ', ', $msg_bcc ); 186 | 187 | $msg_rt = array(); 188 | foreach( $this->mRT as $name => $target ){ 189 | if( !is_string( $name ) ) { 190 | $msg_rt[] = $target; 191 | } else { 192 | $msg_rt[] = "$name <$target>"; 193 | } 194 | } 195 | $msg['rt'] = null; 196 | if( count( $msg_rt ) > 0 ) $msg['rt'] = "Reply-to: " . implode( ', ', $msg_rt ); 197 | 198 | $msg['subject'] = $this->mSubject; 199 | $msg['message'] = $this->mMessage; 200 | $msg['version'] = 'X-Mailer: PHP/' . phpversion(); 201 | 202 | $msg['headers'] = $msg['from'] . "\r\n"; 203 | if( !is_null( $msg['rt'] ) ) $msg['headers'] .= $msg['rt'] . "\r\n"; 204 | if( !is_null( $msg['cc'] ) ) $msg['headers'] .= $msg['cc'] . "\r\n"; 205 | if( !is_null( $msg['bcc'] ) ) $msg['headers'] .= $msg['bcc'] . "\r\n"; 206 | $msg['headers'] .= $msg['version']; 207 | 208 | $result = mail( $msg['to'], $msg['subject'], $msg['message'], $msg['headers'] ); 209 | 210 | return $result; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /Plugins/FlaggedRevs.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | class FlaggedRevs { 21 | 22 | private $wiki; 23 | 24 | /** 25 | * @param Wiki $wikiClass 26 | * @throws DependencyError 27 | */ 28 | function __construct( Wiki &$wikiClass ) { 29 | 30 | if( !array_key_exists( 'FlaggedRevs', $wikiClass->get_extensions() ) ) { 31 | throw new DependencyError( "FlaggedRevs", "http://www.mediawiki.org/wiki/Extension:FlaggedRevs" ); 32 | } 33 | 34 | $this->wiki = $wikiClass; 35 | } 36 | 37 | /** 38 | * @param $revid 39 | * @param null $reason 40 | * @param int $status 41 | * @return bool 42 | * @throws AssertFailure 43 | * @throws BadEntryError 44 | * @throws HookError 45 | * @throws LoggedOut 46 | * @throws MWAPIError 47 | */ 48 | public function review( $revid, $reason = null, $status = 1 ) { 49 | 50 | if( !in_array( 'review', $this->wiki->get_userrights() ) ) { 51 | pecho( "User is not allowed to review revisions", PECHO_FATAL ); 52 | return false; 53 | } 54 | 55 | $tokens = $this->wiki->get_tokens(); 56 | 57 | if( $tokens['edit'] == '+\\' ) { 58 | pecho( "User has logged out.\n\n", PECHO_FATAL ); 59 | return false; 60 | } 61 | 62 | if( mb_strlen( $reason, '8bit' ) > 255 ) { 63 | pecho( "Comment is over 255 bytes, the maximum allowed.\n\n", PECHO_FATAL ); 64 | return false; 65 | } 66 | 67 | pecho( "Reviewing $revid...\n\n", PECHO_NOTICE ); 68 | 69 | $editarray = array( 70 | 'flag_accuracy' => $status, 71 | 'action' => 'review', 72 | 'token' => $tokens['edit'], 73 | 'revid' => $revid 74 | ); 75 | 76 | if( !empty( $reason ) ) $editArray['comment'] = $reason; 77 | 78 | if( $this->wiki->get_maxlag() ) { 79 | $editarray['maxlag'] = $this->wiki->get_maxlag(); 80 | } 81 | 82 | Hooks::runHook( 'StartReview', array( &$editarray ) ); 83 | 84 | $result = $this->wiki->apiQuery( $editarray, true ); 85 | 86 | if( isset( $result['review'] ) ) { 87 | if( isset( $result['review']['revid'] ) ) { 88 | return true; 89 | } else { 90 | pecho( "Review error...\n\n" . print_r( $result['review'], true ) . "\n\n", PECHO_FATAL ); 91 | return false; 92 | } 93 | } else { 94 | pecho( "Review error...\n\n" . print_r( $result, true ), PECHO_FATAL ); 95 | return false; 96 | } 97 | } 98 | 99 | /** 100 | * @param $title 101 | * @param string $level 102 | * @param null $reason 103 | * @param bool|false $autoreview 104 | * @param bool|false $watch 105 | * @return bool 106 | * @throws AssertFailure 107 | * @throws BadEntryError 108 | * @throws HookError 109 | * @throws LoggedOut 110 | * @throws MWAPIError 111 | */ 112 | public function stabilize( $title, $level = 'none', $reason = null, $autoreview = false, $watch = false ) { 113 | 114 | if( !in_array( 'stablesettings', $this->wiki->get_userrights() ) ) { 115 | pecho( "User is not allowed to change the stabilization settings", PECHO_FATAL ); 116 | return false; 117 | } 118 | 119 | $tokens = $this->wiki->get_tokens(); 120 | 121 | if( $tokens['edit'] == '+\\' ) { 122 | pecho( "User has logged out.\n\n", PECHO_FATAL ); 123 | return false; 124 | } 125 | 126 | if( mb_strlen( $reason, '8bit' ) > 255 ) { 127 | pecho( "Comment is over 255 bytes, the maximum allowed.\n\n", PECHO_FATAL ); 128 | return false; 129 | } 130 | 131 | pecho( "Stabilizing title...\n\n", PECHO_NOTICE ); 132 | 133 | $editarray = array( 134 | 'action' => 'review', 135 | 'title' => $title, 136 | 'token' => $tokens['edit'], 137 | 'protectlevel' => $level 138 | ); 139 | 140 | if( $watch ) $editArray['watch'] = 'yes'; 141 | if( !empty( $reason ) ) $editArray['reason'] = $reason; 142 | 143 | if( $this->wiki->get_maxlag() ) { 144 | $editarray['maxlag'] = $this->wiki->get_maxlag(); 145 | } 146 | 147 | Hooks::runHook( 'StartStabilize', array( &$editarray ) ); 148 | 149 | $result = $this->wiki->apiQuery( $editarray, true ); 150 | 151 | if( isset( $result['stabilize'] ) ) { 152 | if( isset( $result['stabilize']['title'] ) ) { 153 | return true; 154 | } else { 155 | pecho( "Stabilization error...\n\n" . print_r( $result['stabilize'], true ) . "\n\n", PECHO_FATAL ); 156 | return false; 157 | } 158 | } else { 159 | pecho( "Stabilization error...\n\n" . print_r( $result, true ), PECHO_FATAL ); 160 | return false; 161 | } 162 | 163 | } 164 | 165 | public function flagconfig() { } 166 | 167 | public function reviewedpages() { } 168 | 169 | public function unreviewedpages() { } 170 | 171 | public function oldreviewedpages() { } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /Plugins/GlobalBlocking.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | class GlobalBlocking { 21 | 22 | public static function load( Wiki &$wikiClass ) { 23 | 24 | if( !array_key_exists( 'GlobalBlocking', $wikiClass->get_extensions() ) ) { 25 | throw new DependencyError( "GlobalBlocking", "http://www.mediawiki.org/wiki/Extension:GlobalBlocking" ); 26 | } 27 | 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /Plugins/GlobalUserInfo.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | class GlobalUserInfo { 21 | 22 | /** 23 | * Wiki class 24 | * 25 | * @var Wiki 26 | * @access private 27 | */ 28 | private $wiki; 29 | 30 | /** 31 | * Username 32 | * 33 | * @var string 34 | * @access private 35 | */ 36 | private $pgUsername; 37 | 38 | /** 39 | * Global groups member is a part of 40 | * 41 | * @var array 42 | * @access private 43 | */ 44 | private $groups = array(); 45 | 46 | /** 47 | * Accounts that user has merged on other wikis 48 | * 49 | * @var array 50 | * @access private 51 | */ 52 | private $merged = array(); 53 | 54 | /** 55 | * Accounts that are not attached to the global account 56 | * 57 | * @var array 58 | * @access private 59 | */ 60 | private $unattached = array(); 61 | 62 | /** 63 | * Whether or not global account exists 64 | * 65 | * @var bool 66 | * @access private 67 | */ 68 | private $exists = true; 69 | 70 | /** 71 | * Date that global account was created 72 | * 73 | * @var string 74 | * @access private 75 | */ 76 | private $registration; 77 | 78 | /** 79 | * Global account ID 80 | * 81 | * @var int 82 | * @access private 83 | */ 84 | private $id; 85 | 86 | /** 87 | * Construction method for the GlobalUserInfo class 88 | * 89 | * @access public 90 | * @param Wiki &$wikiClass The Wiki class object 91 | * @param mixed $pgUsername Username 92 | * @throws APIError 93 | * @throws AssertFailure 94 | * @throws DependencyError 95 | * @throws LoggedOut 96 | * @throws MWAPIError 97 | */ 98 | function __construct( Wiki &$wikiClass, $pgUsername ) { 99 | 100 | if( !array_key_exists( 'Central Auth', $wikiClass->get_extensions() ) ) { 101 | throw new DependencyError( "CentralAuth", "http://www.mediawiki.org/wiki/Extension:CentralAuth" ); 102 | } 103 | 104 | $this->username = ucfirst( $pgUsername ); 105 | $this->wiki = $wikiClass; 106 | 107 | $guiRes = $this->wiki->apiQuery( 108 | array( 109 | 'action' => 'query', 110 | 'meta' => 'globaluserinfo', 111 | 'guiuser' => ucfirst( $pgUsername ), 112 | 'guiprop' => 'groups|merged|unattached', 113 | ), false, false 114 | ); 115 | 116 | if( !isset( $guiRes['query']['globaluserinfo'] ) ) { 117 | $this->exists = false; 118 | if( isset( $guiRes['error'] ) && $guiRes['error']['code'] != 'guinosuchuser' ) { 119 | throw new MWAPIError($guiRes['error']); 120 | } elseif( @$guiRes['error']['code'] != 'guinosuchuser' ) { 121 | throw new MWAPIError(array('code' => 'UnknownError', 'info' => 'Unknown API Error')); 122 | } 123 | } else { 124 | $this->groups = $guiRes['query']['globaluserinfo']['groups']; 125 | $this->merged = $guiRes['query']['globaluserinfo']['merged']; 126 | $this->merged = $guiRes['query']['globaluserinfo']['unattached']; 127 | $this->id = $guiRes['query']['globaluserinfo']['id']; 128 | $this->registration = $guiRes['query']['globaluserinfo']['registration']; 129 | } 130 | } 131 | 132 | /** 133 | * Returns the global account ID 134 | * 135 | * @return int 136 | * @access public 137 | */ 138 | public function get_id() { 139 | return $this->id; 140 | } 141 | 142 | /** 143 | * Returns the date that global account was created 144 | * 145 | * @return string 146 | * @access public 147 | */ 148 | public function get_registration() { 149 | return $this->registration; 150 | } 151 | 152 | /** 153 | * Returns the global groups member is a part of 154 | * 155 | * @return array 156 | * @access public 157 | */ 158 | public function get_groups() { 159 | return $this->groups; 160 | } 161 | 162 | /** 163 | * Returns the accounts that user has merged on other wikis 164 | * 165 | * @return array 166 | * @access public 167 | */ 168 | public function get_merged() { 169 | return $this->merged; 170 | } 171 | 172 | /** 173 | * Returns the accounts that are not attached to the global account 174 | * 175 | * @return array 176 | * @access public 177 | */ 178 | public function get_unattached() { 179 | return $this->unattached; 180 | } 181 | 182 | /** 183 | * Returns whether or not global account exists 184 | * 185 | * @return bool 186 | * @access public 187 | */ 188 | public function get_exists() { 189 | return $this->unattached; 190 | } 191 | 192 | } -------------------------------------------------------------------------------- /Plugins/IRC.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | class IRC { 21 | 22 | /** 23 | * IRC Socket connection 24 | * 25 | * @var object 26 | * @access public 27 | */ 28 | public $f; 29 | 30 | /** 31 | * Channel(s) 32 | * 33 | * @var string|array 34 | * @access private 35 | */ 36 | private $chan; 37 | 38 | /** 39 | * Construct function, front-end for fsockopen. 40 | * @param string $User Username to send to IRC 41 | * @param string $Nick Nick to use 42 | * @param string $Pass Password to send 43 | * @param string $Server Server to connect to 44 | * @param string $pgPort Port to use 45 | * @param string $Gecos AKA Real Name, Information field, etc. 46 | * @param string|array Channel (s) to connect to 47 | */ 48 | function __construct( $User, $Nick, $Pass, $Server, $pgPort, $Gecos, $Channel ) { 49 | $this->f = fsockopen( $Server, $pgPort, $errno, $errstr, 30 ); 50 | 51 | if( !$this->f ) { 52 | die( $errstr . ' (' . $errno . ")\n" ); 53 | } 54 | 55 | pecho( "Logging into IRC as $User into $Server:$pgPort\n\n", PECHO_NOTICE ); 56 | 57 | $this->sendToIrc( 'USER ' . $User . ' "' . $Server . '" "localhost" :' . $Gecos . "\n" ); 58 | $this->sendToIrc( 'PASS ' . $Pass . "\n" ); 59 | $this->sendToIrc( 'NICK ' . $Nick . "\n" ); 60 | 61 | if( !is_array( $Channel ) ) { 62 | $this->chan = array( $Channel ); 63 | } else { 64 | $this->chan = $Channel; 65 | } 66 | $this->joinChan( $Channel ); 67 | } 68 | 69 | /** 70 | * Destruct function, quits from IRC 71 | * @return void 72 | */ 73 | public function quit() { 74 | fwrite( $this->f, 'QUIT ' . "\n" ); 75 | } 76 | 77 | /** 78 | * Sends a raw message to IRC 79 | * @param string $msg Message to send 80 | * @return void 81 | */ 82 | public function sendToIrc( $msg ) { 83 | fwrite( $this->f, $msg ); 84 | } 85 | 86 | /** 87 | * Send a message to a channel, formatted in PRIVMSG format 88 | * @param string $msg Message to send 89 | * @param string $chan Channel to send to 90 | * @return void 91 | */ 92 | public function sendPrivmsg( $msg, $chan ) { 93 | pecho( "Sending $msg to $chan...\n\n", PECHO_VERBOSE ); 94 | fwrite( $this->f, "PRIVMSG " . $chan . " :$msg\n" ); 95 | } 96 | 97 | /** 98 | * Return the pingpong game 99 | * @param string $payload Data from the PING message 100 | * @return void 101 | */ 102 | public function sendPong( $payload ) { 103 | fwrite( $this->f, "PONG " . $payload . "\r\n" ); 104 | } 105 | 106 | /** 107 | * Joins a channel, or the locally stored channel(s) 108 | * @param string $chan Channel to join. Default null. 109 | * @return void 110 | */ 111 | public function joinChan( $chan = null ) { 112 | if( !is_null( $chan ) && !is_array( $chan ) ) { 113 | pecho( "Joining $chan...\n", PECHO_VERBOSE ); 114 | fwrite( $this->f, 'JOIN ' . $chan . "\n" ); 115 | usleep( 5000 ); 116 | } elseif( !is_null( $chan ) ) { 117 | foreach( $chan as $channel ){ 118 | pecho( "Joining $channel...\n", PECHO_VERBOSE ); 119 | fwrite( $this->f, 'JOIN ' . $channel . "\n" ); 120 | usleep( 5000 ); 121 | } 122 | } 123 | } 124 | 125 | /** 126 | * Leaves a channel, or the locally stored channel(s) 127 | * @param string $chan Channel to part. Default null 128 | * @return void 129 | */ 130 | public function partChan( $chan = null ) { 131 | if( !is_null( $chan ) ) { 132 | pecho( "Parting $chan...\n", PECHO_VERBOSE ); 133 | fwrite( $this->f, 'PART ' . $chan . "\n" ); 134 | usleep( 5000 ); 135 | } else { 136 | foreach( $this->chan as $chan ){ 137 | pecho( "Parting $chan...\n", PECHO_VERBOSE ); 138 | fwrite( $this->f, 'PART ' . $chan . "\n" ); 139 | usleep( 5000 ); 140 | } 141 | } 142 | } 143 | 144 | /** 145 | * Splits apart the various parts of an IRC line into usable sections, e.g. !commands, cloaks, etc. 146 | * @param string $line Line that IRC sent 147 | * @param array $trigger Trigger character for !commands (e.g. !, ., @, etc) 148 | * @param bool $feed Whether or not the IRC server is a MediaWiki RC channel 149 | * @return array Parsed line 150 | * @static 151 | */ 152 | public static function parseLine( $line, $trigger, $feed = false ) { 153 | $return = array(); 154 | $return['trueraw'] = $line; 155 | $return['truerawmsg'] = explode( " ", $line ); 156 | unset( $return['truerawmsg'][0], $return['truerawmsg'][1], $return['truerawmsg'][2] ); 157 | $return['truerawmsg'] = substr( implode( ' ', $return['truerawmsg'] ), 1 ); 158 | 159 | if( $feed ) { 160 | $line = str_replace( array( "\n", "\r", "\002" ), '', $line ); 161 | $line = preg_replace( '/\003(\d\d?(,\d\d?)?)?/', '', $line ); 162 | } else { 163 | $line = str_replace( array( "\n", "\r" ), '', $line ); 164 | $line = preg_replace( '/' . chr( 3 ) . '.{2,}/i', '', $line ); 165 | } 166 | 167 | $return['raw'] = $line; 168 | 169 | /* 170 | Data for a privmsg: 171 | $d[0] = Nick!User@Host format. 172 | $d[1] = Action, e.g. "PRIVMSG", "MODE", etc. If it's a message from the server, it's the numerial code 173 | $d[2] = The channel somethign was spoken in 174 | $d[3] = The text that was spoken 175 | */ 176 | $d = $return['message'] = explode( ' ', $line ); 177 | $return['n!u@h'] = $d[0]; 178 | 179 | unset( $return['message'][0], $return['message'][1], $return['message'][2] ); 180 | $return['message'] = substr( implode( ' ', $return['message'] ), 1 ); 181 | 182 | $return['nick'] = substr( $d[0], 1 ); 183 | $return['nick'] = explode( '!', $return['nick'] ); 184 | $return['nick'] = $return['nick'][0]; 185 | 186 | $return['cloak'] = explode( '@', $d[0] ); 187 | $return['cloak'] = @$return['cloak'][1]; 188 | 189 | $return['user'] = explode( '!', $d[0] ); 190 | $return['user'] = explode( '@', $return['user'][1] ); 191 | $return['user'] = $return['user'][0]; 192 | 193 | $return['chan'] = strtolower( $d[2] ); 194 | 195 | $return['type'] = $return['payload'] = $d[1]; 196 | 197 | if( in_array( substr( $return['message'], 0, 1 ), $trigger ) ) { 198 | $return['command'] = explode( ' ', substr( strtolower( $return['message'] ), 1 ) ); 199 | $return['command'] = $return['command'][0]; 200 | 201 | //Get the parameters 202 | $return['param'] = explode( ' ', $return['message'] ); 203 | unset( $return['param'][0] ); 204 | $return['param'] = implode( ' ', $return['param'] ); 205 | $return['param'] = trim( $return['param'] ); 206 | } 207 | 208 | /* 209 | End result: 210 | $return['raw'] = Raw data 211 | $return['message'] = The text that appears in the channel 212 | $return['n!u@h'] = The person who said the line, in N!U@H format 213 | $return['nick'] = The nick who said the line 214 | $return['cloak'] = The cloak of the person who said the line 215 | $return['user'] = The username who said the line 216 | $return['chan'] = The channel the line was said in 217 | $return['type'] = The action that was done (eg PRIVMSG, MODE) 218 | $return['payload'] = For pings, this is $d[1] 219 | $return['command'] = The command that was said, eg !status (excuding !) 220 | $return['param'] = Parameters of the command 221 | */ 222 | return $return; 223 | } 224 | 225 | /** 226 | * Parses the title, user, etc from a MediaWiki RC feed 227 | * @link http://www.mediawiki.org/wiki/Manual:IRC_RC_Bot 228 | * @param string $msg Message from feed 229 | * @return array Parsed line 230 | * @static 231 | */ 232 | public static function parseRC( $msg ) { 233 | if( preg_match( '/^\[\[((Talk|User|Wikipedia|Image|MediaWiki|Template|Help|Category|Portal|Special)(( |_)talk)?:)?([^\x5d]*)\]\] (\S*) (http:\/\/en\.wikipedia\.org\/w\/index\.php\?(oldid|diff)=(\d*)&(rcid|oldid)=(\d*).*|http:\/\/en\.wikipedia\.org\/wiki\/\S+)? \* ([^*]*) \* (\(([^)]*)\))? (.*)$/S', $msg, $m ) ) { 234 | 235 | $return = array(); 236 | 237 | //print_r($m); 238 | 239 | $return['namespace'] = $m[2]; 240 | $return['pagename'] = $m[5]; 241 | $return['fullpagename'] = $m[1] . $m[5]; 242 | $return['basepagename'] = explode( '/', $return['fullpagename'] ); 243 | $return['basepagename'] = $return['basepagename'][0]; 244 | $return['subpagename'] = str_replace( $return['basepagename'] . '/', '', $return['fullpagename'] ); 245 | $return['flags'] = str_split( $m[6] ); 246 | $return['action'] = $m[6]; 247 | $return['url'] = $m[7]; 248 | $return['revid'] = $m[9]; 249 | $return['oldid'] = $m[11]; 250 | $return['username'] = $m[12]; 251 | $return['len'] = $m[14]; 252 | $return['comment'] = $m[15]; 253 | $return['timestamp'] = time(); 254 | $return['is_new'] = false; 255 | $return['is_minor'] = false; 256 | $return['is_bot'] = false; 257 | $return['is_delete'] = false; 258 | $return['actionpage'] = null; 259 | 260 | if( in_array( 'N', $return['flags'] ) ) { 261 | $return['is_new'] = true; 262 | } 263 | 264 | if( in_array( 'M', $return['flags'] ) ) { 265 | $return['is_minor'] = true; 266 | } 267 | 268 | if( in_array( 'B', $return['flags'] ) ) { 269 | $return['is_bot'] = true; 270 | } 271 | 272 | if( $return['action'] == 'delete' ) { 273 | $return['is_delete'] = true; 274 | $tmp = explode( '[[', $return['comment'] ); 275 | $tmp = explode( ']]', $tmp[1] ); 276 | $return['actionpage'] = $tmp[0]; 277 | $return['actionpageprefix'] = explode( '/', $return['actionpage'] ); 278 | $return['actionpageprefix'] = $return['actionpageprefix'][0]; 279 | } 280 | 281 | return $return; 282 | } 283 | } 284 | 285 | /** 286 | * @param $errno 287 | * @return bool|string 288 | */ 289 | public static function get_error($errno) 290 | { 291 | if ($errno != null) { 292 | switch ($errno) { 293 | case 401: 294 | return "Nickname/Channel is currently unused"; 295 | case 402: 296 | return "Server not found"; 297 | case 403: 298 | return "Channel not found"; 299 | case 404: 300 | return "Cannot send to channel"; 301 | case 405: 302 | return "Too many channels joined"; 303 | case 406: 304 | return "There was no such nickname"; 305 | } 306 | } else { 307 | return false; 308 | } 309 | } 310 | } 311 | 312 | class SimpleIRC { 313 | 314 | private $server; 315 | private $pgPort; 316 | private $user; 317 | private $pass; 318 | private $nick; 319 | private $channel; 320 | private $callback; 321 | 322 | function __construct( $server, $pgPort = 6667, $user, $pass, $nick, $channel, $callback = null ) { 323 | global $pgIRCTrigger, $pgHooks; 324 | 325 | if( func_num_args() > 6 ) { 326 | $this->server = $server; 327 | $this->port = $pgPort; 328 | $this->user = $user; 329 | $this->pass = $pass; 330 | $this->nick = $nick; 331 | $this->channel = $channel; 332 | $this->callback = $callback; 333 | } else { 334 | $this->server = $server; 335 | $this->port = 6667; 336 | $this->user = $pgPort; 337 | $this->pass = $user; 338 | $this->nick = $pass; 339 | $this->channel = $nick; 340 | $this->callback = $channel; 341 | } 342 | 343 | $pgHooks['SimpleIRCPrivMSG'][] = $callback; 344 | 345 | $irc = new IRC( $this->user, $this->nick, $this->pass, $this->server, $this->port, "Peachy IRC Bot Version " . PEACHYVERSION, $this->channel ); 346 | 347 | while( !feof( $irc->f ) ){ 348 | 349 | $parsed = IRC::parseLine( fgets( $irc->f, 1024 ), $pgIRCTrigger, true ); 350 | 351 | if( @$parsed['n!u@h'] == 'PING' ) { 352 | $irc->sendPong( $parsed['payload'] ); 353 | } 354 | 355 | if( @$parsed['type'] == '376' || @$parser['type'] == '422' ) { 356 | $feed->joinChan(); 357 | sleep( 5 ); 358 | } 359 | 360 | if( @$parsed['type'] == 'PRIVMSG' ) { 361 | Hooks::runHook( 'SimpleIRCPrivMSG', array( &$parsed, &$irc, &$this ) ); 362 | } 363 | } 364 | } 365 | 366 | } 367 | -------------------------------------------------------------------------------- /Plugins/RFA.php: -------------------------------------------------------------------------------- 1 | initPage( $page )->get_text(); 42 | 43 | $split = preg_split( 44 | "/^(?:(?:'''|(?:clude><\/includeonly>)?={4,5}(?:<\/noin<\/includeonly>clude><\/includeonly>''')?)" 45 | . "\s*?(Support|Oppose|Neutral|Comments)\s*?(?:'''|(?:'''clude><\/includeonly>)?={4,5}(?:<\/noin<\/includeonly>clude><\/includeonly>)?)|;\s*(Support|Oppose|Neutral|Comments))\s*(?:
|
)?\s*$/im" 46 | , $rawwikitext, -1, PREG_SPLIT_DELIM_CAPTURE 47 | ); 48 | 49 | $header = array_shift( $split ); 50 | 51 | //=== Deal with the header ===// 52 | $header = str_ireplace( array( '', '' ), '', $header ); 53 | 54 | if( preg_match( "/===\s*\[\[User:(.*?)\|.*?\]\]\s*===/", $header, $matches ) ) { 55 | $this->username = $matches[1]; 56 | } elseif( preg_match( "/===\s*\[\[.*?\|(.*?)\]\]\s*===/", $header, $matches ) ) { 57 | $this->username = $matches[1]; 58 | } 59 | 60 | $header = str_replace( array( '[[', ']]' ), '', $header ); 61 | 62 | if( preg_match( "/end(?:ing|ed)?(?: no earlier than)? (.*?) \(UTC\)/i", $header, $matches ) ) { 63 | $this->enddate = $matches[1]; 64 | } 65 | //=== End header stuff ===// 66 | 67 | //Now parse through each non-header section, figuring out what they are 68 | //Nothing expected = 0, Support = 1, Oppose = 2, Neutral = 3 69 | $nextsection = 0; 70 | 71 | foreach( $split as $splut ){ 72 | $splut = trim( $splut ); 73 | if( empty( $splut ) ) { 74 | continue; 75 | } 76 | 77 | if( strcasecmp( $splut, 'Support' ) == 0 ) { 78 | $nextsection = 1; 79 | } elseif( strcasecmp( $splut, 'Oppose' ) == 0 ) { 80 | $nextsection = 2; 81 | } elseif( strcasecmp( $splut, 'Neutral' ) == 0 ) { 82 | $nextsection = 3; 83 | } else { 84 | switch( $nextsection ){ 85 | case 1: 86 | $support = $splut; 87 | break; 88 | case 2: 89 | $oppose = $splut; 90 | break; 91 | case 3: 92 | $neutral = $splut; 93 | break; 94 | } 95 | $nextsection = 0; 96 | } 97 | } 98 | 99 | if( !isset( $support ) ) { 100 | $this->lasterror = "Support section not found"; 101 | return false; 102 | } 103 | if( !isset( $oppose ) ) { 104 | $this->lasterror = "Oppose section not found"; 105 | return false; 106 | } 107 | if( !isset( $neutral ) ) { 108 | $this->lasterror = "Neutral section not found"; 109 | return false; 110 | } 111 | 112 | $this->support = $this->analyzeSection( $support ); 113 | $this->oppose = $this->analyzeSection( $oppose ); 114 | $this->neutral = $this->analyzeSection( $neutral ); 115 | 116 | //Merge all votes in one array and sort: 117 | $m = array(); 118 | foreach( $this->support as $s ){ 119 | if( isset( $s['name'] ) ) $m[] = $s['name']; 120 | } 121 | foreach( $this->oppose as $o ){ 122 | if( isset( $o['name'] ) ) $m[] = $o['name']; 123 | } 124 | foreach( $this->neutral as $n ){ 125 | if( isset( $n['name'] ) ) $m[] = $n['name']; 126 | } 127 | sort( $m ); 128 | //Find duplicates: 129 | for( $i = 0; $i < count( $m ); $i++ ){ 130 | if( $i != count( $m ) - 1 ) { 131 | if( $m[$i] == $m[$i + 1] ) { 132 | $this->duplicates[] = $m[$i]; 133 | } 134 | } 135 | } 136 | 137 | return true; 138 | } 139 | 140 | public function get_username() { 141 | return $this->username; 142 | } 143 | 144 | public function get_enddate() { 145 | return $this->enddate; 146 | } 147 | 148 | public function get_support() { 149 | return $this->support; 150 | } 151 | 152 | public function get_oppose() { 153 | return $this->oppose; 154 | } 155 | 156 | public function get_neutral() { 157 | return $this->neutral; 158 | } 159 | 160 | public function get_duplicates() { 161 | return $this->duplicates; 162 | } 163 | 164 | public function get_lasterror() { 165 | return $this->lasterror; 166 | } 167 | 168 | /** 169 | * Attempts to find a signature in $input using the default regex. Returns matches. 170 | * @param $input 171 | * @param $matches 172 | * 173 | * @return int 174 | */ 175 | protected function findSig( $input, &$matches ) { 176 | //Supports User: and User talk: wikilinks, {{fullurl}}, unsubsted {{unsigned}}, unsubsted {{unsigned2}}, anything that looks like a custom sig template 177 | return preg_match_all( 178 | "/\[\[[Uu]ser(?:[\s_][Tt]alk)?\:([^\]\|\/]*)(?:\|[^\]]*)?\]\]" //1: Normal [[User:XX]] and [[User talk:XX]] 179 | . "|\{\{(?:[Ff]ullurl\:[Uu]ser(?:[\s_][Tt]alk)?\:|[Uu]nsigned\|)([^\}\|]*)(?:|[\|\}]*)?\}\}" //2: {{fullurl}} and {{unsigned}} templates 180 | . "|(?:\{\{)[Uu]ser(?:[\s_][Tt]alk)?\:([^\}\/\|]*)" //3: {{User:XX/sig}} templates 181 | . "|\{\{[Uu]nsigned2\|[^\|]*\|([^\}]*)\}\}" //4: {{unsigned2|Date|XX}} templates 182 | . "|(?:\[\[)[Uu]ser\:([^\]\/\|]*)\/[Ss]ig[\|\]]/" //5: [[User:XX/sig]] links (compromise measure) 183 | , $input, $matches, PREG_OFFSET_CAPTURE 184 | ); 185 | } 186 | 187 | /** 188 | * Attempts to find a signature in $input using a different regex. Returns matches. 189 | * @param $input 190 | * @param $matches 191 | * 192 | * @return int 193 | */ 194 | protected function findSigAlt( $input, &$matches ) { 195 | return preg_match_all( 196 | "/\[\[[Uu]ser(?:[\s_][Tt]alk)?\:([^\]\/\|]*)" //5: "[[User:XX/PageAboutMe" links (notice no end tag) 197 | . "|\[\[[Ss]pecial\:[Cc]ontributions\/([^\|\]]*)/" 198 | , $input, $matches, PREG_OFFSET_CAPTURE 199 | ); 200 | } 201 | 202 | /** 203 | * Attempts to find a signature in $input. Returns the name of the user, false on failure. 204 | * @param $input 205 | * @param $iffy 206 | * 207 | * @return bool|string false if not found Signature, or the Signature if it is found 208 | */ 209 | protected function findSigInLine( $input, &$iffy ) { 210 | $iffy = 0; 211 | 212 | $parsee_array = explode( "\n", $input ); 213 | for( $n = 0; $n < count( $parsee_array ); $n++ ){ //This for will terminate when a sig is found. 214 | $parsee = $parsee_array[$n]; 215 | //Okay, let's try and remove "copied from above" messages. If the line has more than one timestamp, we'll disregard anything after the first. 216 | //Note: we're ignoring people who use custom timestamps - if these peoples' votes are moved, the mover's name will show up as having voted. 217 | 218 | //If more than one timestamp is found in the first portion of the vote: 219 | $tsmatches = array(); 220 | $dummymatches = array(); 221 | if( preg_match_all( '/' . "[0-2][0-9]\:[0-5][0-9], [1-3]?[0-9] (?:January|February|March|April|May|June|July|August|September|October|November|December) \d{4} \(UTC\)" . '/', $parsee, $tsmatches, PREG_OFFSET_CAPTURE ) > 1 ) { 222 | //Go through each timestamp-section, looking for a signature 223 | foreach( $tsmatches[0] as $minisection ){ 224 | $temp = substr( $parsee, 0, $minisection[1] ); 225 | //If a signature is found, stop and use it as voter 226 | if( $this->findSig( $temp, $dummymatches ) != 0 ) { //KNOWN ISSUE: Write description later 227 | $parsee = $temp; 228 | break; 229 | } 230 | } 231 | } 232 | 233 | //Start the main signature-finding: 234 | $matches = array(); 235 | if( $this->findSig( $parsee, $matches ) == 0 ) { 236 | //Okay, signature not found. Let's try the backup regex 237 | if( $this->findSigAlt( $parsee, $matches ) == 0 ) { 238 | //Signature was not found in this iteration of the main loop :( 239 | continue; //Go on to next newline (may be iffy) 240 | } else { 241 | $merged = array_merge( $matches[1], $matches[2] ); 242 | } 243 | } else { 244 | //Merge the match arrays: 245 | $merged = array_merge( $matches[5], $matches[1], $matches[3], $matches[2], $matches[4] ); 246 | } 247 | //Remove blank values and arrays of the form ('',-1): 248 | foreach( $merged as $key => $value ){ 249 | if( is_array( $value ) && ( $value[0] == '' ) && ( $value[1] == -1 ) ) { 250 | unset( $merged[$key] ); 251 | } elseif( $value == "" ) { 252 | unset( $merged[$key] ); 253 | } 254 | } 255 | 256 | //Let's find out the real signature 257 | $keys = array(); 258 | $values = array(); 259 | foreach( $merged as $mergee ){ 260 | $keys[] = $mergee[0]; 261 | $values[] = $mergee[1]; 262 | } 263 | //Now sort: 264 | array_multisort( $values, SORT_DESC, SORT_NUMERIC, $keys ); 265 | //Now we should have the most relevant match (i.e., the sig) at the top of $keys 266 | $i = 0; 267 | $foundsig = ''; 268 | while( $foundsig == '' ){ 269 | $foundsig = trim( $keys[$i++] ); 270 | if( $i == count( $keys ) ) break; //If we can only find blank usernames in the sig, catch overflow 271 | //Also fires when the first sig is also the last sig, so not an error 272 | } 273 | 274 | //Set iffy flag (level 1) if went beyond first line 275 | if( $n > 0 ) { 276 | $iffy = 1; 277 | } 278 | return $foundsig; 279 | } 280 | 281 | return false; 282 | } 283 | 284 | /** 285 | * Analyzes an RFA section. Returns an array of parsed signatures on success. Undefined behaviour on failure. 286 | * @param string $input 287 | * 288 | * @return array 289 | */ 290 | private function analyzeSection( $input ) { 291 | //Remove trailing sharp, if any 292 | $input = preg_replace( '/#\s*$/', '', $input ); 293 | 294 | //Old preg_split regex: "/(^|\n)\s*\#[^\#\:\*]/" 295 | $parsed = preg_split( "/(^|\n)\#/", $input ); 296 | //Shift off first empty element: 297 | array_shift( $parsed ); 298 | 299 | foreach( $parsed as &$parsee ){ //Foreach line 300 | //If the line is empty for some reason, ignore 301 | $parsee = trim( $parsee ); 302 | if( empty( $parsee ) ) continue; 303 | 304 | //If the line has been indented (disabled), or is a comment, ignore 305 | if( ( $parsee[0] == ':' ) || ( $parsee[0] == '*' ) || ( $parsee[0] == '#' ) ) { 306 | $parsee = '///###///'; 307 | continue; 308 | }; //struck-out vote or comment 309 | 310 | $parsedsig = $this->findSigInLine( $parsee, $iffy ); //Find signature 311 | $orgsig = $parsee; 312 | $parsee = array(); 313 | $parsee['context'] = $orgsig; 314 | if( $parsedsig === false ) { 315 | $parsee['error'] = 'Signature not found'; 316 | } else { 317 | $parsee['name'] = $parsedsig; 318 | } 319 | if( @$iffy == 1 ) { 320 | $parsee['iffy'] = '1'; 321 | } 322 | } //Foreach line 323 | 324 | if( ( count( $parsed ) == 1 ) && ( @trim( $parsed[0]['name'] ) == '' ) ) { //filters out placeholder sharp sign used in empty sections 325 | $parsed = array(); 326 | } 327 | 328 | //Delete struck-out keys "continued" in foreach 329 | foreach( $parsed as $key => $value ){ 330 | if( $value == '///###///' ) { 331 | unset( $parsed[$key] ); 332 | } 333 | } 334 | 335 | return $parsed; 336 | } 337 | 338 | } 339 | -------------------------------------------------------------------------------- /Plugins/RPED.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | class RPED { 21 | 22 | /** 23 | * Wiki class 24 | * 25 | * @var Wiki 26 | * @access private 27 | */ 28 | private $wiki; 29 | 30 | /** 31 | * maxURLLength 32 | * Default maximum length of the URL to be posted 33 | * 34 | * @var int 35 | * @access private 36 | */ 37 | private $defaultMaxURLLength; 38 | 39 | /** 40 | * Construction method for the RPED class 41 | * 42 | * @access public 43 | * @param Wiki &$wikiClass The Wiki class object 44 | */ 45 | function __construct( Wiki &$wikiClass ) { 46 | $this->wiki = $wikiClass; 47 | $defaultMaxURLLength = 2000; 48 | return; 49 | } 50 | 51 | /** 52 | * Insert a page title into the rped_page table 53 | * 54 | * @static 55 | * @access public 56 | * @param string $page 57 | * @return void 58 | */ 59 | public function insert( $page ) { 60 | $this->wiki->apiQuery( 61 | array( 62 | 'action' => 'rped', 63 | 'insert' => $page, 64 | ), true 65 | ); 66 | } 67 | 68 | /** 69 | * Delete a page title from the rped_page table 70 | * 71 | * @static 72 | * @access public 73 | * @param string $page 74 | * @return void 75 | */ 76 | public function delete( $page ) { 77 | $this->wiki->apiQuery( 78 | array( 79 | 'action' => 'rped', 80 | 'delete' => $page, 81 | ), true 82 | ); 83 | } 84 | 85 | /** 86 | * Insert/delete an array of page titles into/from the rped_page table 87 | * 88 | * @static 89 | * @access public 90 | * @param string $command Either 'insert' or 'delete' 91 | * @param array $pageArray The array of page title to insert 92 | * @param int $maxURLLength The maximum length of the url to be POSTed 93 | * @return void 94 | */ 95 | public function insertOrDeleteArray( $command, $pageArray, $maxURLLength = 0 ) { 96 | if( $command != 'insert' && $command != 'delete' ) { 97 | die( 'Something tried to call insertOrDeleteArray without' 98 | . 'specifying an insert or delete command.' ); 99 | } 100 | if( $maxURLLength == 0 ) { 101 | $maxURLLength = $this->defaultMaxURLLength; 102 | } 103 | $line = ''; 104 | foreach( $pageArray as $page ){ 105 | if( $line != '' ) { 106 | $line .= '|'; 107 | } 108 | if( strlen( $line ) + strlen( $page ) > $maxURLLength ) { 109 | if( $command == 'delete' ) { 110 | $this->delete( $line ); 111 | } else { 112 | $this->insert( $line ); 113 | } 114 | $line = ''; 115 | } 116 | $line .= $page; 117 | } 118 | if( $command == 'delete' ) { 119 | $this->delete( $line ); 120 | } else { 121 | $this->insert( $line ); 122 | } 123 | } 124 | 125 | /** 126 | * Insert an array of page titles into/from the rped_page table 127 | * 128 | * @static 129 | * @access public 130 | * @param array $pageArray The array of page title to insert 131 | * @param int $maxURLLength The maximum length of the url to be POSTed 132 | * @return void 133 | */ 134 | public function insertArray( $pageArray, $maxURLLength = 0 ) { 135 | $this->insertOrDeleteArray( 'insert', $pageArray, $maxURLLength ); 136 | } 137 | 138 | /** 139 | * Delete an array of page titles from the rped_page table 140 | * 141 | * @static 142 | * @access public 143 | * @param array $pageArray The array of page title to insert 144 | * @param int $maxURLLength The maximum length of the url to be POSTed 145 | * @return void 146 | */ 147 | public function deleteArray( $pageArray, $maxURLLength = 0 ) { 148 | $this->insertOrDeleteArray( 'delete', $pageArray, $maxURLLength ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Plugins/SiteMatrix.php: -------------------------------------------------------------------------------- 1 | . 18 | */ 19 | 20 | class SiteMatrix { 21 | 22 | /** 23 | * Loads list of all SiteMatrix wikis 24 | * 25 | * @static 26 | * @access public 27 | * @param Wiki &$wikiClass The Wiki class object 28 | * @return array List of all wikis 29 | * @throws AssertFailure 30 | * @throws DependencyError 31 | * @throws LoggedOut 32 | * @throws MWAPIError 33 | */ 34 | public static function load( Wiki &$wikiClass ) { 35 | 36 | if( !array_key_exists( 'SiteMatrix', $wikiClass->get_extensions() ) ) { 37 | throw new DependencyError( "SiteMatrix" ); 38 | } 39 | 40 | $SMres = $wikiClass->apiQuery( 41 | array( 42 | 'action' => 'sitematrix', 43 | ) 44 | ); 45 | 46 | $wikis = $SMres['sitematrix']; 47 | //return $wikis; 48 | 49 | $retarray = array( 50 | 'raw' => $wikis, 51 | 'urls' => array(), 52 | 'langs' => array(), 53 | 'names' => array(), 54 | 'privates' => array() 55 | ); 56 | 57 | foreach( $wikis as $site ){ 58 | if( is_array( $site ) ) { 59 | if( isset( $site['site'] ) ) { 60 | 61 | $retarray['langs'][] = $site['code']; 62 | $retarray['names'][$site['code']] = $site['name']; 63 | 64 | foreach( $site['site'] as $site2 ){ 65 | $retarray['urls'][] = $site2['url']; 66 | 67 | if( isset( $site2['private'] ) ) $retarray['privates'][] = $site2; 68 | } 69 | } else { 70 | foreach( $site as $site2 ){ 71 | $sites2['urls'][] = $site2['url']; 72 | 73 | if( isset( $site2['private'] ) ) $retarray['privates'][] = $site2; 74 | } 75 | } 76 | } 77 | } 78 | 79 | return $retarray; 80 | 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Peachy MediaWiki Framework 2 | ======= 3 | 4 | This is a framework for Mediawiki sites 5 | 6 | If you have feature requests please file a bug. 7 | 8 | ###### Badges 9 | 10 | * Status: [![Build Status](https://travis-ci.org/MW-Peachy/Peachy.svg?branch=master)](https://travis-ci.org/MW-Peachy/Peachy) 11 | * Coverage: [![Code Coverage](https://scrutinizer-ci.com/g/MW-Peachy/Peachy/badges/coverage.png?s=c4f43d2284ed1fe068b692b9d7778f940912f2ee)](https://scrutinizer-ci.com/g/MW-Peachy/Peachy/) 12 | * Quality: [![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/MW-Peachy/Peachy/badges/quality-score.png?s=16084244d02ed6ee537ab1dbd1c7bc4310a30049)](https://scrutinizer-ci.com/g/MW-Peachy/Peachy/) 13 | 14 | ###### Addwiki (alternative) 15 | 16 | For a more modern set of libraries please consider taking a look at the addwiki PHP libraries. 17 | 18 | * GitHub: https://github.com/addwiki 19 | * Docs: https://addwiki.github.io/ 20 | * Packagist: https://packagist.org/packages/addwiki/ 21 | -------------------------------------------------------------------------------- /Tests/GenFunctionsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( $expected, iin_array( $needle, $haystack ) ); 37 | } 38 | 39 | /** 40 | * Data Provider for test_strtoupper_safe() 41 | * 42 | * @return array 43 | */ 44 | public function provide_strtoupper_safe() { 45 | return array( 46 | array( 'foo', 'FOO' ), 47 | array( 'FOO', 'FOO' ), 48 | array( array( 'foo' ), array( 'FOO' ) ), 49 | array( array( array( 'foo' ) ), array( array( 'FOO' ) ) ), 50 | array( array( array( 'foo' ), array( 'boo' ), 'F' ), array( array( 'FOO' ), array( 'BOO' ), 'F' ) ), 51 | ); 52 | } 53 | 54 | /** 55 | * @dataProvider provide_strtoupper_safe 56 | * @covers ::strtoupper_safe 57 | * @param $input 58 | * @param $expected 59 | */ 60 | public function test_strtoupper_safe( $input, $expected ) { 61 | $this->assertSame( $expected, strtoupper_safe( $input ) ); 62 | } 63 | 64 | /** 65 | * Data Provider for test_in_string() 66 | * 67 | * @return array 68 | */ 69 | public function provide_in_string() { 70 | return array( 71 | array( 'B', 'aBc', false, true ), 72 | array( 'b', 'aBc', false, false ), 73 | array( 'b', 'aBc', true, true ), 74 | ); 75 | } 76 | 77 | /** 78 | * @dataProvider provide_in_string 79 | * @covers ::in_string 80 | * @param $needle 81 | * @param $haystack 82 | * @param $insensitive 83 | * @param $expected 84 | */ 85 | public function test_in_string( $needle, $haystack, $insensitive, $expected ) { 86 | $this->assertEquals( $expected, in_string( $needle, $haystack, $insensitive ) ); 87 | } 88 | 89 | /** 90 | * Data Provider for test_in_array_recursive() 91 | * 92 | * @return array 93 | */ 94 | public function provide_in_array_recursive() { 95 | return array( 96 | array( 'BOO', array( 'boo' ), true, true ), 97 | array( 'BOO', array( 'BOO' ), true, true ), 98 | array( 'boo', array( 'BOO' ), true, true ), 99 | array( 'boo', array( 'BOO', 'boo' ), true, true ), 100 | array( 'boo', array(), true, false ), 101 | array( '', array( 'boo' ), true, false ), 102 | array( 'boo', array( array( array( 'boo' ) ) ), false, true ), 103 | array( 'boo', array( array( array( 'foo' => 'boo' ) ) ), false, true ), 104 | array( 'boo', array( array( array( 'foo' => 'BOO' ) ) ), true, true ), 105 | array( 'boo', array( array( array( 'foo' => 'moo' ) ) ), false, false ), 106 | ); 107 | } 108 | 109 | /** 110 | * @dataProvider provide_in_array_recursive 111 | * @covers ::in_array_recursive 112 | * @param $needle 113 | * @param $haystack 114 | * @param $insensitive 115 | * @param $expected 116 | */ 117 | public function test_in_array_recursive( $needle, $haystack, $insensitive, $expected ) { 118 | $this->assertEquals( $expected, in_array_recursive( $needle, $haystack, $insensitive ) ); 119 | } 120 | 121 | /** 122 | * Data Provider for test_checkExclusion() 123 | * 124 | * @return array 125 | */ 126 | public function provide_checkExclusion() { 127 | return array( 128 | array( false, '' ), 129 | array( false, '{{bots}}' ), 130 | array( true, '{{nobots}}' ), 131 | array( false, '{{nobots|allow=addbot}}', 'addbot' ), 132 | array( true, '{{nobots|deny=addbot}}', 'addbot' ), 133 | ); 134 | } 135 | 136 | /** 137 | * @dataProvider provide_checkExclusion 138 | * @covers ::checkExclusion 139 | * @param $expected 140 | * @param $text 141 | * @param null $pgUsername 142 | * @param null $optout 143 | */ 144 | public function test_checkExclusion( $expected, $text, $pgUsername = null, $optout = null ) { 145 | $this->assertSame( $expected, checkExclusion( $this->getMockWiki(), $text, $pgUsername, $optout ) ); 146 | } 147 | 148 | /** 149 | * @return \Wiki 150 | */ 151 | private function getMockWiki() { 152 | $mock = $this->getMockBuilder( 'Wiki' ) 153 | ->disableOriginalConstructor() 154 | ->getMock(); 155 | $mock->expects( $this->once() ) 156 | ->method( 'get_nobots' ) 157 | ->will( $this->returnValue( true ) ); 158 | return $mock; 159 | } 160 | 161 | } -------------------------------------------------------------------------------- /Tests/Includes/AutoUpdateTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder( 'HTTP' ) 59 | ->disableOriginalConstructor() 60 | ->getMock(); 61 | $mock->expects( $this->any() ) 62 | ->method( 'get' ) 63 | ->will( $this->returnValue( json_encode( $data ) ) ); 64 | return $mock; 65 | } 66 | 67 | /** 68 | * @return array 69 | */ 70 | public function provideCheckforupdate() 71 | { 72 | return array( 73 | array( 74 | true, 75 | array( 'message' => 'API rate limit exceeded' ), 76 | '/Cant check for updates right now, next window in/' 77 | ), 78 | array( 79 | false, 80 | array( 'commit' => array( 'sha' => 'testshahash' ) ), 81 | '/Peachy Updated! Changes will go into effect on the next run./', 82 | ), 83 | array( 84 | true, 85 | array( 'commit' => array( 'sha' => 'testshahash' ) ), 86 | '/Peachy is up to date/', 87 | serialize( array( 'commit' => array( 'sha' => 'testshahash' ) ) ) 88 | ), 89 | array( 90 | false, 91 | array( 'commit' => array( 'sha' => 'testshahash' ) ), 92 | '/Peachy Updated! Changes will go into effect on the next run./', 93 | serialize( array( 'commit' => array( 'sha' => 'differenthash!' ) ) ) 94 | ), 95 | ); 96 | } 97 | 98 | /** 99 | * @dataProvider provideCheckforupdate 100 | * @covers AutoUpdate::Checkforupdate 101 | * @covers AutoUpdate::cacheLastGithubETag 102 | * @covers AutoUpdate::getTimeToNextLimitWindow 103 | * @covers AutoUpdate::__construct 104 | * @covers AutoUpdate::updatePeachy 105 | * @covers AutoUpdate::getLocalPath 106 | * @covers AutoUpdate::copyOverGitFiles 107 | * @covers AutoUpdate::rrmdir 108 | * @param $expected 109 | * @param $data 110 | * @param string $outputRegex 111 | * @param null $updatelog 112 | * @param bool $experimental 113 | * @param bool $wasexperimental 114 | */ 115 | public function testCheckforupdate( $expected, $data, $outputRegex = '/.*?/', $updatelog = null, $experimental = false, $wasexperimental = false ) { 116 | $updater = $this->getUpdater( $this->getMockHttp( $data ) ); 117 | if( $updatelog === null ) { 118 | if( file_exists( __DIR__ . '/../../Includes/' . ( $experimental ? 'Update.log' : 'StableUpdate.log' ) ) ) { 119 | unlink( __DIR__ . '/../../Includes/' . ( $experimental ? 'Update.log' : 'StableUpdate.log' ) ); 120 | } 121 | } else { 122 | file_put_contents( __DIR__ . '/../../Includes/' . ( $experimental ? 'Update.log' : 'StableUpdate.log' ), $updatelog ); 123 | } 124 | 125 | if( $wasexperimental ) { 126 | file_put_contents( __DIR__ . '/../../Includes/updateversion', serialize( 'master' ) ); 127 | } else file_put_contents( __DIR__ . '/../../Includes/updateversion', serialize( 'stable' ) ); 128 | 129 | $this->expectOutputRegex( $outputRegex ); 130 | $result = $updater->Checkforupdate(); 131 | if( !$result ) $updater->updatePeachy(); 132 | $this->assertEquals( $expected, $result ); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /Tests/Includes/PageTest.php: -------------------------------------------------------------------------------- 1 | expectOutputString( "Getting page info for page ID {$pageid}..\n\n" ); 30 | $expectedPageId = $pageid; 31 | } else { 32 | $this->expectOutputString( "Getting page info for {$title}..\n\n" ); 33 | $expectedPageId = 1234; 34 | } 35 | 36 | $page = new Page( $this->getMockWiki(), $title, $pageid, $followRedir, $normalize, $timestamp ); 37 | 38 | $this->assertEquals( $expectedPageId, $page->get_id() ); 39 | $this->assertEquals( 0, $page->get_namespace() ); 40 | $this->assertEquals( 'Foo', $page->get_title() ); 41 | $this->assertEquals( 76, $page->get_length() ); 42 | $this->assertEquals( false, $page->redirectFollowed() ); 43 | $this->assertEquals( 66654, $page->get_talkID() ); 44 | $this->assertEquals( '', $page->get_preload() ); 45 | } 46 | 47 | /** 48 | * Provides data for testValidConstruction() 49 | * 50 | * @return array 51 | */ 52 | public function provideValidConstruction() { 53 | return array( 54 | array( 'Foo' ), 55 | array( 'Foo', 1234 ), 56 | array( 'Foo', 1234, false ), 57 | array( 'Foo', 1234, true ), 58 | array( 'Foo', 1234, false, true ), 59 | array( 'Foo', 1234, true, false ), 60 | array( 'Foo', 1234, true, true, '20141212121212' ), 61 | array( 'Foo', 1234, true, true, 2014 ), 62 | array( 'Foo', 1234, true, true, 20141212121212 ), 63 | ); 64 | } 65 | 66 | /** 67 | * Test for invalid construction of the Page class 68 | * 69 | * @covers Page::__construct 70 | * @dataProvider provideInvalidConstruction 71 | * 72 | * @param $expectedException 73 | * @param $wiki 74 | * @param $title 75 | * @param null $pageid 76 | * @param bool $followRedir 77 | * @param bool $normalize 78 | * @param null $timestamp 79 | */ 80 | public function testInvalidConstruction( $expectedException, $wiki, $title , $pageid = null , $followRedir = true , $normalize= true, $timestamp = null ) { 81 | $this->setExpectedException( $expectedException ); 82 | new Page( $wiki, $title, $pageid, $followRedir, $normalize, $timestamp ); 83 | } 84 | 85 | /** 86 | * Data provider for testInvalidConstruction() 87 | * 88 | * @return array 89 | */ 90 | public function provideInvalidConstruction() { 91 | return array( 92 | array( 'NoTitle', $this->getMockWiki(), null, null ), 93 | array( 'InvalidArgumentException', $this->getMockWiki(), new stdClass() ), 94 | array( 'InvalidArgumentException', $this->getMockWiki(), 'ImATitle', new stdClass() ), 95 | array( 'InvalidArgumentException', $this->getMockWiki(), 'ImATitle', null, null ), 96 | array( 'InvalidArgumentException', $this->getMockWiki(), 'ImATitle', null, true, null ), 97 | array( 'InvalidArgumentException', $this->getMockWiki(), 'ImATitle', null, true, true, new stdClass() ), 98 | ); 99 | } 100 | 101 | /** 102 | * @return \Wiki 103 | */ 104 | private function getMockWiki() { 105 | $mock = $this->getMockBuilder( 'Wiki' ) 106 | ->disableOriginalConstructor() 107 | ->getMock(); 108 | $mock->expects( $this->any() ) 109 | ->method( 'get_namespaces' ) 110 | ->will( $this->returnValue( array( 0 => '', 1 => 'Talk' ) ) ); 111 | $mock->expects( $this->any() ) 112 | ->method( 'apiQuery' ) 113 | ->will( 114 | $this->returnCallback( 115 | function ( $params ) { 116 | if( $params['action'] === 'query' 117 | && $params['prop'] === 'info' 118 | && $params['inprop'] === 'protection|talkid|watched|watchers|notificationtimestamp|subjectid|url|readable|preload|displaytitle' 119 | ) { 120 | if( array_key_exists( 'titles', $params ) ) { 121 | $title = $params['titles']; 122 | $pageid = 1234; 123 | } else { 124 | $title = 'Foo'; 125 | $pageid = $params['pageids']; 126 | } 127 | return array( 128 | 'query' => array( 129 | 'pages' => array( 130 | $pageid => array( 131 | 'pageid' => $pageid, 132 | 'ns' => 0, 133 | 'title' => $title, 134 | 'contentmodel' => 'wikitext', 135 | 'pagelanguage' => 'en', 136 | 'touched' => '2014-01-26T01:13:44Z', 137 | 'lastrevid' => 999, 138 | 'counter' => '', 139 | 'length' => 76, 140 | 'redirect' => '', 141 | 'protection' => array(), 142 | 'notificationtimestamp' => '', 143 | 'talkid' => '66654', 144 | 'fullurl' => 'imafullurl', 145 | 'editurl' => 'imaediturl', 146 | 'readable' => '', 147 | 'preload' => '', 148 | 'displaytitle' => 'Foo', 149 | ) 150 | ) 151 | ) 152 | ); 153 | } 154 | return array(); 155 | } 156 | ) 157 | ); 158 | return $mock; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Tests/Includes/PeachyAWBFunctionsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( $expected, PeachyAWBFunctions::fixDateTags( $text ) ); 50 | } 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /Tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | Peachy Documentation 4 | 5 | . 6 | */Tests/*,Tests/* 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | 18 | Tests 19 | 20 | 21 | 22 | 23 | Includes 24 | Plugins 25 | Scripts 26 | 27 | 28 | 29 | --------------------------------------------------------------------------------