After a long an arduous development journey, it is finally here! The 4th preview release of MapGuide Open Source 4.0 is now available for the following platforms:
- Windows
- Ubuntu Linux 22.04
- CentOS Linux 7.0
Need directions?
After a long an arduous development journey, it is finally here! The 4th preview release of MapGuide Open Source 4.0 is now available for the following platforms:
In the previous installment, we had gotten a test MapGuide installation with bundled PHP 8.1 up and running, and we were able to successfully produce a PHP error when hitting the Site Administrator.
This PHP error referenced previously was a milestone because it meant that our PHP 8.1 setup (FastCGI on Apache via mod_fcgid) was working, our PHP code was actually running and so the actual errors is the result of the vast swaths of our current PHP web tier applications needing to be migrated across to work against this new PHP 8.1 binding for the MapGuide API.
And so for the next few months I did just that, not just for PHP, but also for Java and .net. You can consider this post to be a preview of what you'll need to do yourself if you want to migrate your PHP/Java/.net MapGuide applications to MGOS 4.0.
PHP Migration Overview
The PHP binding was the one I expect to be the most work because besides finally supporting PHP 8.1, the major feature of this new PHP binding is that constants.php is no longer required! This is because with vanilla SWIG we now can bake all the constants of the MapGuide API into the PHP binding extension itself! So I expect a lot of "include 'constants.php'" references needing to be removed.
Once all the constants.php references are removed, we found the prime issue with this new PHP binding. PHP didn't like some of our C++ classes had overloaded methods whose signatures did not exist in the parent class. This manifested in the form of fatal PHP errors like the following:
PHP Fatal error: Declaration of MgCoordinateSystemMeasure::GetDistance(MgCoordinate|float|null $arg1, MgCoordinate|float|null $arg2, float $arg3, float $arg4): float must be compatible with MgMeasure::GetDistance(?MgCoordinate $arg1, ?MgCoordinate $arg2): float in Unknown on line 0
In this case, our MgMeasure class has a GetDistance method of the following signature:
double GetDistance(MgCoordinate* coord1, MgCoordinate* coord2)
In the derived MgCoordinateSystemMeasure, it has a new overload of GetDistance that has this signature:
double GetDistance(double x1, double y1, double x2, double y2)
However, when this converted to PHP proxy classes by SWIG, PHP doesn't like this class setup because under its inheritance model, it is expecting the GetDistance overload with 4 double parameters to also exist in the base MgMeasure class. This is not the case, and thus PHP throws the above fatal error.
To workaround this problem, we had to use the SWIG %rename directive to rename the conflicting overload signature in MgCoordinateSystemMeasure in PHP to the following:
double GetDistanceSimple(double x1, double y1, double x2, double y2)
With this rename, there is no longer a signature conflict in the generated proxy class and PHP no longer throws a fatal error. Fortunately, only 2 classes in the MapGuide API have this problem, so the amount of method renaming is minimal.
Once this issue was addressed, I tried firing up the PHP implementation of the AJAX viewer and I got an interactive map! Everything seemed to be working until I tried to generate a map plot, and found my second problem. I was getting PHP fatal errors like this:
PHP Fatal error: Uncaught TypeError: No matching function for overloaded 'MgRenderingService_RenderMap' in quickplotgeneratepicture.php:115
Fortunately, this one was easier to explain and fix. In PHP 8.1 (maybe even earlier in the PHP 7.x series), the type checking became more stricter which meant int parameters must take integers, double parameters must take doubles, etc, etc, you couldn't pass ints as doubles or vice versa, and for a method like RenderMap of MgRenderingService, there are lots of overloads that take many different combinations of int and double parameters.
Our map plotting code was passing in strings where int/double parameters were expected. In PHP 5.6 this was allowed because the type checking was evidently more lax. Now such cases cause the above PHP fatal error. This was easy enough to fix, we just use the intval and doubleval functions to make sure ints are being passed into int parameters and doubles are being passed into double parameters.
And with that, the rest of the changes involving fixing up our exception handling code due to a major change with how MapGuide applications should be handling exceptions from the MapGuide API. As part of this SWIG binding work, we've flattened the MapGuide exception hierarchy into a single MgException class, and introduced a new exception code property to allow handling MapGuide exceptions on a case-by-case basis.
So if you were handling specific MapGuide exceptions like this:
1 2 3 4 5 6 7 8 | try { //Some code that could throw } catch (MgUnauthorizedAccessException $e) { ... } |
You would now rewrite them like this:
1 2 3 4 5 6 7 8 9 10 | try { //Some code that could throw } catch (MgException $e) { if ($e->GetExceptionCode() == MgExceptionCodes::MgUnauthorizedAccessException) { ... } } |
The reason for flattening the exception hierarchy was:
Java Migration Overview
.net Migration Overview
This migration was the one I had been dreading the most. Not because I feared this binding was going to be fragile, because we already had an exhaustive test suite which this binding passed with flying colors.
But rather, I had been dreading this one because all of our .net code that is going to use this binding (AJAX viewer, code samples, etc) are all legacy pre-historic aspx webforms and I wasn't sure if such code would accept the brand new .net development story I had planned for it.
Consider the current .net development story.
What does netstandard2.0 support imply? It implies your MapGuide application using these packages can work on all of these platforms. Now practically speaking, despite now being netstandard2.0 packages, these packages will only work on platforms where the underlying OS is Windows and (maybe) Linux as those are the platforms where we can actually compile the underlying supporting native libraries needed by these nuget packages. So no Mac OSX, no Xamarin, etc.
In practical terms, it means you are no longer shackled to legacy .net framework for building MapGuide .net applications. You can now use .net core from its earliest netstandard2.0-supported iterations all the way to the latest .net 6.0 (as of this post).
That's great and all, but going back to the original issue: Can the current suite of aspx webforms code accept this new way of consuming the .net MapGuide API and come along for the ride? I hope so! Because the alternative is to rewrite all of this code with more modern .net web technologies (razor pages maybe?), and while such a rewrite has merit and probably warranted, it is not warranted right now because that would add many more months of dev work to my already time-poor schedule. We have bigger fish to fry! We just hope this current codebase will cooperate with our new .net packaging paradigm with minimal effort.
So let's start with the basic facts.
Getting this to work on Linux
With the bindings now all being generated by vanilla SWIG and all working on Windows, it was a case of getting the build system fully working again on Linux with these new bindings and updated web tier components.
Fortunately, on the binding front, most of the CMake configurations added in the initial phases of this work only needed minor adjustments, so the bulk of the work was actually building PHP 8.1 and integrating this into the Apache httpd server, which we are also building from source and updating our various httpd/php config file templates to work with this new version of PHP.
Where we stand now
We now finally have MapGuide API bindings generated with vanilla, unmodified SWIG that work on both Windows and Linux. This has been a long and arduous journey and I can finally see the light at the end of this tunnel!
A new RFC has been posted for PSC discussion/voting. I hope there isn't strong opposition/dissent on these changes, because I truly believe that this work is absolutely necessary for MapGuide going forward. Newer versions of .net/Java/PHP will always get released and inevitably we will need our MapGuide API bindings to work on these newer versions. Our current infrastructure to keep up with newer .net/Java/PHP versions is just not maintainable or tenable.
If/when this RFC is adopted, the long overdue Preview 4 release should drop not too long after!
A security fix is now available for MapGuide Open Source.
This fix mitigates several XSS vulnerabilities reported in the MapGuide Site Administrator tool.
To apply, simply extract the zip contents to the www/mapadmin folder of your MapGuide installation and overwrite all existing files.
This fix can be applied to the following versions of MapGuide Open Source:
Special thanks to Eitan Shav of mend.io who found and reported this vulnerability
In the interest of getting back into the habit of releasing things again and to line up authoring expectations/experience for another upcoming MapGuide Open Source 4.0 preview release, here's a long overdue release of MapGuide Maestro. Here's a summary of what's changed since the last release (Wow! It really has been 4 years since the last one?)
Previously, we made some important breakthroughs on the PHP8 binding for MapGuide. Our sanity PHP tests passed (meaning that the core essentials of the MapGuide API were working), but the main PHP test suite itself had a large amount of failures.
Thanks to dead-easy PHP debugging in VSCode, I was able to step into the PHP test code line-by-line and was able to discover that most of these failures was due to a change in how we're meant to be reading out expected test results from our test fixture SQLite databases. We were reading out test results as SQLite BLOBs which broke test result comparison in PHP8. Switching over to reading out these test results as strings instead fixed up along with updating how we check for exceptions thrown by the MapGuide API fixed up all but one test case.
That one remaining test case failure is due deleting a resource created in a preceding test that doesn't exist. While I could spend more time finding out why, the fact of the matter is that 99.9% of the current test suite is now passing on Windows, and it would be more fruitful now to sort out the revised bundling and configuration story with PHP 8 and try to get a working MapGuide installation with the PHP 8 bundled installation, fixing up whatever code and configurations along the way until we have all of our PHP-based MapGuide applications fully functional, which for reference is the following:
Since I got the basic sanity checking PHP script running without crashing, there was one problem remaining around MgByteReader contents being read incorrectly. This turned out to be another out-of-date SWIG typemap around byte arrays. Once that got fixed we are able to read out the contents of the MgByteReader without content corruption. This is an important thing to test because a lot of results from the MapGuide API (XML results, rendered map images, map plots, etc) come in the form of MgByteReaders. If we can't read content out of these readers without data corruption we have major problems, but that is fortunately not the case (so far!).
With that out of the way, it was onto to the current PHP test suite for the MapGuide API and getting it fixed up and passing.
But before we go there, I wanted to check if our new PHP binding was leaking memory in the most obvious cases. To that effect, I built our PHP binding with reference counting diagnostics enabled and wrote this PHP script.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | <?php function exceptionTest() { /* Expected release book-keeping: MgException - 1 */ try { $rid = new MgResourceIdentifier("iamnotvalid"); } catch (MgException $ex) { } } function throwingFunction() { $agfRw = new MgAgfReaderWriter(); $wktRw = new MgWktReaderWriter(); $rid = new MgResourceIdentifier("iamnotvalid"); } function exceptionTest2() { /* Expected release book-keeping: MgAgfReaderWriter - 1 MgWktReaderWriter - 1 MgException - 1 */ try { throwingFunction(); } catch (MgException $ex) { } } function exceptionTest3() { /* Expected release book-keeping: MgAgfReaderWriter - 1 MgWktReaderWriter - 1 MgException - 2 */ try { $rid = new MgResourceIdentifier("iamnotvalid"); } catch (MgException $ex) { } try { throwingFunction(); } catch (MgException $ex) { //Previous iterations of the binding would leak the previous $ex when //reusing the variable in this catch block //If the release book-keeping for MgException is 2, then this is no //longer the case } } function geometryXformTest() { /* Expected release book-keeping: MgCoordinateSystemFactory - 1 MgCoordinateSystem - 2 MgTransform - 1 MgWktReaderWriter - 1 MgPoint - 2 MgCoordinateXY - 2 */ $csFactory = new MgCoordinateSystemFactory(); $cs1 = $csFactory->CreateFromCode("LL84"); $cs2 = $csFactory->CreateFromCode("WGS84.PseudoMercator"); $xform = $csFactory->GetTransform($cs1, $cs2); $wktRw = new MgWktReaderWriter(); $pt = $wktRw->Read("POINT (1 2)"); $coord = $pt->GetCoordinate(); echo "Point (".$coord->GetX().", ".$coord->GetY().")\n"; $pt = $pt->Transform($xform); $coord = $pt->GetCoordinate(); echo "XPoint (".$coord->GetX().", ".$coord->GetY().")\n"; } function connectionServiceTest() { /* Expected release book-keeping: MgUserInformation - 1 MgSiteConnection - 1 MgProxyResourceService - 1 MgProxyFeatureService - 1 MgProxyRenderingService - 1 MgProxyMappingService - 1 MgProxyKmlService - 1 MgProxyDrawingService - 1 MgProxyTileService - 1 MgProxyProfilingService - 1 */ $userInfo = new MgUserInformation("Anonymous", ""); $site = new MgSiteConnection(); $site->Open($userInfo); $service1 = $site->CreateService(MgServiceType::ResourceService); $service2 = $site->CreateService(MgServiceType::FeatureService); $service3 = $site->CreateService(MgServiceType::RenderingService); $service4 = $site->CreateService(MgServiceType::MappingService); $service5 = $site->CreateService(MgServiceType::KmlService); $service6 = $site->CreateService(MgServiceType::DrawingService); $service7 = $site->CreateService(MgServiceType::TileService); $service8 = $site->CreateService(MgServiceType::ProfilingService); } function functionWithParam($userInfo) { $userInfo->SetLocale("en"); } function parameterPassingTest() { // We want to check parameters of Mg* objects passed do not encounter // refcounting shenanigans, but if they do, we want to make sure that // proper increment/decrement is happening so that the MgUserInformation // is released to 0 refcount (and thus deleted) when this function returns /* Expected release book-keeping: MgUserInformation - 1 */ $userInfo = new MgUserInformation(); functionWithParam($userInfo); echo "Locale: " . $userInfo->GetLocale() . "\n"; } MgInitializeWebTier("C:\\mg-4.0-install\\Web\\www\\webconfig_dev.ini"); echo "=========== BEGIN - exceptionTest() ===========\n"; exceptionTest(); echo "=========== END - exceptionTest() ===========\n"; echo "=========== BEGIN - exceptionTest2() ===========\n"; exceptionTest2(); echo "=========== END - exceptionTest2() ===========\n"; echo "=========== BEGIN - exceptionTest3() ===========\n"; exceptionTest3(); echo "=========== END - exceptionTest3() ===========\n"; echo "=========== BEGIN - connectionServiceTest() ===========\n"; connectionServiceTest(); echo "=========== END - connectionServiceTest() ===========\n"; echo "=========== BEGIN - parameterPassingTest() ===========\n"; parameterPassingTest(); echo "=========== END - parameterPassingTest() ===========\n"; echo "=========== BEGIN - geometryXformTest() ===========\n"; geometryXformTest(); echo "=========== END - geometryXformTest() ===========\n"; ?> |
1 2 | xdebug.mode = debug xdebug.start_with_request = yes |
Recently I have gotten back into the grind of MapGuide development pushing to clear the final hurdle of having functional PHP bindings for the MapGuide API.
Since I last touched this code, several things have changed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | <?php echo "Initializing web tier"; try { MgInitializeWebTier("C:\\mg-4.0-install\\Web\\www\\webconfig_dev.ini"); } catch (MgException $initEx) { echo "Init failure!"; die; } catch (Exception $ex) { echo "[php]: Exception: " . $ex->getMessage() . "\n"; die; } echo "[php]: Initialized\n"; // We haven't and shouldn't need to require/include constants.php // they are now baked into the PHP extension along with the other // MapGuide API proxy classes echo "[php]: Testing some constants\n"; echo " - " . MgMimeType::Agf . "\n"; echo " - " . MgMimeType::Kml . "\n"; echo " - " . MgMimeType::Mvt . "\n"; echo " - " . MgImageFormats::Png . "\n"; echo " - " . MgImageFormats::Gif . "\n"; echo " - " . MgLogFileType::Authentication . "\n"; echo " - " . MgServerInformationProperties::ClientOperationsQueueCount . "\n"; $user = new MgUserInformation("Anonymous", ""); // Basic set/get $user->SetLocale("en"); echo "[php]: Locale is: " . $user->GetLocale() . "\n"; $conn = new MgSiteConnection(); $conn->Open($user); // Create a session repository $site = $conn->GetSite(); $sessionID = $site->CreateSession(); echo "[php]: Created session: $sessionID\n"; $user->SetMgSessionId($sessionID); // Get an instance of the required services. $resourceService = $conn->CreateService(MgServiceType::ResourceService); echo "[php]: Created Resource Service\n"; $mappingService = $conn->CreateService(MgServiceType::MappingService); echo "[php]: Created Mapping Service\n"; $resId = new MgResourceIdentifier("Library://"); echo "[php]: Enumeratin'\n"; $resources = $resourceService->EnumerateResources($resId, -1, ""); echo $resources->ToString() . "\n"; echo "[php]: Coordinate System\n"; $csFactory = new MgCoordinateSystemFactory(); echo "[php]: CS Catalog\n"; $catalog = $csFactory->GetCatalog(); echo "[php]: Category Dictionary\n"; $catDict = $catalog->GetCategoryDictionary(); echo "[php]: CS Dictionary\n"; $csDict = $catalog->GetCoordinateSystemDictionary(); echo "[php]: Datum Dictionary\n"; $datumDict = $catalog->GetDatumDictionary(); echo "[php]: Coordinate System - LL84\n"; $cs1 = $csFactory->CreateFromCode("LL84"); echo "[php]: Coordinate System - WGS84.PseudoMercator\n"; $cs2 = $csFactory->CreateFromCode("WGS84.PseudoMercator"); echo "[php]: Make xform\n"; $xform = $csFactory->GetTransform($cs1, $cs2); echo "[php]: WKT Reader\n"; $wktRw = new MgWktReaderWriter(); echo "[php]: WKT Point\n"; $pt = $wktRw->Read("POINT (1 2)"); $coord = $pt->GetCoordinate(); echo "[php]: X: ".$coord->GetX().", Y: ".$coord->GetY()."\n"; $site->DestroySession($sessionID); echo "[php]: Destroyed session $sessionID\n"; echo "[php]: Test byte reader\n"; $bs = new MgByteSource("abcd1234", 8); $content = ""; $br = $bs->GetReader(); $buf = ""; while ($br->Read($buf, 2) > 0) { echo "Buffer: '$buf'\n"; $content .= $buf; } echo "[php]: Test byte reader2\n"; $bs2 = new MgByteSource("abcd1234", 8); $content = ""; $br2 = $bs2->GetReader(); $buf = ""; while ($br2->Read($buf, 3) > 0) { echo "Buffer: '$buf'\n"; $content .= $buf; } $agfRw = new MgAgfReaderWriter(); echo "[php]: Trigger an exception\n"; try { $agfRw->Read(NULL); } catch (MgException $ex) { echo "[php]: MgException caught\n"; echo "[php]: MgException - Code: ".$ex->GetExceptionCode()."\n"; echo "[php]: MgException - Message: ".$ex->GetExceptionMessage()."\n"; echo "[php]: MgException - Details: ".$ex->GetDetails()."\n"; echo "[php]: MgException - Stack: ".$ex->GetStackTrace()."\n"; echo "Is standard PHP exception too? " . ($ex instanceof Exception) . "\n"; } catch (Exception $ex) { echo "[php]: Exception: " . $ex->getMessage() . "\n"; } echo "[php]: Trigger another exception\n"; try { $r2 = new MgResourceIdentifier(""); } catch (MgException $ex2) { echo "[php]: MgException caught\n"; echo "[php]: MgException - Code: ".$ex2->GetExceptionCode()."\n"; echo "[php]: MgException - Message: ".$ex2->GetExceptionMessage()."\n"; echo "[php]: MgException - Details: ".$ex2->GetDetails()."\n"; echo "[php]: MgException - Stack: ".$ex2->GetStackTrace()."\n"; echo "Is standard PHP exception too? " . ($ex2 instanceof Exception) . "\n"; } catch (Exception $ex) { echo "[php]: Exception: " . $ex->getMessage() . "\n"; } echo "Press any key to exit\n"; readline(); ?> |