Issues (4069)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

modules/Administration/SugarSpriteBuilder.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
3
/*********************************************************************************
4
 * SugarCRM Community Edition is a customer relationship management program developed by
5
 * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc.
6
7
 * SuiteCRM is an extension to SugarCRM Community Edition developed by Salesagility Ltd.
8
 * Copyright (C) 2011 - 2014 Salesagility Ltd.
9
 *
10
 * This program is free software; you can redistribute it and/or modify it under
11
 * the terms of the GNU Affero General Public License version 3 as published by the
12
 * Free Software Foundation with the addition of the following permission added
13
 * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
14
 * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
15
 * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
16
 *
17
 * This program is distributed in the hope that it will be useful, but WITHOUT
18
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
19
 * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
20
 * details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License along with
23
 * this program; if not, see http://www.gnu.org/licenses or write to the Free
24
 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
25
 * 02110-1301 USA.
26
 *
27
 * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
28
 * SW2-130, Cupertino, CA 95014, USA. or at email address [email protected].
29
 *
30
 * The interactive user interfaces in modified source and object code versions
31
 * of this program must display Appropriate Legal Notices, as required under
32
 * Section 5 of the GNU Affero General Public License version 3.
33
 *
34
 * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
35
 * these Appropriate Legal Notices must retain the display of the "Powered by
36
 * SugarCRM" logo and "Supercharged by SuiteCRM" logo. If the display of the logos is not
37
 * reasonably feasible for  technical reasons, the Appropriate Legal Notices must
38
 * display the words  "Powered by SugarCRM" and "Supercharged by SuiteCRM".
39
 ********************************************************************************/
40
41
42
require_once("include/SugarTheme/cssmin.php");
43
44
class SugarSpriteBuilder
45
{
46
	var $isAvailable = false;
47
	var $silentRun = false;
48
    var $fromSilentUpgrade = false;
49
    var $writeToUpgradeLog = false;
50
51
	var $debug = false;
52
	var $fileName = 'sprites';
53
	var $cssMinify = true;
54
55
	// class supported image types
56
	var $supportedTypeMap = array(
57
		IMG_GIF => IMAGETYPE_GIF,
58
		IMG_JPG => IMAGETYPE_JPEG,
59
		IMG_PNG => IMAGETYPE_PNG,
60
	);
61
62
	// sprite settings
63
	var $pngCompression = 9;
64
	var $pngFilter = PNG_NO_FILTER;
65
	var $maxWidth = 75;
66
	var $maxHeight = 75;
67
	var $rowCnt = 30;
68
69
	// processed image types
70
	var $imageTypes = array();
71
72
	// source files
73
	var $spriteSrc = array();
74
	var $spriteRepeat = array();
75
76
	// sprite resource images
77
	var $spriteImg;
78
79
	// sprite_config collection
80
	var $sprites_config = array();
81
82
83
    public function __construct()
84
    {
85
		// check if we have gd installed
86
		if(function_exists('imagecreatetruecolor'))
87
        {
88
			$this->isAvailable = true;
89
            foreach($this->supportedTypeMap as $gd_bit => $imagetype)
90
            {
91
                if(imagetypes() & $gd_bit) {
92
                    // swap gd_bit & imagetype
93
                    $this->imageTypes[$imagetype] = $gd_bit;
94
                }
95
            }
96
		}
97
98
        if(function_exists('logThis') && isset($GLOBALS['path']))
99
        {
100
            $this->writeToUpgradeLog = true;
101
        }
102
	}
103
104
105
    /**
106
     * addDirectory
107
     *
108
     * This function is used to create the spriteSrc array
109
     * @param $name String value of the sprite name
110
     * @param $dir String value of the directory associated with the sprite entry
111
     */
112
	public function addDirectory($name, $dir) {
113
114
		// sprite namespace
115
		if(!array_key_exists($name, $this->spriteSrc))
116
        {
117
			$this->spriteSrc[$name] = array();
118
		}
119
120
		// add files from directory
121
		$this->spriteSrc[$name][$dir] = $this->getFileList($dir);
122
	}
123
124
	/**
125
     * getFileList
126
     *
127
     * This method processes files in a directory and adds them to the sprites array
128
     * @param $dir String value of the directory to scan for image files in
129
     */
130
	private function getFileList($dir) {
131
		$list = array();
132
		if(is_dir($dir)) {
133
			if($dh = opendir($dir)) {
134
135
				// optional sprites_config.php file
136
				$this->loadSpritesConfig($dir);
137
138
			    while (($file = readdir($dh)) !== false)
139
                {
140
					if ($file != "." && $file != ".." && $file != "sprites_config.php")
141
                    {
142
143
						// file info & check supported image format
144
						if($info = $this->getFileInfo($dir, $file)) {
145
146
							// skip excluded files
147
							if(isset($this->sprites_config[$dir]['exclude']) && array_search($file, $this->sprites_config[$dir]['exclude']) !== false)
148
                            {
149
                                global $mod_strings;
150
                                $msg = string_format($mod_strings['LBL_SPRITES_EXCLUDING_FILE'], array("{$dir}/{$file}"));
151
								$GLOBALS['log']->debug($msg);
152
                                $this->logMessage($msg);
153
							} else {
154
								// repeatable sprite ?
155
								$isRepeat = false;
156
157
								if(isset($this->sprites_config[$dir]['repeat']))
158
                                {
159
									foreach($this->sprites_config[$dir]['repeat'] as $repeat)
160
                                    {
161
										if($info['x'] == $repeat['width'] && $info['y'] == $repeat['height'])
162
                                        {
163
											$id = md5($repeat['width'].$repeat['height'].$repeat['direction']);
164
											$isRepeat = true;
165
											$this->spriteRepeat['repeat_'.$repeat['direction'].'_'.$id][$dir][$file] = $info;
166
										}
167
									}
168
								}
169
170
								if(!$isRepeat)
171
                                {
172
									$list[$file] = $info;
173
                                }
174
							}
175
						} else if(preg_match('/\.(jpg|jpeg|gif|png|bmp|ico)$/i', $file)) {
176
                            $GLOBALS['log']->error('Unable to process image file ' . $file);
177
                            //$this->logMessage('Unable to process image file ' . $file);
178
                        }
179
	   	     		}
180
   		 		}
181
			}
182
		    closedir($dh);
183
		}
184
		return $list;
185
	}
186
187
188
    /**
189
     * loadSpritesConfig
190
     *
191
     * This function is used to load the sprites_config.php file.  The sprites_config.php file may be used to add entries
192
     * to the sprites_config member variable which may contain a list of array entries of files/directories to exclude from
193
     * being included into the sprites image.
194
     *
195
     * @param $dir String value of the directory containing the custom sprites_config.php file
196
     */
197
	private function loadSpritesConfig($dir) {
198
		$sprites_config = array();
199
		if(file_exists("$dir/sprites_config.php"))
200
        {
201
			include("$dir/sprites_config.php");
202
			if(count($sprites_config)) {
203
				$this->sprites_config = array_merge($this->sprites_config, $sprites_config);
204
			}
205
		}
206
	}
207
208
209
	/**
210
     * getFileInfo
211
     *
212
     * This is a private helper function to return attributes about an image.  If the width, height or type of the
213
     * image file cannot be determined, then we do not process the file.
214
     *
215
     * @return array of file info entries containing file information (x, y, type) if image type is supported
216
     */
217
	private function getFileInfo($dir, $file) {
218
		$result = false;
219
		$info = @getimagesize($dir.'/'.$file);
220
		if($info) {
221
222
			// supported image type ?
223
			if(isset($this->imageTypes[$info[2]]))
224
            {
225
				$w = $info[0];
226
				$h = $info[1];
227
				$surface = $w * $h;
228
229
				// be sure we have an image size
230
				$addSprite = false;
231
				if($surface)
232
                {
233
					// sprite dimensions
234
					if($w <= $this->maxWidth && $h <= $this->maxHeight)
235
                    {
236
						$addSprite = true;
237
					}
238
				}
239
240
				if($addSprite)
241
                {
242
					$result = array();
243
					$result['x'] = $w;
244
					$result['y'] = $h;
245
					$result['type'] = $info[2];
246
				}
247
			} else {
248
                $msg = "Skipping unsupported image file type ({$info[2]}) for file {$file}";
249
                $GLOBALS['log']->error($msg);
250
                $this->logMessage($msg."\n");
251
            }
252
		}
253
		return $result;
254
	}
255
256
257
    /**
258
     * createSprites
259
     *
260
     * This is the public function to allow the sprites to be built.
261
     *
262
     * @return $result boolean value indicating whether or not sprites were created
0 ignored issues
show
The doc-type $result could not be parsed: Unknown type name "$result" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
263
     */
264
	public function createSprites() {
265
266
        global $mod_strings;
267
268
		if(!$this->isAvailable)
269
        {
270
			if(!$this->silentRun)
271
            {
272
                $msg = $mod_strings['LBL_SPRITES_NOT_SUPPORTED'];
273
                $GLOBALS['log']->warn($msg);
274
                $this->logMessage($msg);
275
            }
276
			return false;
277
		}
278
279
		// add repeatable sprites
280
		if(count($this->spriteRepeat))
281
        {
282
			$this->spriteSrc = array_merge($this->spriteSrc, $this->spriteRepeat);
283
        }
284
285
		foreach($this->spriteSrc as $name => $dirs)
286
        {
287
			if(!$this->silentRun)
288
            {
289
                $msg = string_format($mod_strings['LBL_SPRITES_CREATING_NAMESPACE'], array($name));
290
                $GLOBALS['log']->debug($msg);
291
				$this->logMessage($msg);
292
            }
293
294
			// setup config for sprite placement algorithm
295
			if(substr($name, 0, 6) == 'repeat')
296
            {
297
				$isRepeat = true;
298
                $type = substr($name, 7, 10) == 'horizontal' ? 'horizontal' : 'vertical';
299
				$config = array(
300
					'type' => $type,
301
				);
302
			} else {
303
				$isRepeat = false;
304
				$config = array(
305
					'type' => 'boxed',
306
					'width' => $this->maxWidth,
307
					'height' => $this->maxHeight,
308
					'rowcnt' => $this->rowCnt,
309
				);
310
			}
311
312
			// use separate class to arrange the images
313
			$sp = new SpritePlacement($dirs, $config);
314
			$sp->processSprites();
315
316
			//if(! $this->silentRun)
317
			//	echo " (size {$sp->width()}x{$sp->height()})<br />";
318
319
			// we need a target image size
320
			if($sp->width() && $sp->height())
321
            {
322
				// init sprite image
323
				$this->initSpriteImg($sp->width(), $sp->height());
324
325
				// add sprites based upon determined coordinates
326
				foreach($dirs as $dir => $files)
327
                {
328
					if(!$this->silentRun)
329
                    {
330
                        $msg = string_format($mod_strings['LBL_SPRITES_PROCESSING_DIR'], array($dir));
331
                        $GLOBALS['log']->debug($msg);
332
                        $this->logMessage($msg);
333
                    }
334
335
					foreach($files as $file => $info)
336
                    {
337
						if($im = $this->loadImage($dir, $file, $info['type']))
338
                        {
339
							// coordinates
340
							$dst_x = $sp->spriteMatrix[$dir.'/'.$file]['x'];
341
							$dst_y = $sp->spriteMatrix[$dir.'/'.$file]['y'];
342
343
							imagecopy($this->spriteImg, $im, $dst_x, $dst_y, 0, 0, $info['x'], $info['y']);
344
							imagedestroy($im);
345
346
							if(!$this->silentRun)
347
                            {
348
                                $msg = string_format($mod_strings['LBL_SPRITES_ADDED'], array("{$dir}/{$file}"));
349
                                $GLOBALS['log']->debug($msg);
350
                                $this->logMessage($msg);
351
                            }
352
						}
353
					}
354
				}
355
356
				// dir & filenames
357
				if($isRepeat)
358
                {
359
					$outputDir = sugar_cached("sprites/Repeatable");
360
					$spriteFileName = "{$name}.png";
361
					$cssFileName = "{$this->fileName}.css";
362
					$metaFileName = "{$this->fileName}.meta.php";
363
					$nameSpace = "Repeatable";
364
				} else {
365
					$outputDir = sugar_cached("sprites/$name");
366
					$spriteFileName = "{$this->fileName}.png";
367
					$cssFileName = "{$this->fileName}.css";
368
					$metaFileName = "{$this->fileName}.meta.php";
369
					$nameSpace = "{$name}";
370
				}
371
372
				// directory structure
373
				if(!is_dir(sugar_cached("sprites/$nameSpace")))
374
                {
375
					sugar_mkdir(sugar_cached("sprites/$nameSpace"), 0775, true);
376
                }
377
378
				// save sprite image
379
				imagepng($this->spriteImg, "$outputDir/$spriteFileName", $this->pngCompression, $this->pngFilter);
380
				imagedestroy($this->spriteImg);
381
382
				/* generate css & metadata */
383
384
				$head = '';
385
				$body = '';
386
				$metadata = '';
387
388
				foreach($sp->spriteSrc as $id => $info)
389
                {
390
					// sprite id
391
					$hash_id = md5($id);
392
393
					// header
394
					$head .= "span.spr_{$hash_id},\n";
395
396
					// image size
397
					$w = $info['x'];
398
					$h = $info['y'];
399
400
					// image offset
401
					$offset_x = $sp->spriteMatrix[$id]['x'];
402
					$offset_y = $sp->spriteMatrix[$id]['y'];
403
404
					// sprite css
405
					$body .= "/* {$id} */
406
span.spr_{$hash_id} {
407
width: {$w}px;
408
height: {$h}px;
409
background-position: -{$offset_x}px -{$offset_y}px;
410
}\n";
411
412
					$metadata .= '$sprites["'.$id.'"] = array ("class"=>"'.$hash_id.'","width"=>"'.$w.'","height"=>"'.$h.'");'."\n";
413
				}
414
415
				// common css header
416
                require_once('include/utils.php');
417
                $bg_path = getVersionedPath('index.php').'&entryPoint=getImage&imageName='.$spriteFileName.'&spriteNamespace='.$nameSpace;
418
				$head = rtrim($head, "\n,")." {background: url('../../../{$bg_path}'); no-repeat;display:inline-block;}\n";
419
420
				// append mode for repeatable sprites
421
                $fileMode = $isRepeat ? 'a' : 'w';
422
423
				// save css
424
				$css_content = "\n/* autogenerated sprites - $name */\n".$head.$body;
425
				if($this->cssMinify)
426
                {
427
					$css_content = cssmin::minify($css_content);
428
                }
429
				$fh = fopen("$outputDir/$cssFileName", $fileMode);
430
				fwrite($fh, $css_content);
431
				fclose($fh);
432
433
				/* save metadata */
434
				$add_php_tag = (file_exists("$outputDir/$metaFileName") && $isRepeat) ? false : true;
435
				$fh = fopen("$outputDir/$metaFileName", $fileMode);
436
				if($add_php_tag)
437
                {
438
					fwrite($fh, '<?php');
439
                }
440
				fwrite($fh, "\n/* sprites metadata - $name */\n");
441
				fwrite($fh, $metadata."\n");
442
				fclose($fh);
443
444
			// if width & height
445
			} else {
446
447
				if(!$this->silentRun)
448
                {
449
                    $msg = string_format($mod_strings['LBL_SPRITES_ADDED'], array($name));
450
                    $GLOBALS['log']->debug($msg);
451
                    $this->logMessage($msg);
452
                }
453
454
			}
455
456
		}
457
		return true;
458
	}
459
460
461
	/**
462
     * initSpriteImg
463
     *
464
     * @param w int value representing width of sprite
465
     * @param h int value representing height of sprite
466
     * Private function to initialize creating the sprite canvas image
467
     */
468
	private function initSpriteImg($w, $h) {
469
		$this->spriteImg = imagecreatetruecolor($w,$h);
470
		$transparent = imagecolorallocatealpha($this->spriteImg, 0, 0, 0, 127);
471
		imagefill($this->spriteImg, 0, 0, $transparent);
472
		imagealphablending($this->spriteImg, false);
473
		imagesavealpha($this->spriteImg, true);
474
	}
475
476
477
	/**
478
     * loadImage
479
     *
480
     * private function to load image resources
481
     *
482
     * @param $dir String value of directory where image is located
483
     * @param $file String value of file
484
     * @param $type String value of the file type (IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG)
485
     *
486
     */
487
	private function loadImage($dir, $file, $type) {
488
		$path_file = $dir.'/'.$file;
489
		switch($type) {
490
			case IMAGETYPE_GIF:
491
				return imagecreatefromgif($path_file);
492
			case IMAGETYPE_JPEG:
493
				return imagecreatefromjpeg($path_file);
494
			case IMAGETYPE_PNG:
495
				return imagecreatefrompng($path_file);
496
			default:
497
				return false;
498
		}
499
	}
500
501
    /**
502
     * private logMessage
503
     *
504
     * This is a private function used to log messages generated from this class.  Depending on whether or not
505
     * silentRun or fromSilentUpgrade is set to true/false then it will either output to screen or write to log file
506
     *
507
     * @param $msg String value of message to log into file or echo into output buffer depending on the context
508
     */
509
    private function logMessage($msg)
510
    {
511
        if(!$this->silentRun && !$this->fromSilentUpgrade)
512
        {
513
            echo $msg . '<br />';
514
        } else if ($this->fromSilentUpgrade && $this->writeToUpgradeLog) {
515
            logThis($msg, $GLOBALS['path']);
516
        } else if(!$this->silentRun) {
517
            echo $msg . "\n";
518
        }
519
    }
520
}
521
522
523
/**
524
 * SpritePlacement
525
 *
526
 */
527
class SpritePlacement
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
528
{
529
530
	// occupied space
531
	var $spriteMatrix = array();
532
533
	// minimum surface
534
	var $minSurface = 0;
535
536
	// sprite src (flattened array)
537
	var $spriteSrc = array();
538
539
	// placement config array
540
	/*
541
		type = 	boxed
542
				horizontal
543
				vertical
544
545
		required params for
546
		type 1 	-> width
547
				-> height
548
				-> rowcnt
549
550
	*/
551
	var $config = array();
552
553
	function __construct($spriteSrc, $config) {
554
555
		// convert spriteSrc to flat array
556
		foreach($spriteSrc as $dir => $files) {
557
			foreach($files as $file => $info) {
558
				// use full path as identifier
559
				$full_path = $dir.'/'.$file;
560
				$this->spriteSrc[$full_path] = $info;
561
			}
562
		}
563
564
		$this->config = $config;
565
	}
566
567
	function processSprites() {
568
569
		foreach($this->spriteSrc as $id => $info) {
570
571
			// dimensions
572
			$x = $info['x'];
573
			$y = $info['y'];
574
575
			// update min surface
576
			$this->minSurface += $x * $y;
577
578
			// get coordinates where to add this sprite
579
			if($coor = $this->addSprite($x, $y)) {
580
				$this->spriteMatrix[$id] = $coor;
581
			}
582
		}
583
	}
584
585
	// returns x/y coordinates to fit the sprite
586
	function addSprite($w, $h) {
587
		$result = false;
588
589
		switch($this->config['type']) {
590
591
			// boxed
592
			case 'boxed':
0 ignored issues
show
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
593
594
				$spriteX = $this->config['width'];
595
				$spriteY = $this->config['height'];
596
				$spriteCnt = count($this->spriteMatrix) + 1;
597
				$y = ceil($spriteCnt / $this->config['rowcnt']);
598
				$x = $spriteCnt - (($y - 1) * $this->config['rowcnt']);
599
				$result = array(
600
					'x' => ($x * $spriteX) + 1 - $spriteX,
601
					'y' => ($y * $spriteY) + 1 - $spriteY);
602
603
				break;
604
605
			// horizontal -> align vertically
606
			case 'horizontal':
607
				$result = array('x' => 1, 'y' => $this->height() + 1);
608
				break;
609
610
			// vertical -> align horizontally
611
			case 'vertical':
612
				$result = array('x' => $this->width() + 1, 'y' => 1);
613
				break;
614
615
			default:
616
				$GLOBALS['log']->warn(__CLASS__.": Unknown sprite placement algorithm -> {$this->config['type']}");
617
				break;
618
		}
619
620
		return $result;
621
	}
622
623
	// calculate total width
624
	function width() {
625
		return $this->getMaxAxis('x');
626
	}
627
628
	// calculate total height
629
	function height() {
630
		return $this->getMaxAxis('y');
631
	}
632
633
	// helper function to get highest axis value
634
	function getMaxAxis($axis) {
635
		$val = 0;
636
		foreach($this->spriteMatrix as $id => $coor) {
637
			$new_val = $coor[$axis] + $this->spriteSrc[$id][$axis] - 1;
638
			if($new_val > $val) {
639
				$val = $new_val;
640
			}
641
		}
642
		return $val;
643
	}
644
}
645
646
?>
647