From 04d6d5ca99ebfd1cebb8ce06618fb3811fc1a8aa Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 9 Jan 2020 10:55:03 +0100 Subject: phpmyadmin working --- .../libraries/advisory_rules_generic.txt | 402 ++ .../libraries/advisory_rules_mysql_before80003.txt | 57 + srcs/phpmyadmin/libraries/certs/12d55845.0 | 20 + srcs/phpmyadmin/libraries/certs/2e5ac55d.0 | 20 + srcs/phpmyadmin/libraries/certs/4042bcee.0 | 31 + srcs/phpmyadmin/libraries/certs/6187b673.0 | 31 + srcs/phpmyadmin/libraries/certs/README.rst | 16 + srcs/phpmyadmin/libraries/certs/cacert.pem | 51 + srcs/phpmyadmin/libraries/classes/Advisor.php | 707 +++ srcs/phpmyadmin/libraries/classes/Bookmark.php | 395 ++ .../libraries/classes/BrowseForeigners.php | 361 ++ .../libraries/classes/CentralColumns.php | 1207 +++++ srcs/phpmyadmin/libraries/classes/Charsets.php | 210 + .../libraries/classes/Charsets/Charset.php | 103 + .../libraries/classes/Charsets/Collation.php | 549 ++ .../libraries/classes/CheckUserPrivileges.php | 372 ++ srcs/phpmyadmin/libraries/classes/Config.php | 1813 +++++++ .../libraries/classes/Config/ConfigFile.php | 531 ++ .../libraries/classes/Config/Descriptions.php | 934 ++++ srcs/phpmyadmin/libraries/classes/Config/Form.php | 238 + .../libraries/classes/Config/FormDisplay.php | 924 ++++ .../classes/Config/FormDisplayTemplate.php | 526 ++ .../libraries/classes/Config/Forms/BaseForm.php | 89 + .../classes/Config/Forms/BaseFormList.php | 150 + .../classes/Config/Forms/Page/BrowseForm.php | 30 + .../classes/Config/Forms/Page/DbStructureForm.php | 30 + .../classes/Config/Forms/Page/EditForm.php | 32 + .../classes/Config/Forms/Page/ExportForm.php | 18 + .../classes/Config/Forms/Page/ImportForm.php | 18 + .../classes/Config/Forms/Page/NaviForm.php | 18 + .../classes/Config/Forms/Page/PageFormList.php | 37 + .../classes/Config/Forms/Page/SqlForm.php | 18 + .../Config/Forms/Page/TableStructureForm.php | 30 + .../classes/Config/Forms/Setup/ConfigForm.php | 32 + .../classes/Config/Forms/Setup/ExportForm.php | 18 + .../classes/Config/Forms/Setup/FeaturesForm.php | 77 + .../classes/Config/Forms/Setup/ImportForm.php | 18 + .../classes/Config/Forms/Setup/MainForm.php | 29 + .../classes/Config/Forms/Setup/NaviForm.php | 18 + .../classes/Config/Forms/Setup/ServersForm.php | 116 + .../classes/Config/Forms/Setup/SetupFormList.php | 37 + .../classes/Config/Forms/Setup/SqlForm.php | 28 + .../classes/Config/Forms/User/ExportForm.php | 160 + .../classes/Config/Forms/User/FeaturesForm.php | 95 + .../classes/Config/Forms/User/ImportForm.php | 73 + .../classes/Config/Forms/User/MainForm.php | 98 + .../classes/Config/Forms/User/NaviForm.php | 74 + .../classes/Config/Forms/User/SqlForm.php | 54 + .../classes/Config/Forms/User/UserFormList.php | 35 + .../libraries/classes/Config/PageSettings.php | 233 + .../classes/Config/ServerConfigChecks.php | 583 ++ .../classes/Config/SpecialSchemaLinks.php | 478 ++ .../libraries/classes/Config/Validator.php | 594 ++ srcs/phpmyadmin/libraries/classes/Console.php | 158 + .../classes/Controllers/AbstractController.php | 51 + .../classes/Controllers/AjaxController.php | 97 + .../Controllers/BrowseForeignersController.php | 82 + .../Controllers/Database/AbstractController.php | 42 + .../Database/CentralColumnsController.php | 195 + .../Database/DataDictionaryController.php | 156 + .../Controllers/Database/EventsController.php | 43 + .../Database/MultiTableQueryController.php | 61 + .../Controllers/Database/RoutinesController.php | 44 + .../classes/Controllers/Database/SqlController.php | 49 + .../Controllers/Database/StructureController.php | 1088 ++++ .../Controllers/Database/TriggersController.php | 43 + .../classes/Controllers/HomeController.php | 517 ++ .../Controllers/Server/BinlogController.php | 149 + .../Controllers/Server/CollationsController.php | 100 + .../Controllers/Server/DatabasesController.php | 424 ++ .../Controllers/Server/EnginesController.php | 69 + .../Controllers/Server/PluginsController.php | 77 + .../Controllers/Server/ReplicationController.php | 72 + .../classes/Controllers/Server/SqlController.php | 34 + .../Server/Status/AbstractController.php | 42 + .../Server/Status/AdvisorController.php | 60 + .../Server/Status/MonitorController.php | 146 + .../Server/Status/ProcessesController.php | 240 + .../Server/Status/QueriesController.php | 75 + .../Controllers/Server/Status/StatusController.php | 260 + .../Server/Status/VariablesController.php | 639 +++ .../Controllers/Server/VariablesController.php | 238 + .../Controllers/Setup/AbstractController.php | 70 + .../classes/Controllers/Setup/ConfigController.php | 55 + .../classes/Controllers/Setup/FormController.php | 50 + .../classes/Controllers/Setup/HomeController.php | 228 + .../Controllers/Setup/ServersController.php | 66 + .../Controllers/Table/AbstractController.php | 54 + .../classes/Controllers/Table/ChartController.php | 261 + .../Table/GisVisualizationController.php | 227 + .../Controllers/Table/IndexesController.php | 179 + .../Controllers/Table/RelationController.php | 398 ++ .../classes/Controllers/Table/SearchController.php | 1244 +++++ .../classes/Controllers/Table/SqlController.php | 53 + .../Controllers/Table/StructureController.php | 1648 ++++++ .../TransformationOverviewController.php | 80 + srcs/phpmyadmin/libraries/classes/Core.php | 1302 +++++ .../libraries/classes/CreateAddField.php | 555 ++ .../libraries/classes/Database/DatabaseList.php | 60 + .../libraries/classes/Database/Designer.php | 407 ++ .../libraries/classes/Database/Designer/Common.php | 830 +++ .../classes/Database/Designer/DesignerTable.php | 103 + .../libraries/classes/Database/MultiTableQuery.php | 145 + srcs/phpmyadmin/libraries/classes/Database/Qbe.php | 1963 +++++++ .../libraries/classes/Database/Search.php | 347 ++ .../libraries/classes/DatabaseInterface.php | 3187 +++++++++++ .../libraries/classes/Dbi/DbiExtension.php | 248 + .../phpmyadmin/libraries/classes/Dbi/DbiMysqli.php | 610 +++ srcs/phpmyadmin/libraries/classes/Di/Migration.php | 71 + .../libraries/classes/Display/ChangePassword.php | 182 + .../libraries/classes/Display/CreateTable.php | 56 + .../phpmyadmin/libraries/classes/Display/Error.php | 56 + .../libraries/classes/Display/Export.php | 825 +++ .../libraries/classes/Display/GitRevision.php | 144 + .../libraries/classes/Display/Import.php | 127 + .../libraries/classes/Display/ImportAjax.php | 140 + .../libraries/classes/Display/Results.php | 5698 ++++++++++++++++++++ srcs/phpmyadmin/libraries/classes/Encoding.php | 358 ++ srcs/phpmyadmin/libraries/classes/Engines/Bdb.php | 76 + .../libraries/classes/Engines/Berkeleydb.php | 19 + .../libraries/classes/Engines/Binlog.php | 31 + .../libraries/classes/Engines/Innobase.php | 19 + .../libraries/classes/Engines/Innodb.php | 395 ++ .../libraries/classes/Engines/Memory.php | 34 + .../phpmyadmin/libraries/classes/Engines/Merge.php | 21 + .../libraries/classes/Engines/MrgMyisam.php | 29 + .../libraries/classes/Engines/Myisam.php | 88 + .../libraries/classes/Engines/Ndbcluster.php | 54 + srcs/phpmyadmin/libraries/classes/Engines/Pbxt.php | 195 + .../classes/Engines/PerformanceSchema.php | 31 + srcs/phpmyadmin/libraries/classes/Error.php | 526 ++ srcs/phpmyadmin/libraries/classes/ErrorHandler.php | 604 +++ srcs/phpmyadmin/libraries/classes/ErrorReport.php | 294 + srcs/phpmyadmin/libraries/classes/Export.php | 1225 +++++ srcs/phpmyadmin/libraries/classes/File.php | 828 +++ srcs/phpmyadmin/libraries/classes/FileListing.php | 108 + srcs/phpmyadmin/libraries/classes/Font.php | 236 + srcs/phpmyadmin/libraries/classes/Footer.php | 370 ++ .../libraries/classes/Gis/GisFactory.php | 50 + .../libraries/classes/Gis/GisGeometry.php | 407 ++ .../classes/Gis/GisGeometryCollection.php | 419 ++ .../libraries/classes/Gis/GisLineString.php | 360 ++ .../libraries/classes/Gis/GisMultiLineString.php | 449 ++ .../libraries/classes/Gis/GisMultiPoint.php | 416 ++ .../libraries/classes/Gis/GisMultiPolygon.php | 617 +++ srcs/phpmyadmin/libraries/classes/Gis/GisPoint.php | 363 ++ .../libraries/classes/Gis/GisPolygon.php | 618 +++ .../libraries/classes/Gis/GisVisualization.php | 726 +++ srcs/phpmyadmin/libraries/classes/Header.php | 705 +++ srcs/phpmyadmin/libraries/classes/Import.php | 1727 ++++++ srcs/phpmyadmin/libraries/classes/Index.php | 901 ++++ srcs/phpmyadmin/libraries/classes/IndexColumn.php | 188 + srcs/phpmyadmin/libraries/classes/InsertEdit.php | 3520 ++++++++++++ .../libraries/classes/InternalRelations.php | 505 ++ srcs/phpmyadmin/libraries/classes/IpAllowDeny.php | 336 ++ srcs/phpmyadmin/libraries/classes/Language.php | 204 + .../libraries/classes/LanguageManager.php | 975 ++++ srcs/phpmyadmin/libraries/classes/Linter.php | 186 + srcs/phpmyadmin/libraries/classes/ListAbstract.php | 107 + srcs/phpmyadmin/libraries/classes/ListDatabase.php | 177 + srcs/phpmyadmin/libraries/classes/Logging.php | 102 + srcs/phpmyadmin/libraries/classes/Menu.php | 680 +++ srcs/phpmyadmin/libraries/classes/Message.php | 812 +++ srcs/phpmyadmin/libraries/classes/Mime.php | 41 + srcs/phpmyadmin/libraries/classes/MultSubmits.php | 651 +++ .../libraries/classes/Navigation/Navigation.php | 280 + .../classes/Navigation/NavigationTree.php | 1581 ++++++ .../libraries/classes/Navigation/NodeFactory.php | 93 + .../libraries/classes/Navigation/Nodes/Node.php | 842 +++ .../classes/Navigation/Nodes/NodeColumn.php | 116 + .../Navigation/Nodes/NodeColumnContainer.php | 55 + .../classes/Navigation/Nodes/NodeDatabase.php | 717 +++ .../classes/Navigation/Nodes/NodeDatabaseChild.php | 62 + .../Nodes/NodeDatabaseChildContainer.php | 43 + .../Navigation/Nodes/NodeDatabaseContainer.php | 52 + .../classes/Navigation/Nodes/NodeEvent.php | 51 + .../Navigation/Nodes/NodeEventContainer.php | 52 + .../classes/Navigation/Nodes/NodeFunction.php | 53 + .../Navigation/Nodes/NodeFunctionContainer.php | 53 + .../classes/Navigation/Nodes/NodeIndex.php | 41 + .../Navigation/Nodes/NodeIndexContainer.php | 55 + .../classes/Navigation/Nodes/NodeProcedure.php | 53 + .../Navigation/Nodes/NodeProcedureContainer.php | 53 + .../classes/Navigation/Nodes/NodeTable.php | 310 ++ .../Navigation/Nodes/NodeTableContainer.php | 54 + .../classes/Navigation/Nodes/NodeTrigger.php | 41 + .../Navigation/Nodes/NodeTriggerContainer.php | 52 + .../classes/Navigation/Nodes/NodeView.php | 51 + .../classes/Navigation/Nodes/NodeViewContainer.php | 54 + .../phpmyadmin/libraries/classes/Normalization.php | 1105 ++++ srcs/phpmyadmin/libraries/classes/OpenDocument.php | 179 + srcs/phpmyadmin/libraries/classes/Operations.php | 2263 ++++++++ .../libraries/classes/OutputBuffering.php | 144 + srcs/phpmyadmin/libraries/classes/ParseAnalyze.php | 84 + srcs/phpmyadmin/libraries/classes/Partition.php | 270 + srcs/phpmyadmin/libraries/classes/Pdf.php | 178 + srcs/phpmyadmin/libraries/classes/Plugins.php | 633 +++ .../classes/Plugins/Auth/AuthenticationConfig.php | 172 + .../classes/Plugins/Auth/AuthenticationCookie.php | 964 ++++ .../classes/Plugins/Auth/AuthenticationHttp.php | 214 + .../classes/Plugins/Auth/AuthenticationSignon.php | 282 + .../classes/Plugins/AuthenticationPlugin.php | 371 ++ .../classes/Plugins/Export/ExportCodegen.php | 447 ++ .../libraries/classes/Plugins/Export/ExportCsv.php | 347 ++ .../classes/Plugins/Export/ExportExcel.php | 90 + .../classes/Plugins/Export/ExportHtmlword.php | 670 +++ .../classes/Plugins/Export/ExportJson.php | 295 + .../classes/Plugins/Export/ExportLatex.php | 709 +++ .../classes/Plugins/Export/ExportMediawiki.php | 386 ++ .../libraries/classes/Plugins/Export/ExportOds.php | 345 ++ .../libraries/classes/Plugins/Export/ExportOdt.php | 813 +++ .../libraries/classes/Plugins/Export/ExportPdf.php | 395 ++ .../classes/Plugins/Export/ExportPhparray.php | 259 + .../libraries/classes/Plugins/Export/ExportSql.php | 2915 ++++++++++ .../classes/Plugins/Export/ExportTexytext.php | 624 +++ .../libraries/classes/Plugins/Export/ExportXml.php | 593 ++ .../classes/Plugins/Export/ExportYaml.php | 230 + .../classes/Plugins/Export/Helpers/Pdf.php | 855 +++ .../Plugins/Export/Helpers/TableProperty.php | 277 + .../libraries/classes/Plugins/Export/README | 255 + .../libraries/classes/Plugins/ExportPlugin.php | 386 ++ .../classes/Plugins/IOTransformationsPlugin.php | 98 + .../classes/Plugins/Import/AbstractImportCsv.php | 94 + .../libraries/classes/Plugins/Import/ImportCsv.php | 818 +++ .../libraries/classes/Plugins/Import/ImportLdi.php | 176 + .../classes/Plugins/Import/ImportMediawiki.php | 604 +++ .../libraries/classes/Plugins/Import/ImportOds.php | 427 ++ .../libraries/classes/Plugins/Import/ImportShp.php | 335 ++ .../libraries/classes/Plugins/Import/ImportSql.php | 200 + .../libraries/classes/Plugins/Import/ImportXml.php | 375 ++ .../libraries/classes/Plugins/Import/README | 156 + .../classes/Plugins/Import/ShapeFileImport.php | 46 + .../classes/Plugins/Import/Upload/UploadApc.php | 83 + .../Plugins/Import/Upload/UploadNoplugin.php | 60 + .../Plugins/Import/Upload/UploadProgress.php | 97 + .../Plugins/Import/Upload/UploadSession.php | 95 + .../libraries/classes/Plugins/ImportPlugin.php | 96 + .../libraries/classes/Plugins/Schema/Dia/Dia.php | 190 + .../Plugins/Schema/Dia/DiaRelationSchema.php | 238 + .../Plugins/Schema/Dia/RelationStatsDia.php | 228 + .../classes/Plugins/Schema/Dia/TableStatsDia.php | 231 + .../libraries/classes/Plugins/Schema/Eps/Eps.php | 280 + .../Plugins/Schema/Eps/EpsRelationSchema.php | 254 + .../Plugins/Schema/Eps/RelationStatsEps.php | 120 + .../classes/Plugins/Schema/Eps/TableStatsEps.php | 183 + .../Plugins/Schema/ExportRelationSchema.php | 310 ++ .../libraries/classes/Plugins/Schema/Pdf/Pdf.php | 422 ++ .../Plugins/Schema/Pdf/PdfRelationSchema.php | 798 +++ .../Plugins/Schema/Pdf/RelationStatsPdf.php | 163 + .../classes/Plugins/Schema/Pdf/TableStatsPdf.php | 233 + .../classes/Plugins/Schema/RelationStats.php | 120 + .../libraries/classes/Plugins/Schema/SchemaDia.php | 100 + .../libraries/classes/Plugins/Schema/SchemaEps.php | 101 + .../libraries/classes/Plugins/Schema/SchemaPdf.php | 133 + .../libraries/classes/Plugins/Schema/SchemaSvg.php | 88 + .../Plugins/Schema/Svg/RelationStatsSvg.php | 140 + .../libraries/classes/Plugins/Schema/Svg/Svg.php | 281 + .../Plugins/Schema/Svg/SvgRelationSchema.php | 284 + .../classes/Plugins/Schema/Svg/TableStatsSvg.php | 204 + .../classes/Plugins/Schema/TableStats.php | 208 + .../libraries/classes/Plugins/SchemaPlugin.php | 90 + .../Abs/Bool2TextTransformationsPlugin.php | 69 + .../Abs/CodeMirrorEditorTransformationPlugin.php | 75 + .../Abs/DateFormatTransformationsPlugin.php | 158 + .../Abs/DownloadTransformationsPlugin.php | 93 + .../Abs/ExternalTransformationsPlugin.php | 160 + .../Abs/FormattedTransformationsPlugin.php | 65 + .../Abs/HexTransformationsPlugin.php | 71 + .../Abs/ImageLinkTransformationsPlugin.php | 63 + .../Abs/ImageUploadTransformationsPlugin.php | 121 + .../Abs/InlineTransformationsPlugin.php | 78 + .../Abs/LongToIPv4TransformationsPlugin.php | 66 + .../Abs/PreApPendTransformationsPlugin.php | 68 + .../Abs/RegexValidationTransformationsPlugin.php | 74 + .../Abs/SQLTransformationsPlugin.php | 62 + .../Abs/SubstringTransformationsPlugin.php | 93 + .../Abs/TextFileUploadTransformationsPlugin.php | 103 + .../Abs/TextImageLinkTransformationsPlugin.php | 75 + .../Abs/TextLinkTransformationsPlugin.php | 77 + .../Transformations/Input/Image_JPEG_Upload.php | 44 + .../Input/Text_Plain_FileUpload.php | 43 + .../Input/Text_Plain_Iptobinary.php | 141 + .../Input/Text_Plain_JsonEditor.php | 85 + .../Input/Text_Plain_RegexValidation.php | 44 + .../Transformations/Input/Text_Plain_SqlEditor.php | 85 + .../Transformations/Input/Text_Plain_XmlEditor.php | 85 + .../Output/Application_Octetstream_Download.php | 43 + .../Output/Application_Octetstream_Hex.php | 43 + .../Transformations/Output/Image_JPEG_Inline.php | 43 + .../Transformations/Output/Image_JPEG_Link.php | 43 + .../Transformations/Output/Image_PNG_Inline.php | 43 + .../Output/Text_Octetstream_Sql.php | 43 + .../Output/Text_Plain_Binarytoip.php | 97 + .../Output/Text_Plain_Bool2Text.php | 45 + .../Output/Text_Plain_Dateformat.php | 43 + .../Transformations/Output/Text_Plain_External.php | 43 + .../Output/Text_Plain_Formatted.php | 43 + .../Output/Text_Plain_Imagelink.php | 43 + .../Transformations/Output/Text_Plain_Json.php | 101 + .../Transformations/Output/Text_Plain_Sql.php | 60 + .../Transformations/Output/Text_Plain_Xml.php | 101 + .../classes/Plugins/Transformations/README | 4 + .../classes/Plugins/Transformations/TEMPLATE | 45 + .../Plugins/Transformations/TEMPLATE_ABSTRACT | 73 + .../Plugins/Transformations/Text_Plain_Link.php | 43 + .../Transformations/Text_Plain_Longtoipv4.php | 43 + .../Transformations/Text_Plain_PreApPend.php | 44 + .../Transformations/Text_Plain_Substring.php | 43 + .../classes/Plugins/TransformationsInterface.php | 47 + .../classes/Plugins/TransformationsPlugin.php | 69 + .../classes/Plugins/TwoFactor/Application.php | 162 + .../classes/Plugins/TwoFactor/Invalid.php | 68 + .../libraries/classes/Plugins/TwoFactor/Key.php | 213 + .../libraries/classes/Plugins/TwoFactor/Simple.php | 68 + .../libraries/classes/Plugins/TwoFactorPlugin.php | 183 + .../libraries/classes/Plugins/UploadInterface.php | 35 + .../Options/Groups/OptionsPropertyMainGroup.php | 35 + .../Options/Groups/OptionsPropertyRootGroup.php | 35 + .../Options/Groups/OptionsPropertySubgroup.php | 66 + .../Properties/Options/Items/BoolPropertyItem.php | 35 + .../Properties/Options/Items/DocPropertyItem.php | 35 + .../Options/Items/HiddenPropertyItem.php | 35 + .../Options/Items/MessageOnlyPropertyItem.php | 35 + .../Options/Items/NumberPropertyItem.php | 35 + .../Properties/Options/Items/RadioPropertyItem.php | 35 + .../Options/Items/SelectPropertyItem.php | 35 + .../Properties/Options/Items/TextPropertyItem.php | 35 + .../Properties/Options/OptionsPropertyGroup.php | 109 + .../Properties/Options/OptionsPropertyItem.php | 136 + .../Properties/Options/OptionsPropertyOneItem.php | 161 + .../Properties/Plugins/ExportPluginProperties.php | 64 + .../Properties/Plugins/ImportPluginProperties.php | 33 + .../Properties/Plugins/PluginPropertyItem.php | 177 + .../Properties/Plugins/SchemaPluginProperties.php | 46 + .../libraries/classes/Properties/PropertyItem.php | 48 + .../libraries/classes/RecentFavoriteTable.php | 405 ++ srcs/phpmyadmin/libraries/classes/Relation.php | 2280 ++++++++ .../libraries/classes/RelationCleanup.php | 392 ++ srcs/phpmyadmin/libraries/classes/Replication.php | 190 + .../libraries/classes/ReplicationGui.php | 602 +++ srcs/phpmyadmin/libraries/classes/Response.php | 614 +++ srcs/phpmyadmin/libraries/classes/Rte/Events.php | 680 +++ srcs/phpmyadmin/libraries/classes/Rte/Export.php | 168 + srcs/phpmyadmin/libraries/classes/Rte/Footer.php | 160 + srcs/phpmyadmin/libraries/classes/Rte/General.php | 118 + srcs/phpmyadmin/libraries/classes/Rte/Routines.php | 1743 ++++++ srcs/phpmyadmin/libraries/classes/Rte/RteList.php | 518 ++ srcs/phpmyadmin/libraries/classes/Rte/Triggers.php | 527 ++ srcs/phpmyadmin/libraries/classes/Rte/Words.php | 89 + srcs/phpmyadmin/libraries/classes/Sanitize.php | 469 ++ .../phpmyadmin/libraries/classes/SavedSearches.php | 466 ++ srcs/phpmyadmin/libraries/classes/Scripts.php | 164 + .../phpmyadmin/libraries/classes/Server/Plugin.php | 274 + .../libraries/classes/Server/Plugins.php | 74 + .../libraries/classes/Server/Privileges.php | 5649 +++++++++++++++++++ .../phpmyadmin/libraries/classes/Server/Select.php | 128 + .../libraries/classes/Server/Status/Data.php | 430 ++ .../libraries/classes/Server/Status/Monitor.php | 546 ++ .../libraries/classes/Server/UserGroups.php | 390 ++ srcs/phpmyadmin/libraries/classes/Server/Users.php | 64 + srcs/phpmyadmin/libraries/classes/Session.php | 234 + .../libraries/classes/Setup/ConfigGenerator.php | 184 + .../libraries/classes/Setup/FormProcessing.php | 77 + srcs/phpmyadmin/libraries/classes/Setup/Index.php | 198 + srcs/phpmyadmin/libraries/classes/Sql.php | 2328 ++++++++ srcs/phpmyadmin/libraries/classes/SqlQueryForm.php | 457 ++ .../phpmyadmin/libraries/classes/StorageEngine.php | 465 ++ srcs/phpmyadmin/libraries/classes/SubPartition.php | 182 + srcs/phpmyadmin/libraries/classes/SysInfo.php | 73 + srcs/phpmyadmin/libraries/classes/SysInfoBase.php | 50 + srcs/phpmyadmin/libraries/classes/SysInfoLinux.php | 103 + srcs/phpmyadmin/libraries/classes/SysInfoSunOS.php | 81 + srcs/phpmyadmin/libraries/classes/SysInfoWINNT.php | 135 + .../libraries/classes/SystemDatabase.php | 137 + srcs/phpmyadmin/libraries/classes/Table.php | 2771 ++++++++++ .../libraries/classes/TablePartitionDefinition.php | 200 + srcs/phpmyadmin/libraries/classes/Template.php | 142 + srcs/phpmyadmin/libraries/classes/Theme.php | 387 ++ srcs/phpmyadmin/libraries/classes/ThemeManager.php | 417 ++ srcs/phpmyadmin/libraries/classes/Tracker.php | 942 ++++ srcs/phpmyadmin/libraries/classes/Tracking.php | 1320 +++++ .../libraries/classes/Transformations.php | 485 ++ .../libraries/classes/Twig/CoreExtension.php | 41 + .../libraries/classes/Twig/I18n/NodeTrans.php | 171 + .../classes/Twig/I18n/TokenParserTrans.php | 85 + .../libraries/classes/Twig/I18nExtension.php | 45 + .../libraries/classes/Twig/MessageExtension.php | 54 + .../libraries/classes/Twig/PluginsExtension.php | 52 + .../libraries/classes/Twig/RelationExtension.php | 71 + .../libraries/classes/Twig/SanitizeExtension.php | 64 + .../classes/Twig/ServerPrivilegesExtension.php | 51 + .../classes/Twig/StorageEngineExtension.php | 37 + .../libraries/classes/Twig/TableExtension.php | 36 + .../libraries/classes/Twig/TrackerExtension.php | 36 + .../classes/Twig/TransformationsExtension.php | 48 + .../libraries/classes/Twig/UrlExtension.php | 52 + .../libraries/classes/Twig/UtilExtension.php | 212 + srcs/phpmyadmin/libraries/classes/TwoFactor.php | 303 ++ srcs/phpmyadmin/libraries/classes/Types.php | 875 +++ srcs/phpmyadmin/libraries/classes/Url.php | 274 + srcs/phpmyadmin/libraries/classes/UserPassword.php | 286 + .../libraries/classes/UserPreferences.php | 287 + .../libraries/classes/UserPreferencesHeader.php | 148 + srcs/phpmyadmin/libraries/classes/Util.php | 4975 +++++++++++++++++ .../libraries/classes/Utils/HttpRequest.php | 260 + .../libraries/classes/VersionInformation.php | 239 + srcs/phpmyadmin/libraries/classes/ZipExtension.php | 299 + srcs/phpmyadmin/libraries/common.inc.php | 489 ++ srcs/phpmyadmin/libraries/config.default.php | 3283 +++++++++++ srcs/phpmyadmin/libraries/config.values.php | 478 ++ srcs/phpmyadmin/libraries/db_common.inc.php | 161 + srcs/phpmyadmin/libraries/db_table_exists.inc.php | 114 + srcs/phpmyadmin/libraries/language_stats.inc.php | 97 + srcs/phpmyadmin/libraries/mult_submits.inc.php | 354 ++ srcs/phpmyadmin/libraries/replication.inc.php | 195 + srcs/phpmyadmin/libraries/server_common.inc.php | 45 + .../libraries/tbl_columns_definition_form.inc.php | 556 ++ srcs/phpmyadmin/libraries/tbl_common.inc.php | 58 + srcs/phpmyadmin/libraries/vendor_config.php | 82 + 419 files changed, 141927 insertions(+) create mode 100644 srcs/phpmyadmin/libraries/advisory_rules_generic.txt create mode 100644 srcs/phpmyadmin/libraries/advisory_rules_mysql_before80003.txt create mode 100644 srcs/phpmyadmin/libraries/certs/12d55845.0 create mode 100644 srcs/phpmyadmin/libraries/certs/2e5ac55d.0 create mode 100644 srcs/phpmyadmin/libraries/certs/4042bcee.0 create mode 100644 srcs/phpmyadmin/libraries/certs/6187b673.0 create mode 100644 srcs/phpmyadmin/libraries/certs/README.rst create mode 100644 srcs/phpmyadmin/libraries/certs/cacert.pem create mode 100644 srcs/phpmyadmin/libraries/classes/Advisor.php create mode 100644 srcs/phpmyadmin/libraries/classes/Bookmark.php create mode 100644 srcs/phpmyadmin/libraries/classes/BrowseForeigners.php create mode 100644 srcs/phpmyadmin/libraries/classes/CentralColumns.php create mode 100644 srcs/phpmyadmin/libraries/classes/Charsets.php create mode 100644 srcs/phpmyadmin/libraries/classes/Charsets/Charset.php create mode 100644 srcs/phpmyadmin/libraries/classes/Charsets/Collation.php create mode 100644 srcs/phpmyadmin/libraries/classes/CheckUserPrivileges.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/ConfigFile.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Descriptions.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Form.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/FormDisplay.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/FormDisplayTemplate.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/BaseForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/BaseFormList.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/Page/BrowseForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/Page/DbStructureForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/Page/EditForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/Page/ExportForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/Page/ImportForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/Page/NaviForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/Page/PageFormList.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/Page/SqlForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/Page/TableStructureForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/ConfigForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/ExportForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/FeaturesForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/ImportForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/MainForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/NaviForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/ServersForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/SetupFormList.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/SqlForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/User/ExportForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/User/FeaturesForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/User/ImportForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/User/MainForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/User/NaviForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/User/SqlForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Forms/User/UserFormList.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/PageSettings.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/ServerConfigChecks.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/SpecialSchemaLinks.php create mode 100644 srcs/phpmyadmin/libraries/classes/Config/Validator.php create mode 100644 srcs/phpmyadmin/libraries/classes/Console.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/AbstractController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/AjaxController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/BrowseForeignersController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Database/AbstractController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Database/CentralColumnsController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Database/DataDictionaryController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Database/EventsController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Database/MultiTableQueryController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Database/RoutinesController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Database/SqlController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Database/StructureController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Database/TriggersController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/HomeController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/BinlogController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/CollationsController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/DatabasesController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/EnginesController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/PluginsController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/ReplicationController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/SqlController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/AbstractController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/AdvisorController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/MonitorController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/ProcessesController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/QueriesController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/StatusController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/VariablesController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/VariablesController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Setup/AbstractController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Setup/ConfigController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Setup/FormController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Setup/HomeController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Setup/ServersController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Table/AbstractController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Table/ChartController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Table/GisVisualizationController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Table/IndexesController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Table/RelationController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Table/SearchController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Table/SqlController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Table/StructureController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/TransformationOverviewController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Core.php create mode 100644 srcs/phpmyadmin/libraries/classes/CreateAddField.php create mode 100644 srcs/phpmyadmin/libraries/classes/Database/DatabaseList.php create mode 100644 srcs/phpmyadmin/libraries/classes/Database/Designer.php create mode 100644 srcs/phpmyadmin/libraries/classes/Database/Designer/Common.php create mode 100644 srcs/phpmyadmin/libraries/classes/Database/Designer/DesignerTable.php create mode 100644 srcs/phpmyadmin/libraries/classes/Database/MultiTableQuery.php create mode 100644 srcs/phpmyadmin/libraries/classes/Database/Qbe.php create mode 100644 srcs/phpmyadmin/libraries/classes/Database/Search.php create mode 100644 srcs/phpmyadmin/libraries/classes/DatabaseInterface.php create mode 100644 srcs/phpmyadmin/libraries/classes/Dbi/DbiExtension.php create mode 100644 srcs/phpmyadmin/libraries/classes/Dbi/DbiMysqli.php create mode 100644 srcs/phpmyadmin/libraries/classes/Di/Migration.php create mode 100644 srcs/phpmyadmin/libraries/classes/Display/ChangePassword.php create mode 100644 srcs/phpmyadmin/libraries/classes/Display/CreateTable.php create mode 100644 srcs/phpmyadmin/libraries/classes/Display/Error.php create mode 100644 srcs/phpmyadmin/libraries/classes/Display/Export.php create mode 100644 srcs/phpmyadmin/libraries/classes/Display/GitRevision.php create mode 100644 srcs/phpmyadmin/libraries/classes/Display/Import.php create mode 100644 srcs/phpmyadmin/libraries/classes/Display/ImportAjax.php create mode 100644 srcs/phpmyadmin/libraries/classes/Display/Results.php create mode 100644 srcs/phpmyadmin/libraries/classes/Encoding.php create mode 100644 srcs/phpmyadmin/libraries/classes/Engines/Bdb.php create mode 100644 srcs/phpmyadmin/libraries/classes/Engines/Berkeleydb.php create mode 100644 srcs/phpmyadmin/libraries/classes/Engines/Binlog.php create mode 100644 srcs/phpmyadmin/libraries/classes/Engines/Innobase.php create mode 100644 srcs/phpmyadmin/libraries/classes/Engines/Innodb.php create mode 100644 srcs/phpmyadmin/libraries/classes/Engines/Memory.php create mode 100644 srcs/phpmyadmin/libraries/classes/Engines/Merge.php create mode 100644 srcs/phpmyadmin/libraries/classes/Engines/MrgMyisam.php create mode 100644 srcs/phpmyadmin/libraries/classes/Engines/Myisam.php create mode 100644 srcs/phpmyadmin/libraries/classes/Engines/Ndbcluster.php create mode 100644 srcs/phpmyadmin/libraries/classes/Engines/Pbxt.php create mode 100644 srcs/phpmyadmin/libraries/classes/Engines/PerformanceSchema.php create mode 100644 srcs/phpmyadmin/libraries/classes/Error.php create mode 100644 srcs/phpmyadmin/libraries/classes/ErrorHandler.php create mode 100644 srcs/phpmyadmin/libraries/classes/ErrorReport.php create mode 100644 srcs/phpmyadmin/libraries/classes/Export.php create mode 100644 srcs/phpmyadmin/libraries/classes/File.php create mode 100644 srcs/phpmyadmin/libraries/classes/FileListing.php create mode 100644 srcs/phpmyadmin/libraries/classes/Font.php create mode 100644 srcs/phpmyadmin/libraries/classes/Footer.php create mode 100644 srcs/phpmyadmin/libraries/classes/Gis/GisFactory.php create mode 100644 srcs/phpmyadmin/libraries/classes/Gis/GisGeometry.php create mode 100644 srcs/phpmyadmin/libraries/classes/Gis/GisGeometryCollection.php create mode 100644 srcs/phpmyadmin/libraries/classes/Gis/GisLineString.php create mode 100644 srcs/phpmyadmin/libraries/classes/Gis/GisMultiLineString.php create mode 100644 srcs/phpmyadmin/libraries/classes/Gis/GisMultiPoint.php create mode 100644 srcs/phpmyadmin/libraries/classes/Gis/GisMultiPolygon.php create mode 100644 srcs/phpmyadmin/libraries/classes/Gis/GisPoint.php create mode 100644 srcs/phpmyadmin/libraries/classes/Gis/GisPolygon.php create mode 100644 srcs/phpmyadmin/libraries/classes/Gis/GisVisualization.php create mode 100644 srcs/phpmyadmin/libraries/classes/Header.php create mode 100644 srcs/phpmyadmin/libraries/classes/Import.php create mode 100644 srcs/phpmyadmin/libraries/classes/Index.php create mode 100644 srcs/phpmyadmin/libraries/classes/IndexColumn.php create mode 100644 srcs/phpmyadmin/libraries/classes/InsertEdit.php create mode 100644 srcs/phpmyadmin/libraries/classes/InternalRelations.php create mode 100644 srcs/phpmyadmin/libraries/classes/IpAllowDeny.php create mode 100644 srcs/phpmyadmin/libraries/classes/Language.php create mode 100644 srcs/phpmyadmin/libraries/classes/LanguageManager.php create mode 100644 srcs/phpmyadmin/libraries/classes/Linter.php create mode 100644 srcs/phpmyadmin/libraries/classes/ListAbstract.php create mode 100644 srcs/phpmyadmin/libraries/classes/ListDatabase.php create mode 100644 srcs/phpmyadmin/libraries/classes/Logging.php create mode 100644 srcs/phpmyadmin/libraries/classes/Menu.php create mode 100644 srcs/phpmyadmin/libraries/classes/Message.php create mode 100644 srcs/phpmyadmin/libraries/classes/Mime.php create mode 100644 srcs/phpmyadmin/libraries/classes/MultSubmits.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Navigation.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/NavigationTree.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/NodeFactory.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/Node.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeColumn.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeColumnContainer.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeDatabase.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeDatabaseChild.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeDatabaseChildContainer.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeDatabaseContainer.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeEvent.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeEventContainer.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeFunction.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeFunctionContainer.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeIndex.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeIndexContainer.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeProcedure.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeProcedureContainer.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeTable.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeTableContainer.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeTrigger.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeTriggerContainer.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeView.php create mode 100644 srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeViewContainer.php create mode 100644 srcs/phpmyadmin/libraries/classes/Normalization.php create mode 100644 srcs/phpmyadmin/libraries/classes/OpenDocument.php create mode 100644 srcs/phpmyadmin/libraries/classes/Operations.php create mode 100644 srcs/phpmyadmin/libraries/classes/OutputBuffering.php create mode 100644 srcs/phpmyadmin/libraries/classes/ParseAnalyze.php create mode 100644 srcs/phpmyadmin/libraries/classes/Partition.php create mode 100644 srcs/phpmyadmin/libraries/classes/Pdf.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationConfig.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationCookie.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationHttp.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationSignon.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/AuthenticationPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportCodegen.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportCsv.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportExcel.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportHtmlword.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportJson.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportLatex.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportMediawiki.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportOds.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportOdt.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportPdf.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportPhparray.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportSql.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportTexytext.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportXml.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportYaml.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Export/Helpers/Pdf.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Export/Helpers/TableProperty.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Export/README create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/ExportPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/IOTransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Import/AbstractImportCsv.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportCsv.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportLdi.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportMediawiki.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportOds.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportShp.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportSql.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportXml.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Import/README create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Import/ShapeFileImport.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadApc.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadNoplugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadProgress.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadSession.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/ImportPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/Dia/Dia.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/Dia/DiaRelationSchema.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/Dia/RelationStatsDia.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/Dia/TableStatsDia.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/Eps/Eps.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/Eps/EpsRelationSchema.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/Eps/RelationStatsEps.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/Eps/TableStatsEps.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/ExportRelationSchema.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/Pdf/Pdf.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/Pdf/PdfRelationSchema.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/Pdf/RelationStatsPdf.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/Pdf/TableStatsPdf.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/RelationStats.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/SchemaDia.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/SchemaEps.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/SchemaPdf.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/SchemaSvg.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/Svg/RelationStatsSvg.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/Svg/Svg.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/Svg/SvgRelationSchema.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/Svg/TableStatsSvg.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Schema/TableStats.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/SchemaPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/Bool2TextTransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/CodeMirrorEditorTransformationPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/DateFormatTransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/DownloadTransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/ExternalTransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/FormattedTransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/HexTransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/ImageLinkTransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/ImageUploadTransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/InlineTransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/LongToIPv4TransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/PreApPendTransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/RegexValidationTransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/SQLTransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/SubstringTransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/TextFileUploadTransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/TextImageLinkTransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/TextLinkTransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Input/Image_JPEG_Upload.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Input/Text_Plain_FileUpload.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Input/Text_Plain_Iptobinary.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Input/Text_Plain_JsonEditor.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Input/Text_Plain_RegexValidation.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Input/Text_Plain_SqlEditor.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Input/Text_Plain_XmlEditor.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Application_Octetstream_Download.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Application_Octetstream_Hex.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Image_JPEG_Inline.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Image_JPEG_Link.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Image_PNG_Inline.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Text_Octetstream_Sql.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Text_Plain_Binarytoip.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Text_Plain_Bool2Text.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Text_Plain_Dateformat.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Text_Plain_External.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Text_Plain_Formatted.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Text_Plain_Imagelink.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Text_Plain_Json.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Text_Plain_Sql.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Text_Plain_Xml.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/README create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/TEMPLATE create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/TEMPLATE_ABSTRACT create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Text_Plain_Link.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Text_Plain_Longtoipv4.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Text_Plain_PreApPend.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Text_Plain_Substring.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/TransformationsInterface.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/TransformationsPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/TwoFactor/Application.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/TwoFactor/Invalid.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/TwoFactor/Key.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/TwoFactor/Simple.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/TwoFactorPlugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Plugins/UploadInterface.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/Options/Groups/OptionsPropertyMainGroup.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/Options/Groups/OptionsPropertyRootGroup.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/Options/Groups/OptionsPropertySubgroup.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/Options/Items/BoolPropertyItem.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/Options/Items/DocPropertyItem.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/Options/Items/HiddenPropertyItem.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/Options/Items/MessageOnlyPropertyItem.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/Options/Items/NumberPropertyItem.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/Options/Items/RadioPropertyItem.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/Options/Items/SelectPropertyItem.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/Options/Items/TextPropertyItem.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/Options/OptionsPropertyGroup.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/Options/OptionsPropertyItem.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/Options/OptionsPropertyOneItem.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/Plugins/ExportPluginProperties.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/Plugins/ImportPluginProperties.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/Plugins/PluginPropertyItem.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/Plugins/SchemaPluginProperties.php create mode 100644 srcs/phpmyadmin/libraries/classes/Properties/PropertyItem.php create mode 100644 srcs/phpmyadmin/libraries/classes/RecentFavoriteTable.php create mode 100644 srcs/phpmyadmin/libraries/classes/Relation.php create mode 100644 srcs/phpmyadmin/libraries/classes/RelationCleanup.php create mode 100644 srcs/phpmyadmin/libraries/classes/Replication.php create mode 100644 srcs/phpmyadmin/libraries/classes/ReplicationGui.php create mode 100644 srcs/phpmyadmin/libraries/classes/Response.php create mode 100644 srcs/phpmyadmin/libraries/classes/Rte/Events.php create mode 100644 srcs/phpmyadmin/libraries/classes/Rte/Export.php create mode 100644 srcs/phpmyadmin/libraries/classes/Rte/Footer.php create mode 100644 srcs/phpmyadmin/libraries/classes/Rte/General.php create mode 100644 srcs/phpmyadmin/libraries/classes/Rte/Routines.php create mode 100644 srcs/phpmyadmin/libraries/classes/Rte/RteList.php create mode 100644 srcs/phpmyadmin/libraries/classes/Rte/Triggers.php create mode 100644 srcs/phpmyadmin/libraries/classes/Rte/Words.php create mode 100644 srcs/phpmyadmin/libraries/classes/Sanitize.php create mode 100644 srcs/phpmyadmin/libraries/classes/SavedSearches.php create mode 100644 srcs/phpmyadmin/libraries/classes/Scripts.php create mode 100644 srcs/phpmyadmin/libraries/classes/Server/Plugin.php create mode 100644 srcs/phpmyadmin/libraries/classes/Server/Plugins.php create mode 100644 srcs/phpmyadmin/libraries/classes/Server/Privileges.php create mode 100644 srcs/phpmyadmin/libraries/classes/Server/Select.php create mode 100644 srcs/phpmyadmin/libraries/classes/Server/Status/Data.php create mode 100644 srcs/phpmyadmin/libraries/classes/Server/Status/Monitor.php create mode 100644 srcs/phpmyadmin/libraries/classes/Server/UserGroups.php create mode 100644 srcs/phpmyadmin/libraries/classes/Server/Users.php create mode 100644 srcs/phpmyadmin/libraries/classes/Session.php create mode 100644 srcs/phpmyadmin/libraries/classes/Setup/ConfigGenerator.php create mode 100644 srcs/phpmyadmin/libraries/classes/Setup/FormProcessing.php create mode 100644 srcs/phpmyadmin/libraries/classes/Setup/Index.php create mode 100644 srcs/phpmyadmin/libraries/classes/Sql.php create mode 100644 srcs/phpmyadmin/libraries/classes/SqlQueryForm.php create mode 100644 srcs/phpmyadmin/libraries/classes/StorageEngine.php create mode 100644 srcs/phpmyadmin/libraries/classes/SubPartition.php create mode 100644 srcs/phpmyadmin/libraries/classes/SysInfo.php create mode 100644 srcs/phpmyadmin/libraries/classes/SysInfoBase.php create mode 100644 srcs/phpmyadmin/libraries/classes/SysInfoLinux.php create mode 100644 srcs/phpmyadmin/libraries/classes/SysInfoSunOS.php create mode 100644 srcs/phpmyadmin/libraries/classes/SysInfoWINNT.php create mode 100644 srcs/phpmyadmin/libraries/classes/SystemDatabase.php create mode 100644 srcs/phpmyadmin/libraries/classes/Table.php create mode 100644 srcs/phpmyadmin/libraries/classes/TablePartitionDefinition.php create mode 100644 srcs/phpmyadmin/libraries/classes/Template.php create mode 100644 srcs/phpmyadmin/libraries/classes/Theme.php create mode 100644 srcs/phpmyadmin/libraries/classes/ThemeManager.php create mode 100644 srcs/phpmyadmin/libraries/classes/Tracker.php create mode 100644 srcs/phpmyadmin/libraries/classes/Tracking.php create mode 100644 srcs/phpmyadmin/libraries/classes/Transformations.php create mode 100644 srcs/phpmyadmin/libraries/classes/Twig/CoreExtension.php create mode 100644 srcs/phpmyadmin/libraries/classes/Twig/I18n/NodeTrans.php create mode 100644 srcs/phpmyadmin/libraries/classes/Twig/I18n/TokenParserTrans.php create mode 100644 srcs/phpmyadmin/libraries/classes/Twig/I18nExtension.php create mode 100644 srcs/phpmyadmin/libraries/classes/Twig/MessageExtension.php create mode 100644 srcs/phpmyadmin/libraries/classes/Twig/PluginsExtension.php create mode 100644 srcs/phpmyadmin/libraries/classes/Twig/RelationExtension.php create mode 100644 srcs/phpmyadmin/libraries/classes/Twig/SanitizeExtension.php create mode 100644 srcs/phpmyadmin/libraries/classes/Twig/ServerPrivilegesExtension.php create mode 100644 srcs/phpmyadmin/libraries/classes/Twig/StorageEngineExtension.php create mode 100644 srcs/phpmyadmin/libraries/classes/Twig/TableExtension.php create mode 100644 srcs/phpmyadmin/libraries/classes/Twig/TrackerExtension.php create mode 100644 srcs/phpmyadmin/libraries/classes/Twig/TransformationsExtension.php create mode 100644 srcs/phpmyadmin/libraries/classes/Twig/UrlExtension.php create mode 100644 srcs/phpmyadmin/libraries/classes/Twig/UtilExtension.php create mode 100644 srcs/phpmyadmin/libraries/classes/TwoFactor.php create mode 100644 srcs/phpmyadmin/libraries/classes/Types.php create mode 100644 srcs/phpmyadmin/libraries/classes/Url.php create mode 100644 srcs/phpmyadmin/libraries/classes/UserPassword.php create mode 100644 srcs/phpmyadmin/libraries/classes/UserPreferences.php create mode 100644 srcs/phpmyadmin/libraries/classes/UserPreferencesHeader.php create mode 100644 srcs/phpmyadmin/libraries/classes/Util.php create mode 100644 srcs/phpmyadmin/libraries/classes/Utils/HttpRequest.php create mode 100644 srcs/phpmyadmin/libraries/classes/VersionInformation.php create mode 100644 srcs/phpmyadmin/libraries/classes/ZipExtension.php create mode 100644 srcs/phpmyadmin/libraries/common.inc.php create mode 100644 srcs/phpmyadmin/libraries/config.default.php create mode 100644 srcs/phpmyadmin/libraries/config.values.php create mode 100644 srcs/phpmyadmin/libraries/db_common.inc.php create mode 100644 srcs/phpmyadmin/libraries/db_table_exists.inc.php create mode 100644 srcs/phpmyadmin/libraries/language_stats.inc.php create mode 100644 srcs/phpmyadmin/libraries/mult_submits.inc.php create mode 100644 srcs/phpmyadmin/libraries/replication.inc.php create mode 100644 srcs/phpmyadmin/libraries/server_common.inc.php create mode 100644 srcs/phpmyadmin/libraries/tbl_columns_definition_form.inc.php create mode 100644 srcs/phpmyadmin/libraries/tbl_common.inc.php create mode 100644 srcs/phpmyadmin/libraries/vendor_config.php (limited to 'srcs/phpmyadmin/libraries') diff --git a/srcs/phpmyadmin/libraries/advisory_rules_generic.txt b/srcs/phpmyadmin/libraries/advisory_rules_generic.txt new file mode 100644 index 0000000..5fd8b5a --- /dev/null +++ b/srcs/phpmyadmin/libraries/advisory_rules_generic.txt @@ -0,0 +1,402 @@ +# phpMyAdmin Advisory rules file +# +# Use only UNIX style newlines +# +# This file is being parsed by Advisor.php, which should handle syntax +# errors correctly. However, PHP Warnings and the like are being consumed by +# the phpMyAdmin error handler, so those won't show up E.g.: Justification line +# is empty because you used an unescape percent sign, sprintf() returns an +# empty string and no warning/error is shown +# +# Rule Syntax: +# 'rule' identifier[the name of the rule] eexpr [an optional precondition] +# expr [variable or value calculation used for the test] +# expr [test, if evaluted to 'true' it fires the rule. Use 'value' to insert the calculated value (without quotes)] +# string [the issue (what is the problem?)] +# string [the recommendation (how do i fix it?)] +# formatted-string '|' comma-seperated-expr [the justification (result of the calculated value / why did this rule fire?)] + +# comma-seperated-expr: expr(,expr)* +# eexpr: [expr] - expr enclosed in [] +# expr: a php code literal with extras: +# - variable names are replaced with their respective values +# - fired('name of rule') is replaced with true/false when given rule has +# been fired. Note however that this is a very simple rules engine. +# Rules are only checked in sequential order as they are written down +# here. If given rule has not been checked yet, fired() will always +# evaluate to false +# - 'value' is replaced with the calculated value. If it is a string, it +# will be put within single quotes +# - other than that you may use any php function, initialized variable or +# constant +# +# identifier: A string enclosed in single quotes +# string: A quoteless string, may contain HTML. Variable names enclosed in +# curly braces are replaced with links to directly edit this variable. +# e.g. {tmp_table_size} +# formatted-string: You may use classic php sprintf() string formatting here, +# the arguments must be appended after a trailing pipe (|) as +# mentioned in above syntax percent signs (%) are +# automatically escaped (%%) in the following cases: When +# followed by a space, dot or comma and at the end of the +# line) +# +# Comments start with # +# + +# Queries + +rule 'Uptime below one day' + Uptime + value < 86400 + Uptime is less than 1 day, performance tuning may not be accurate. + To have more accurate averages it is recommended to let the server run for longer than a day before running this analyzer + The uptime is only %s | ADVISOR_timespanFormat(Uptime) + +rule 'Questions below 1,000' + Questions + value < 1000 + Fewer than 1,000 questions have been run against this server. The recommendations may not be accurate. + Let the server run for a longer time until it has executed a greater amount of queries. + Current amount of Questions: %s | Questions + +rule 'Percentage of slow queries' [Questions > 0] + Slow_queries / Questions * 100 + value >= 5 + There is a lot of slow queries compared to the overall amount of Queries. + You might want to increase {long_query_time} or optimize the queries listed in the slow query log + The slow query rate should be below 5%, your value is %s%. | round(value,2) + +rule 'Slow query rate' [Questions > 0] + (Slow_queries / Questions * 100) / Uptime + value * 60 * 60 > 1 + There is a high percentage of slow queries compared to the server uptime. + You might want to increase {long_query_time} or optimize the queries listed in the slow query log + You have a slow query rate of %s per hour, you should have less than 1% per hour. | ADVISOR_bytime(value,2) + +rule 'Long query time' + long_query_time + value >= 10 + {long_query_time} is set to 10 seconds or more, thus only slow queries that take above 10 seconds are logged. + It is suggested to set {long_query_time} to a lower value, depending on your environment. Usually a value of 1-5 seconds is suggested. + long_query_time is currently set to %ds. | value + +rule 'Slow query logging' [PMA_MYSQL_INT_VERSION < 50600] + log_slow_queries + value == 'OFF' + The slow query log is disabled. + Enable slow query logging by setting {log_slow_queries} to 'ON'. This will help troubleshooting badly performing queries. + log_slow_queries is set to 'OFF' + +rule 'Slow query logging' [PMA_MYSQL_INT_VERSION >= 50600] + slow_query_log + value == 'OFF' + The slow query log is disabled. + Enable slow query logging by setting {slow_query_log} to 'ON'. This will help troubleshooting badly performing queries. + slow_query_log is set to 'OFF' + +# +# versions +rule 'Release Series' + version + substr(value,0,2) <= '5.' && substr(value,2,1) < 1 + The MySQL server version less than 5.1. + You should upgrade, as MySQL 5.1 has improved performance, and MySQL 5.5 even more so. + Current version: %s | value + +rule 'Minor Version' [! fired('Release Series')] + version + substr(value,0,2) <= '5.' && substr(value,2,1) <= 1 && substr(value,4,2) < 30 + Version less than 5.1.30 (the first GA release of 5.1). + You should upgrade, as recent versions of MySQL 5.1 have improved performance and MySQL 5.5 even more so. + Current version: %s | value + +rule 'Minor Version' [! fired('Release Series')] + version + substr(value,0,1) == 5 && substr(value,2,1) == 5 && substr(value,4,2) < 8 + Version less than 5.5.8 (the first GA release of 5.5). + You should upgrade, to a stable version of MySQL 5.5. + Current version: %s | value + +rule 'Distribution' + version_comment + preg_match('/source/i',value) + Version is compiled from source, not a MySQL official binary. + If you did not compile from source, you may be using a package modified by a distribution. The MySQL manual only is accurate for official MySQL binaries, not any package distributions (such as RedHat, Debian/Ubuntu etc). + 'source' found in version_comment + +rule 'Distribution' + version_comment + preg_match('/percona/i',value) + The MySQL manual only is accurate for official MySQL binaries. + Percona documentation is at https://www.percona.com/software/documentation/ + 'percona' found in version_comment + +rule 'MySQL Architecture' + system_memory + value > 3072*1024 && !preg_match('/64/',version_compile_machine) && !preg_match('/64/',version_compile_os) + MySQL is not compiled as a 64-bit package. + Your memory capacity is above 3 GiB (assuming the Server is on localhost), so MySQL might not be able to access all of your memory. You might want to consider installing the 64-bit version of MySQL. + Available memory on this host: %s | ADVISOR_formatByteDown(value*1024, 2, 2) + +# +# Query cache + +rule 'Query caching method' [!fired('Query cache disabled')] + Questions / Uptime + value > 100 + Suboptimal caching method. + You are using the MySQL Query cache with a fairly high traffic database. It might be worth considering to use memcached instead of the MySQL Query cache, especially if you have multiple slaves. + The query cache is enabled and the server receives %d queries per second. This rule fires if there is more than 100 queries per second. | round(value,1) + +# +# Sorts +rule 'Percentage of sorts that cause temporary tables' [Sort_scan + Sort_range > 0] + Sort_merge_passes / (Sort_scan + Sort_range) * 100 + value > 10 + Too many sorts are causing temporary tables. + Consider increasing {sort_buffer_size} and/or {read_rnd_buffer_size}, depending on your system memory limits. + %s% of all sorts cause temporary tables, this value should be lower than 10%. | round(value,1) + +rule 'Rate of sorts that cause temporary tables' + Sort_merge_passes / Uptime + value * 60 * 60 > 1 + Too many sorts are causing temporary tables. + Consider increasing {sort_buffer_size} and/or {read_rnd_buffer_size}, depending on your system memory limits. + Temporary tables average: %s, this value should be less than 1 per hour. | ADVISOR_bytime(value,2) + +rule 'Sort rows' + Sort_rows / Uptime + value * 60 >= 1 + There are lots of rows being sorted. + While there is nothing wrong with a high amount of row sorting, you might want to make sure that the queries which require a lot of sorting use indexed columns in the ORDER BY clause, as this will result in much faster sorting. + Sorted rows average: %s | ADVISOR_bytime(value,2) + +# Joins, scans +rule 'Rate of joins without indexes' + (Select_range_check + Select_scan + Select_full_join) / Uptime + value * 60 * 60 > 1 + There are too many joins without indexes. + This means that joins are doing full table scans. Adding indexes for the columns being used in the join conditions will greatly speed up table joins. + Table joins average: %s, this value should be less than 1 per hour | ADVISOR_bytime(value,2) + +rule 'Rate of reading first index entry' + Handler_read_first / Uptime + value * 60 * 60 > 1 + The rate of reading the first index entry is high. + This usually indicates frequent full index scans. Full index scans are faster than table scans but require lots of CPU cycles in big tables, if those tables that have or had high volumes of UPDATEs and DELETEs, running 'OPTIMIZE TABLE' might reduce the amount of and/or speed up full index scans. Other than that full index scans can only be reduced by rewriting queries. + Index scans average: %s, this value should be less than 1 per hour | ADVISOR_bytime(value,2) + +rule 'Rate of reading fixed position' + Handler_read_rnd / Uptime + value * 60 * 60 > 1 + The rate of reading data from a fixed position is high. + This indicates that many queries need to sort results and/or do a full table scan, including join queries that do not use indexes. Add indexes where applicable. + Rate of reading fixed position average: %s, this value should be less than 1 per hour | ADVISOR_bytime(value,2) + +rule 'Rate of reading next table row' + Handler_read_rnd_next / Uptime + value * 60 * 60 > 1 + The rate of reading the next table row is high. + This indicates that many queries are doing full table scans. Add indexes where applicable. + Rate of reading next table row: %s, this value should be less than 1 per hour | ADVISOR_bytime(value,2) + +# temp tables +rule 'Different tmp_table_size and max_heap_table_size' + tmp_table_size - max_heap_table_size + value !=0 + {tmp_table_size} and {max_heap_table_size} are not the same. + If you have deliberately changed one of either: The server uses the lower value of either to determine the maximum size of in-memory tables. So if you wish to increase the in-memory table limit you will have to increase the other value as well. + Current values are tmp_table_size: %s, max_heap_table_size: %s | ADVISOR_formatByteDown(tmp_table_size, 2, 2), ADVISOR_formatByteDown(max_heap_table_size, 2, 2) + +rule 'Percentage of temp tables on disk' [Created_tmp_tables + Created_tmp_disk_tables > 0] + Created_tmp_disk_tables / (Created_tmp_tables + Created_tmp_disk_tables) * 100 + value > 25 + Many temporary tables are being written to disk instead of being kept in memory. + Increasing {max_heap_table_size} and {tmp_table_size} might help. However some temporary tables are always being written to disk, independent of the value of these variables. To eliminate these you will have to rewrite your queries to avoid those conditions (Within a temporary table: Presence of a BLOB or TEXT column or presence of a column bigger than 512 bytes) as mentioned in the beginning of an Article by the Pythian Group + %s% of all temporary tables are being written to disk, this value should be below 25% | round(value,1) + +rule 'Temp disk rate' [!fired('Percentage of temp tables on disk')] + Created_tmp_disk_tables / Uptime + value * 60 * 60 > 1 + Many temporary tables are being written to disk instead of being kept in memory. + Increasing {max_heap_table_size} and {tmp_table_size} might help. However some temporary tables are always being written to disk, independent of the value of these variables. To eliminate these you will have to rewrite your queries to avoid those conditions (Within a temporary table: Presence of a BLOB or TEXT column or presence of a column bigger than 512 bytes) as mentioned in the MySQL Documentation + Rate of temporary tables being written to disk: %s, this value should be less than 1 per hour | ADVISOR_bytime(value,2) + +# +# MyISAM index cache +rule 'MyISAM key buffer size' + key_buffer_size + value == 0 + Key buffer is not initialized. No MyISAM indexes will be cached. + Set {key_buffer_size} depending on the size of your MyISAM indexes. 64M is a good start. + key_buffer_size is 0 + +rule 'Max % MyISAM key buffer ever used' [key_buffer_size > 0] + Key_blocks_used * key_cache_block_size / key_buffer_size * 100 + value < 95 + MyISAM key buffer (index cache) % used is low. + You may need to decrease the size of {key_buffer_size}, re-examine your tables to see if indexes have been removed, or examine queries and expectations about what indexes are being used. + max % MyISAM key buffer ever used: %s%, this value should be above 95% | round(value,1) + +# Don't fire if above rule fired - we don't need the same advice twice +rule 'Percentage of MyISAM key buffer used' [key_buffer_size > 0 && !fired('Max % MyISAM key buffer ever used')] + ( 1 - Key_blocks_unused * key_cache_block_size / key_buffer_size) * 100 + value < 95 + MyISAM key buffer (index cache) % used is low. + You may need to decrease the size of {key_buffer_size}, re-examine your tables to see if indexes have been removed, or examine queries and expectations about what indexes are being used. + % MyISAM key buffer used: %s%, this value should be above 95% | round(value,1) + +rule 'Percentage of index reads from memory' [Key_read_requests > 0] + 100 - (Key_reads / Key_read_requests * 100) + value < 95 + The % of indexes that use the MyISAM key buffer is low. + You may need to increase {key_buffer_size}. + Index reads from memory: %s%, this value should be above 95% | round(value,1) + +# +# other caches +rule 'Rate of table open' + Opened_tables / Uptime + value*60*60 > 10 + The rate of opening tables is high. + Opening tables requires disk I/O which is costly. Increasing {table_open_cache} might avoid this. + Opened table rate: %s, this value should be less than 10 per hour | ADVISOR_bytime(value,2) + +rule 'Percentage of used open files limit' + Open_files / open_files_limit * 100 + value > 85 + The number of open files is approaching the max number of open files. You may get a "Too many open files" error. + Consider increasing {open_files_limit}, and check the error log when restarting after changing {open_files_limit}. + The number of opened files is at %s% of the limit. It should be below 85% | round(value,1) + +rule 'Rate of open files' + Open_files / Uptime + value * 60 * 60 > 5 + The rate of opening files is high. + Consider increasing {open_files_limit}, and check the error log when restarting after changing {open_files_limit}. + Opened files rate: %s, this value should be less than 5 per hour | ADVISOR_bytime(value,2) + +rule 'Immediate table locks %' [Table_locks_waited + Table_locks_immediate > 0] + Table_locks_immediate / (Table_locks_waited + Table_locks_immediate) * 100 + value < 95 + Too many table locks were not granted immediately. + Optimize queries and/or use InnoDB to reduce lock wait. + Immediate table locks: %s%, this value should be above 95% | round(value,1) + +rule 'Table lock wait rate' + Table_locks_waited / Uptime + value * 60 * 60 > 1 + Too many table locks were not granted immediately. + Optimize queries and/or use InnoDB to reduce lock wait. + Table lock wait rate: %s, this value should be less than 1 per hour | ADVISOR_bytime(value,2) + +rule 'Thread cache' + thread_cache_size + value < 1 + Thread cache is disabled, resulting in more overhead from new connections to MySQL. + Enable the thread cache by setting {thread_cache_size} > 0. + The thread cache is set to 0 + +rule 'Thread cache hit rate %' [thread_cache_size > 0] + 100 - Threads_created / Connections + value < 80 + Thread cache is not efficient. + Increase {thread_cache_size}. + Thread cache hitrate: %s%, this value should be above 80% | round(value,1) + +rule 'Threads that are slow to launch' [slow_launch_time > 0] + Slow_launch_threads + value > 0 + There are too many threads that are slow to launch. + This generally happens in case of general system overload as it is pretty simple operations. You might want to monitor your system load carefully. + %s thread(s) took longer than %s seconds to start, it should be 0 | value, slow_launch_time + +rule 'Slow launch time' + slow_launch_time + value > 2 + Slow_launch_time is above 2s. + Set {slow_launch_time} to 1s or 2s to correctly count threads that are slow to launch. + slow_launch_time is set to %s | value + +# +#Connections +rule 'Percentage of used connections' + Max_used_connections / max_connections * 100 + value > 80 + The maximum amount of used connections is getting close to the value of {max_connections}. + Increase {max_connections}, or decrease {wait_timeout} so that connections that do not close database handlers properly get killed sooner. Make sure the code closes database handlers properly. + Max_used_connections is at %s% of max_connections, it should be below 80% | round(value,1) + +rule 'Percentage of aborted connections' + Aborted_connects / Connections * 100 + value > 1 + Too many connections are aborted. + Connections are usually aborted when they cannot be authorized. This article might help you track down the source. + %s% of all connections are aborted. This value should be below 1% | round(value,1) + +rule 'Rate of aborted connections' + Aborted_connects / Uptime + value * 60 * 60 > 1 + Too many connections are aborted. + Connections are usually aborted when they cannot be authorized. This article might help you track down the source. + Aborted connections rate is at %s, this value should be less than 1 per hour | ADVISOR_bytime(value,2) + +rule 'Percentage of aborted clients' + Aborted_clients / Connections * 100 + value > 2 + Too many clients are aborted. + Clients are usually aborted when they did not close their connection to MySQL properly. This can be due to network issues or code not closing a database handler properly. Check your network and code. + %s% of all clients are aborted. This value should be below 2% | round(value,1) + +rule 'Rate of aborted clients' + Aborted_clients / Uptime + value * 60 * 60 > 1 + Too many clients are aborted. + Clients are usually aborted when they did not close their connection to MySQL properly. This can be due to network issues or code not closing a database handler properly. Check your network and code. + Aborted client rate is at %s, this value should be less than 1 per hour | ADVISOR_bytime(value,2) + +# +# InnoDB +rule 'Is InnoDB disabled?' [PMA_MYSQL_INT_VERSION < 50600] + have_innodb + value != "YES" + You do not have InnoDB enabled. + InnoDB is usually the better choice for table engines. + have_innodb is set to 'value' + +rule 'InnoDB log size' [innodb_buffer_pool_size > 0] + (innodb_log_file_size * innodb_log_files_in_group)/ innodb_buffer_pool_size * 100 + value < 20 && innodb_log_file_size / (1024 * 1024) < 256 + The InnoDB log file size is not an appropriate size, in relation to the InnoDB buffer pool. + Especially on a system with a lot of writes to InnoDB tables you should set {innodb_log_file_size} to 25% of {innodb_buffer_pool_size}. However the bigger this value, the longer the recovery time will be when database crashes, so this value should not be set much higher than 256 MiB. Please note however that you cannot simply change the value of this variable. You need to shutdown the server, remove the InnoDB log files, set the new value in my.cnf, start the server, then check the error logs if everything went fine. See also this blog entry + Your InnoDB log size is at %s% in relation to the InnoDB buffer pool size, it should not be below 20% | round(value,1) + +rule 'Max InnoDB log size' [innodb_buffer_pool_size > 0 && innodb_log_file_size / innodb_buffer_pool_size * 100 < 30] + innodb_log_file_size / (1024 * 1024) + value > 256 + The InnoDB log file size is inadequately large. + It is usually sufficient to set {innodb_log_file_size} to 25% of the size of {innodb_buffer_pool_size}. A very big {innodb_log_file_size} slows down the recovery time after a database crash considerably. See also this Article. You need to shutdown the server, remove the InnoDB log files, set the new value in my.cnf, start the server, then check the error logs if everything went fine. See also this blog entry + Your absolute InnoDB log size is %s MiB | round(value,1) + +rule 'InnoDB buffer pool size' [system_memory > 0] + innodb_buffer_pool_size / system_memory * 100 + value < 60 + Your InnoDB buffer pool is fairly small. + The InnoDB buffer pool has a profound impact on performance for InnoDB tables. Assign all your remaining memory to this buffer. For database servers that use solely InnoDB as storage engine and have no other services (e.g. a web server) running, you may set this as high as 80% of your available memory. If that is not the case, you need to carefully assess the memory consumption of your other services and non-InnoDB-Tables and set this variable accordingly. If it is set too high, your system will start swapping, which decreases performance significantly. See also this article + You are currently using %s% of your memory for the InnoDB buffer pool. This rule fires if you are assigning less than 60%, however this might be perfectly adequate for your system if you don't have much InnoDB tables or other services running on the same machine. | value + +# +# other +rule 'MyISAM concurrent inserts' + concurrent_insert + value === 0 || value === 'NEVER' + Enable {concurrent_insert} by setting it to 1 + Setting {concurrent_insert} to 1 reduces contention between readers and writers for a given table. See also MySQL Documentation + concurrent_insert is set to 0 + +# INSERT DELAYED USAGE +#Delayed_errors 0 +#Delayed_insert_threads 0 +#Delayed_writes 0 +#Not_flushed_delayed_rows diff --git a/srcs/phpmyadmin/libraries/advisory_rules_mysql_before80003.txt b/srcs/phpmyadmin/libraries/advisory_rules_mysql_before80003.txt new file mode 100644 index 0000000..e28b7f4 --- /dev/null +++ b/srcs/phpmyadmin/libraries/advisory_rules_mysql_before80003.txt @@ -0,0 +1,57 @@ +# phpMyAdmin Advisory rules file +# +# See doc in advisory_rules_generic.txt +# + +# +# Query cache + +# Lame: 'ON' == 0 is true, so you need to compare 'ON' == '0' +rule 'Query cache disabled' + query_cache_size + value == 0 || query_cache_type == 'OFF' || query_cache_type == '0' + The query cache is not enabled. + The query cache is known to greatly improve performance if configured correctly. Enable it by setting {query_cache_size} to a 2 digit MiB value and setting {query_cache_type} to 'ON'. Note: If you are using memcached, ignore this recommendation. + query_cache_size is set to 0 or query_cache_type is set to 'OFF' + +rule 'Query cache efficiency (%)' [Com_select + Qcache_hits > 0 && !fired('Query cache disabled')] + Qcache_hits / (Com_select + Qcache_hits) * 100 + value < 20 + Query cache not running efficiently, it has a low hit rate. + Consider increasing {query_cache_limit}. + The current query cache hit rate of %s% is below 20% | round(value,1) + +rule 'Query Cache usage' [!fired('Query cache disabled')] + 100 - Qcache_free_memory / query_cache_size * 100 + value < 80 + Less than 80% of the query cache is being utilized. + This might be caused by {query_cache_limit} being too low. Flushing the query cache might help as well. + The current ratio of free query cache memory to total query cache size is %s%. It should be above 80% | round(value,1) + +rule 'Query cache fragmentation' [!fired('Query cache disabled')] + Qcache_free_blocks / (Qcache_total_blocks / 2) * 100 + value > 20 + The query cache is considerably fragmented. + Severe fragmentation is likely to (further) increase Qcache_lowmem_prunes. This might be caused by many Query cache low memory prunes due to {query_cache_size} being too small. For a immediate but short lived fix you can flush the query cache (might lock the query cache for a long time). Carefully adjusting {query_cache_min_res_unit} to a lower value might help too, e.g. you can set it to the average size of your queries in the cache using this formula: (query_cache_size - qcache_free_memory) / qcache_queries_in_cache + The cache is currently fragmented by %s% , with 100% fragmentation meaning that the query cache is an alternating pattern of free and used blocks. This value should be below 20%. | round(value,1) + +rule 'Query cache low memory prunes' [Qcache_inserts > 0 && !fired('Query cache disabled')] + Qcache_lowmem_prunes / Qcache_inserts * 100 + value > 0.1 + Cached queries are removed due to low query cache memory from the query cache. + You might want to increase {query_cache_size}, however keep in mind that the overhead of maintaining the cache is likely to increase with its size, so do this in small increments and monitor the results. + The ratio of removed queries to inserted queries is %s%. The lower this value is, the better (This rules firing limit: 0.1%) | round(value,1) + +rule 'Query cache max size' [!fired('Query cache disabled')] + query_cache_size + value > 1024 * 1024 * 128 + The query cache size is above 128 MiB. Big query caches may cause significant overhead that is required to maintain the cache. + Depending on your environment, it might be performance increasing to reduce this value. + Current query cache size: %s | ADVISOR_formatByteDown(value, 2, 2) + +rule 'Query cache min result size' [!fired('Query cache disabled')] + query_cache_limit + value == 1024*1024 + The max size of the result set in the query cache is the default of 1 MiB. + Changing {query_cache_limit} (usually by increasing) may increase efficiency. This variable determines the maximum size a query result may have to be inserted into the query cache. If there are many query results above 1 MiB that are well cacheable (many reads, little writes) then increasing {query_cache_limit} will increase efficiency. Whereas in the case of many query results being above 1 MiB that are not very well cacheable (often invalidated due to table updates) increasing {query_cache_limit} might reduce efficiency. + query_cache_limit is set to 1 MiB diff --git a/srcs/phpmyadmin/libraries/certs/12d55845.0 b/srcs/phpmyadmin/libraries/certs/12d55845.0 new file mode 100644 index 0000000..b2e43c9 --- /dev/null +++ b/srcs/phpmyadmin/libraries/certs/12d55845.0 @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow +PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD +Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O +rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq +OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b +xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw +7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD +aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG +SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 +ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr +AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz +R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 +JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo +Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- diff --git a/srcs/phpmyadmin/libraries/certs/2e5ac55d.0 b/srcs/phpmyadmin/libraries/certs/2e5ac55d.0 new file mode 100644 index 0000000..b2e43c9 --- /dev/null +++ b/srcs/phpmyadmin/libraries/certs/2e5ac55d.0 @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow +PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD +Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O +rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq +OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b +xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw +7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD +aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG +SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 +ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr +AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz +R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 +JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo +Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- diff --git a/srcs/phpmyadmin/libraries/certs/4042bcee.0 b/srcs/phpmyadmin/libraries/certs/4042bcee.0 new file mode 100644 index 0000000..9548dc1 --- /dev/null +++ b/srcs/phpmyadmin/libraries/certs/4042bcee.0 @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- diff --git a/srcs/phpmyadmin/libraries/certs/6187b673.0 b/srcs/phpmyadmin/libraries/certs/6187b673.0 new file mode 100644 index 0000000..9548dc1 --- /dev/null +++ b/srcs/phpmyadmin/libraries/certs/6187b673.0 @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- diff --git a/srcs/phpmyadmin/libraries/certs/README.rst b/srcs/phpmyadmin/libraries/certs/README.rst new file mode 100644 index 0000000..bc48f6c --- /dev/null +++ b/srcs/phpmyadmin/libraries/certs/README.rst @@ -0,0 +1,16 @@ +phpMyAdmin SSL certificates +=========================== + +This directory contains copy of root certificates used to sign phpmyadmin.net +and reports.phpmyadmin.net websites. It is used to allow operation on systems +where the certificates are missing or wrongly configured (happens on Windows +with wrongly compiled CURL). + +Currently included SSL certificates: + +* ISRG Root X1 +* DST Root CA X3 + +See https://letsencrypt.org/certificates/ for more info on them. + +In case of update, the filenames can be generated using c_rehash tool. diff --git a/srcs/phpmyadmin/libraries/certs/cacert.pem b/srcs/phpmyadmin/libraries/certs/cacert.pem new file mode 100644 index 0000000..5f2265f --- /dev/null +++ b/srcs/phpmyadmin/libraries/certs/cacert.pem @@ -0,0 +1,51 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow +PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD +Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O +rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq +OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b +xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw +7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD +aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG +SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 +ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr +AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz +R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 +JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo +Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- diff --git a/srcs/phpmyadmin/libraries/classes/Advisor.php b/srcs/phpmyadmin/libraries/classes/Advisor.php new file mode 100644 index 0000000..6dad8b6 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Advisor.php @@ -0,0 +1,707 @@ +dbi = $dbi; + $this->expression = $expression; + /* + * Register functions for ExpressionLanguage, we intentionally + * do not implement support for compile as we do not use it. + */ + $this->expression->register( + 'round', + function () { + }, + function ($arguments, $num) { + return round($num); + } + ); + $this->expression->register( + 'substr', + function () { + }, + function ($arguments, $string, $start, $length) { + return substr($string, $start, $length); + } + ); + $this->expression->register( + 'preg_match', + function () { + }, + function ($arguments, $pattern, $subject) { + return preg_match($pattern, $subject); + } + ); + $this->expression->register( + 'ADVISOR_bytime', + function () { + }, + function ($arguments, $num, $precision) { + return self::byTime($num, $precision); + } + ); + $this->expression->register( + 'ADVISOR_timespanFormat', + function () { + }, + function ($arguments, $seconds) { + return self::timespanFormat((int) $seconds); + } + ); + $this->expression->register( + 'ADVISOR_formatByteDown', + function () { + }, + function ($arguments, $value, $limes = 6, $comma = 0) { + return self::formatByteDown($value, $limes, $comma); + } + ); + $this->expression->register( + 'fired', + function () { + }, + function ($arguments, $value) { + if (! isset($this->runResult['fired'])) { + return 0; + } + + // Did matching rule fire? + foreach ($this->runResult['fired'] as $rule) { + if ($rule['id'] == $value) { + return '1'; + } + } + + return '0'; + } + ); + /* Some global variables for advisor */ + $this->globals = [ + 'PMA_MYSQL_INT_VERSION' => $this->dbi->getVersion(), + ]; + } + + /** + * Get variables + * + * @return mixed + */ + public function getVariables() + { + return $this->variables; + } + + /** + * Set variables + * + * @param array $variables Variables + * + * @return Advisor + */ + public function setVariables(array $variables): self + { + $this->variables = $variables; + + return $this; + } + + /** + * Set a variable and its value + * + * @param string|int $variable Variable to set + * @param mixed $value Value to set + * + * @return Advisor + */ + public function setVariable($variable, $value): self + { + $this->variables[$variable] = $value; + + return $this; + } + + /** + * Get parseResult + * + * @return mixed + */ + public function getParseResult() + { + return $this->parseResult; + } + + /** + * Set parseResult + * + * @param array $parseResult Parse result + * + * @return Advisor + */ + public function setParseResult(array $parseResult): self + { + $this->parseResult = $parseResult; + + return $this; + } + + /** + * Get runResult + * + * @return mixed + */ + public function getRunResult() + { + return $this->runResult; + } + + /** + * Set runResult + * + * @param array $runResult Run result + * + * @return Advisor + */ + public function setRunResult(array $runResult): self + { + $this->runResult = $runResult; + + return $this; + } + + /** + * Parses and executes advisor rules + * + * @return array with run and parse results + */ + public function run(): array + { + // HowTo: A simple Advisory system in 3 easy steps. + + // Step 1: Get some variables to evaluate on + $this->setVariables( + array_merge( + $this->dbi->fetchResult('SHOW GLOBAL STATUS', 0, 1), + $this->dbi->fetchResult('SHOW GLOBAL VARIABLES', 0, 1) + ) + ); + + // Add total memory to variables as well + $sysinfo = SysInfo::get(); + $memory = $sysinfo->memory(); + $this->variables['system_memory'] + = isset($memory['MemTotal']) ? $memory['MemTotal'] : 0; + + $ruleFiles = $this->defineRulesFiles(); + + // Step 2: Read and parse the list of rules + $parsedResults = []; + foreach ($ruleFiles as $ruleFile) { + $parsedResults[] = $this->parseRulesFile($ruleFile); + } + $this->setParseResult(array_merge_recursive(...$parsedResults)); + + // Step 3: Feed the variables to the rules and let them fire. Sets + // $runResult + $this->runRules(); + + return [ + 'parse' => ['errors' => $this->parseResult['errors']], + 'run' => $this->runResult, + ]; + } + + /** + * Stores current error in run results. + * + * @param string $description description of an error. + * @param Throwable $exception exception raised + * + * @return void + */ + public function storeError(string $description, Throwable $exception): void + { + $this->runResult['errors'][] = $description + . ' ' + . sprintf( + __('Error when evaluating: %s'), + $exception->getMessage() + ); + } + + /** + * Executes advisor rules + * + * @return boolean + */ + public function runRules(): bool + { + $this->setRunResult( + [ + 'fired' => [], + 'notfired' => [], + 'unchecked' => [], + 'errors' => [], + ] + ); + + foreach ($this->parseResult['rules'] as $rule) { + $this->variables['value'] = 0; + $precond = true; + + if (isset($rule['precondition'])) { + try { + $precond = $this->ruleExprEvaluate($rule['precondition']); + } catch (Exception $e) { + $this->storeError( + sprintf( + __('Failed evaluating precondition for rule \'%s\'.'), + $rule['name'] + ), + $e + ); + continue; + } + } + + if (! $precond) { + $this->addRule('unchecked', $rule); + } else { + try { + $value = $this->ruleExprEvaluate($rule['formula']); + } catch (Exception $e) { + $this->storeError( + sprintf( + __('Failed calculating value for rule \'%s\'.'), + $rule['name'] + ), + $e + ); + continue; + } + + $this->variables['value'] = $value; + + try { + if ($this->ruleExprEvaluate($rule['test'])) { + $this->addRule('fired', $rule); + } else { + $this->addRule('notfired', $rule); + } + } catch (Exception $e) { + $this->storeError( + sprintf( + __('Failed running test for rule \'%s\'.'), + $rule['name'] + ), + $e + ); + } + } + } + + return true; + } + + /** + * Escapes percent string to be used in format string. + * + * @param string $str string to escape + * + * @return string + */ + public static function escapePercent(string $str): string + { + return preg_replace('/%( |,|\.|$|\(|\)|<|>)/', '%%\1', $str); + } + + /** + * Wrapper function for translating. + * + * @param string $str the string + * @param string $param the parameters + * + * @return string + * @throws Exception + */ + public function translate(string $str, ?string $param = null): string + { + $string = _gettext(self::escapePercent($str)); + if ($param !== null) { + $params = $this->ruleExprEvaluate('[' . $param . ']'); + } else { + $params = []; + } + return vsprintf($string, $params); + } + + /** + * Splits justification to text and formula. + * + * @param array $rule the rule + * + * @return string[] + */ + public static function splitJustification(array $rule): array + { + $jst = preg_split('/\s*\|\s*/', $rule['justification'], 2); + if (count($jst) > 1) { + return [ + $jst[0], + $jst[1], + ]; + } + return [$rule['justification']]; + } + + /** + * Adds a rule to the result list + * + * @param string $type type of rule + * @param array $rule rule itself + * + * @return void + * @throws Exception + */ + public function addRule(string $type, array $rule): void + { + switch ($type) { + case 'notfired': + case 'fired': + $jst = self::splitJustification($rule); + if (count($jst) > 1) { + try { + /* Translate */ + $str = $this->translate($jst[0], $jst[1]); + } catch (Exception $e) { + $this->storeError( + sprintf( + __('Failed formatting string for rule \'%s\'.'), + $rule['name'] + ), + $e + ); + return; + } + + $rule['justification'] = $str; + } else { + $rule['justification'] = $this->translate($rule['justification']); + } + $rule['id'] = $rule['name']; + $rule['name'] = $this->translate($rule['name']); + $rule['issue'] = $this->translate($rule['issue']); + + // Replaces {server_variable} with 'server_variable' + // linking to server_variables.php + $rule['recommendation'] = preg_replace_callback( + '/\{([a-z_0-9]+)\}/Ui', + [ + $this, + 'replaceVariable', + ], + $this->translate($rule['recommendation']) + ); + + // Replaces external Links with Core::linkURL() generated links + $rule['recommendation'] = preg_replace_callback( + '#href=("|\')(https?://[^\1]+)\1#i', + [ + $this, + 'replaceLinkURL', + ], + $rule['recommendation'] + ); + break; + } + + $this->runResult[$type][] = $rule; + } + + /** + * Defines the rules files to use + * + * @return array + */ + protected function defineRulesFiles(): array + { + $isMariaDB = false !== strpos($this->getVariables()['version'], 'MariaDB'); + $ruleFiles = [self::GENERIC_RULES_FILE]; + // If MariaDB (= not MySQL) OR MYSQL < 8.0.3, add another rules file. + if ($isMariaDB || $this->globals['PMA_MYSQL_INT_VERSION'] < 80003) { + $ruleFiles[] = self::BEFORE_MYSQL80003_RULES_FILE; + } + return $ruleFiles; + } + + /** + * Callback for wrapping links with Core::linkURL + * + * @param array $matches List of matched elements form preg_replace_callback + * + * @return string Replacement value + */ + private function replaceLinkURL(array $matches): string + { + return 'href="' . Core::linkURL($matches[2]) . '" target="_blank" rel="noopener noreferrer"'; + } + + /** + * Callback for wrapping variable edit links + * + * @param array $matches List of matched elements form preg_replace_callback + * + * @return string Replacement value + */ + private function replaceVariable(array $matches): string + { + return '' . htmlspecialchars($matches[1]) . ''; + } + + /** + * Runs a code expression, replacing variable names with their respective + * values + * + * @param string $expr expression to evaluate + * + * @return mixed result of evaluated expression + * + * @throws Exception + */ + public function ruleExprEvaluate(string $expr) + { + // Actually evaluate the code + // This can throw exception + $value = $this->expression->evaluate( + $expr, + array_merge($this->variables, $this->globals) + ); + + return $value; + } + + /** + * Reads the rule file into an array, throwing errors messages on syntax + * errors. + * + * @param string $filename Name of file to parse + * + * @return array with parsed data + */ + public static function parseRulesFile(string $filename): array + { + $file = file($filename, FILE_IGNORE_NEW_LINES); + + $errors = []; + $rules = []; + $lines = []; + + if ($file === false) { + $errors[] = sprintf( + __('Error in reading file: The file \'%s\' does not exist or is not readable!'), + $filename + ); + return [ + 'rules' => $rules, + 'lines' => $lines, + 'errors' => $errors, + ]; + } + + $ruleSyntax = [ + 'name', + 'formula', + 'test', + 'issue', + 'recommendation', + 'justification', + ]; + $numRules = count($ruleSyntax); + $numLines = count($file); + $ruleNo = -1; + $ruleLine = -1; + + for ($i = 0; $i < $numLines; $i++) { + $line = $file[$i]; + if ($line == "" || $line[0] == '#') { + continue; + } + + // Reading new rule + if (substr($line, 0, 4) == 'rule') { + if ($ruleLine > 0) { + $errors[] = sprintf( + __( + 'Invalid rule declaration on line %1$s, expected line ' + . '%2$s of previous rule.' + ), + $i + 1, + $ruleSyntax[$ruleLine++] + ); + continue; + } + if (preg_match("/rule\s'(.*)'( \[(.*)\])?$/", $line, $match)) { + $ruleLine = 1; + $ruleNo++; + $rules[$ruleNo] = ['name' => $match[1]]; + $lines[$ruleNo] = ['name' => $i + 1]; + if (isset($match[3])) { + $rules[$ruleNo]['precondition'] = $match[3]; + $lines[$ruleNo]['precondition'] = $i + 1; + } + } else { + $errors[] = sprintf( + __('Invalid rule declaration on line %s.'), + $i + 1 + ); + } + continue; + } elseif ($ruleLine == -1) { + $errors[] = sprintf( + __('Unexpected characters on line %s.'), + $i + 1 + ); + } + + // Reading rule lines + if ($ruleLine > 0) { + if (! isset($line[0])) { + continue; // Empty lines are ok + } + // Non tabbed lines are not + if ($line[0] != "\t") { + $errors[] = sprintf( + __( + 'Unexpected character on line %1$s. Expected tab, but ' + . 'found "%2$s".' + ), + $i + 1, + $line[0] + ); + continue; + } + $rules[$ruleNo][$ruleSyntax[$ruleLine]] = rtrim( + mb_substr($line, 1) + ); + $lines[$ruleNo][$ruleSyntax[$ruleLine]] = $i + 1; + ++$ruleLine; + } + + // Rule complete + if ($ruleLine == $numRules) { + $ruleLine = -1; + } + } + + return [ + 'rules' => $rules, + 'lines' => $lines, + 'errors' => $errors, + ]; + } + + /** + * Formats interval like 10 per hour + * + * @param float $num number to format + * @param integer $precision required precision + * + * @return string formatted string + */ + public static function byTime(float $num, int $precision): string + { + if ($num >= 1) { // per second + $per = __('per second'); + } elseif ($num * 60 >= 1) { // per minute + $num *= 60; + $per = __('per minute'); + } elseif ($num * 60 * 60 >= 1) { // per hour + $num = $num * 60 * 60; + $per = __('per hour'); + } else { + $num = $num * 60 * 60 * 24; + $per = __('per day'); + } + + $num = round($num, $precision); + + if ($num == 0) { + $num = '<' . pow(10, -$precision); + } + + return "$num $per"; + } + + /** + * Wrapper for PhpMyAdmin\Util::timespanFormat + * + * This function is used when evaluating advisory_rules.txt + * + * @param int $seconds the timespan + * + * @return string the formatted value + */ + public static function timespanFormat(int $seconds): string + { + return Util::timespanFormat($seconds); + } + + /** + * Wrapper around PhpMyAdmin\Util::formatByteDown + * + * This function is used when evaluating advisory_rules.txt + * + * @param double|string $value the value to format + * @param int $limes the sensitiveness + * @param int $comma the number of decimals to retain + * + * @return string the formatted value with unit + */ + public static function formatByteDown($value, int $limes = 6, int $comma = 0): string + { + return implode(' ', Util::formatByteDown($value, $limes, $comma)); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Bookmark.php b/srcs/phpmyadmin/libraries/classes/Bookmark.php new file mode 100644 index 0000000..7dc1302 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Bookmark.php @@ -0,0 +1,395 @@ +dbi = $dbi; + $this->user = $user; + } + + /** + * Returns the ID of the bookmark + * + * @return int + */ + public function getId(): int + { + return (int) $this->_id; + } + + /** + * Returns the database of the bookmark + * + * @return string + */ + public function getDatabase(): string + { + return $this->_database; + } + + /** + * Returns the user whom the bookmark belongs to + * + * @return string + */ + public function getUser(): string + { + return $this->_user; + } + + /** + * Returns the label of the bookmark + * + * @return string + */ + public function getLabel(): string + { + return $this->_label; + } + + /** + * Returns the query + * + * @return string + */ + public function getQuery(): string + { + return $this->_query; + } + + /** + * Adds a bookmark + * + * @return boolean whether the INSERT succeeds or not + * + * @access public + */ + public function save(): bool + { + $cfgBookmark = self::getParams($this->user); + if (empty($cfgBookmark)) { + return false; + } + + $query = "INSERT INTO " . Util::backquote($cfgBookmark['db']) + . "." . Util::backquote($cfgBookmark['table']) + . " (id, dbase, user, query, label) VALUES (NULL, " + . "'" . $this->dbi->escapeString($this->_database) . "', " + . "'" . $this->dbi->escapeString($this->_user) . "', " + . "'" . $this->dbi->escapeString($this->_query) . "', " + . "'" . $this->dbi->escapeString($this->_label) . "')"; + return $this->dbi->query($query, DatabaseInterface::CONNECT_CONTROL); + } + + /** + * Deletes a bookmark + * + * @return bool true if successful + * + * @access public + */ + public function delete(): bool + { + $cfgBookmark = self::getParams($this->user); + if (empty($cfgBookmark)) { + return false; + } + + $query = "DELETE FROM " . Util::backquote($cfgBookmark['db']) + . "." . Util::backquote($cfgBookmark['table']) + . " WHERE id = " . $this->_id; + return $this->dbi->tryQuery($query, DatabaseInterface::CONNECT_CONTROL); + } + + /** + * Returns the number of variables in a bookmark + * + * @return int number of variables + */ + public function getVariableCount(): int + { + $matches = []; + preg_match_all("/\[VARIABLE[0-9]*\]/", $this->_query, $matches, PREG_SET_ORDER); + return count($matches); + } + + /** + * Replace the placeholders in the bookmark query with variables + * + * @param array $variables array of variables + * + * @return string query with variables applied + */ + public function applyVariables(array $variables): string + { + // remove comments that encloses a variable placeholder + $query = preg_replace( + '|/\*(.*\[VARIABLE[0-9]*\].*)\*/|imsU', + '${1}', + $this->_query + ); + // replace variable placeholders with values + $number_of_variables = $this->getVariableCount(); + for ($i = 1; $i <= $number_of_variables; $i++) { + $var = ''; + if (! empty($variables[$i])) { + $var = $this->dbi->escapeString($variables[$i]); + } + $query = str_replace('[VARIABLE' . $i . ']', $var, $query); + // backward compatibility + if ($i == 1) { + $query = str_replace('[VARIABLE]', $var, $query); + } + } + return $query; + } + + /** + * Defines the bookmark parameters for the current user + * + * @param string $user Current user + * + * @return array|bool the bookmark parameters for the current user + * @access public + */ + public static function getParams(string $user) + { + static $cfgBookmark = null; + + if (null !== $cfgBookmark) { + return $cfgBookmark; + } + + $relation = new Relation($GLOBALS['dbi']); + $cfgRelation = $relation->getRelationsParam(); + if ($cfgRelation['bookmarkwork']) { + $cfgBookmark = [ + 'user' => $user, + 'db' => $cfgRelation['db'], + 'table' => $cfgRelation['bookmark'], + ]; + } else { + $cfgBookmark = false; + } + + return $cfgBookmark; + } + + /** + * Creates a Bookmark object from the parameters + * + * @param DatabaseInterface $dbi DatabaseInterface object + * @param string $user Current user + * @param array $bkm_fields the properties of the bookmark to add; here, + * $bkm_fields['bkm_sql_query'] is urlencoded + * @param boolean $all_users whether to make the bookmark + * available for all users + * + * @return Bookmark|false + */ + public static function createBookmark( + DatabaseInterface $dbi, + string $user, + array $bkm_fields, + bool $all_users = false + ) { + if (! (isset($bkm_fields['bkm_sql_query']) + && strlen($bkm_fields['bkm_sql_query']) > 0 + && isset($bkm_fields['bkm_label']) + && strlen($bkm_fields['bkm_label']) > 0) + ) { + return false; + } + + $bookmark = new Bookmark($dbi, $user); + $bookmark->_database = $bkm_fields['bkm_database']; + $bookmark->_label = $bkm_fields['bkm_label']; + $bookmark->_query = $bkm_fields['bkm_sql_query']; + $bookmark->_user = $all_users ? '' : $bkm_fields['bkm_user']; + + return $bookmark; + } + + /** + * Gets the list of bookmarks defined for the current database + * + * @param DatabaseInterface $dbi DatabaseInterface object + * @param string $user Current user + * @param string|bool $db the current database name or false + * + * @return Bookmark[] the bookmarks list + * + * @access public + */ + public static function getList( + DatabaseInterface $dbi, + string $user, + $db = false + ): array { + $cfgBookmark = self::getParams($user); + if (empty($cfgBookmark)) { + return []; + } + + $query = "SELECT * FROM " . Util::backquote($cfgBookmark['db']) + . "." . Util::backquote($cfgBookmark['table']) + . " WHERE ( `user` = ''" + . " OR `user` = '" . $dbi->escapeString($cfgBookmark['user']) . "' )"; + if ($db !== false) { + $query .= " AND dbase = '" . $dbi->escapeString($db) . "'"; + } + $query .= " ORDER BY label ASC"; + + $result = $dbi->fetchResult( + $query, + null, + null, + DatabaseInterface::CONNECT_CONTROL, + DatabaseInterface::QUERY_STORE + ); + + if (! empty($result)) { + $bookmarks = []; + foreach ($result as $row) { + $bookmark = new Bookmark($dbi, $user); + $bookmark->_id = $row['id']; + $bookmark->_database = $row['dbase']; + $bookmark->_user = $row['user']; + $bookmark->_label = $row['label']; + $bookmark->_query = $row['query']; + $bookmarks[] = $bookmark; + } + + return $bookmarks; + } + + return []; + } + + /** + * Retrieve a specific bookmark + * + * @param DatabaseInterface $dbi DatabaseInterface object + * @param string $user Current user + * @param string $db the current database name + * @param mixed $id an identifier of the bookmark to get + * @param string $id_field which field to look up the identifier + * @param boolean $action_bookmark_all true: get all bookmarks regardless + * of the owning user + * @param boolean $exact_user_match whether to ignore bookmarks with no user + * + * @return Bookmark the bookmark + * + * @access public + * + */ + public static function get( + DatabaseInterface $dbi, + string $user, + string $db, + $id, + string $id_field = 'id', + bool $action_bookmark_all = false, + bool $exact_user_match = false + ): ?self { + $cfgBookmark = self::getParams($user); + if (empty($cfgBookmark)) { + return null; + } + + $query = "SELECT * FROM " . Util::backquote($cfgBookmark['db']) + . "." . Util::backquote($cfgBookmark['table']) + . " WHERE dbase = '" . $dbi->escapeString($db) . "'"; + if (! $action_bookmark_all) { + $query .= " AND (user = '" + . $dbi->escapeString($cfgBookmark['user']) . "'"; + if (! $exact_user_match) { + $query .= " OR user = ''"; + } + $query .= ")"; + } + $query .= " AND " . Util::backquote($id_field) + . " = '" . $dbi->escapeString((string) $id) . "' LIMIT 1"; + + $result = $dbi->fetchSingleRow($query, 'ASSOC', DatabaseInterface::CONNECT_CONTROL); + if (! empty($result)) { + $bookmark = new Bookmark($dbi, $user); + $bookmark->_id = $result['id']; + $bookmark->_database = $result['dbase']; + $bookmark->_user = $result['user']; + $bookmark->_label = $result['label']; + $bookmark->_query = $result['query']; + return $bookmark; + } + + return null; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/BrowseForeigners.php b/srcs/phpmyadmin/libraries/classes/BrowseForeigners.php new file mode 100644 index 0000000..18bc30a --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/BrowseForeigners.php @@ -0,0 +1,361 @@ +limitChars = $limitChars; + $this->maxRows = $maxRows; + $this->repeatCells = $repeatCells; + $this->showAll = $showAll; + $this->themeImage = $themeImage; + $this->template = $template; + } + + /** + * Function to get html for one relational key + * + * @param integer $horizontal_count the current horizontal count + * @param string $header table header + * @param array $keys all the keys + * @param integer $indexByKeyname index by keyname + * @param array $descriptions descriptions + * @param integer $indexByDescription index by description + * @param string $current_value current value on the edit form + * + * @return array the generated html + */ + private function getHtmlForOneKey( + int $horizontal_count, + string $header, + array $keys, + int $indexByKeyname, + array $descriptions, + int $indexByDescription, + string $current_value + ): array { + $horizontal_count++; + $output = ''; + + // whether the key name corresponds to the selected value in the form + $rightKeynameIsSelected = false; + $leftKeynameIsSelected = false; + + if ($this->repeatCells > 0 && $horizontal_count > $this->repeatCells) { + $output .= $header; + $horizontal_count = 0; + } + + // key names and descriptions for the left section, + // sorted by key names + $leftKeyname = $keys[$indexByKeyname]; + list( + $leftDescription, + $leftDescriptionTitle + ) = $this->getDescriptionAndTitle($descriptions[$indexByKeyname]); + + // key names and descriptions for the right section, + // sorted by descriptions + $rightKeyname = $keys[$indexByDescription]; + list( + $rightDescription, + $rightDescriptionTitle + ) = $this->getDescriptionAndTitle($descriptions[$indexByDescription]); + + $indexByDescription++; + + if (! empty($current_value)) { + $rightKeynameIsSelected = $rightKeyname == $current_value; + $leftKeynameIsSelected = $leftKeyname == $current_value; + } + + $output .= ''; + + $output .= $this->template->render('table/browse_foreigners/column_element', [ + 'keyname' => $leftKeyname, + 'description' => $leftDescription, + 'title' => $leftDescriptionTitle, + 'is_selected' => $leftKeynameIsSelected, + 'nowrap' => true, + ]); + $output .= $this->template->render('table/browse_foreigners/column_element', [ + 'keyname' => $leftKeyname, + 'description' => $leftDescription, + 'title' => $leftDescriptionTitle, + 'is_selected' => $leftKeynameIsSelected, + 'nowrap' => false, + ]); + + $output .= '' + . ''; + + $output .= $this->template->render('table/browse_foreigners/column_element', [ + 'keyname' => $rightKeyname, + 'description' => $rightDescription, + 'title' => $rightDescriptionTitle, + 'is_selected' => $rightKeynameIsSelected, + 'nowrap' => false, + ]); + $output .= $this->template->render('table/browse_foreigners/column_element', [ + 'keyname' => $rightKeyname, + 'description' => $rightDescription, + 'title' => $rightDescriptionTitle, + 'is_selected' => $rightKeynameIsSelected, + 'nowrap' => true, + ]); + + $output .= ''; + + return [ + $output, + $horizontal_count, + $indexByDescription, + ]; + } + + /** + * Function to get html for relational field selection + * + * @param string $db current database + * @param string $table current table + * @param string $field field + * @param array $foreignData foreign column data + * @param string|null $fieldkey field key + * @param string $current_value current columns's value + * + * @return string + */ + public function getHtmlForRelationalFieldSelection( + string $db, + string $table, + string $field, + array $foreignData, + ?string $fieldkey, + string $current_value + ): string { + $gotopage = $this->getHtmlForGotoPage($foreignData); + $foreignShowAll = $this->template->render('table/browse_foreigners/show_all', [ + 'foreign_data' => $foreignData, + 'show_all' => $this->showAll, + 'max_rows' => $this->maxRows, + ]); + + $output = '
' + . '
' + . Url::getHiddenInputs($db, $table) + . '' + . ''; + + if (isset($_POST['rownumber'])) { + $output .= ''; + } + $filter_value = (isset($_POST['foreign_filter']) + ? htmlspecialchars($_POST['foreign_filter']) + : ''); + $output .= '' + . '' + . '' + . '' + . '' + . '' . $gotopage . '' + . '' . $foreignShowAll . '' + . '
' + . '
'; + + $output .= ''; + + if (! is_array($foreignData['disp_row'])) { + $output .= '' + . '
'; + + return $output; + } + + $header = ' + ' . __('Keyname') . ' + ' . __('Description') . ' + + ' . __('Description') . ' + ' . __('Keyname') . ' + '; + + $output .= '' . $header . '' . "\n" + . '' . $header . '' . "\n" + . '' . "\n"; + + $descriptions = []; + $keys = []; + foreach ($foreignData['disp_row'] as $relrow) { + if ($foreignData['foreign_display'] != false) { + $descriptions[] = $relrow[$foreignData['foreign_display']]; + } else { + $descriptions[] = ''; + } + + $keys[] = $relrow[$foreignData['foreign_field']]; + } + + asort($keys); + + $horizontal_count = 0; + $indexByDescription = 0; + + foreach ($keys as $indexByKeyname => $value) { + list( + $html, + $horizontal_count, + $indexByDescription + ) = $this->getHtmlForOneKey( + $horizontal_count, + $header, + $keys, + $indexByKeyname, + $descriptions, + $indexByDescription, + $current_value + ); + $output .= $html; + } + + $output .= '' + . ''; + + return $output; + } + + /** + * Get the description (possibly truncated) and the title + * + * @param string $description the key name's description + * + * @return array the new description and title + */ + private function getDescriptionAndTitle(string $description): array + { + if (mb_strlen($description) <= $this->limitChars) { + $description = htmlspecialchars( + $description + ); + $descriptionTitle = ''; + } else { + $descriptionTitle = htmlspecialchars( + $description + ); + $description = htmlspecialchars( + mb_substr( + $description, + 0, + $this->limitChars + ) + . '...' + ); + } + return [ + $description, + $descriptionTitle, + ]; + } + + /** + * Function to get html for the goto page option + * + * @param array|null $foreignData foreign data + * + * @return string + */ + private function getHtmlForGotoPage(?array $foreignData): string + { + $gotopage = ''; + isset($_POST['pos']) ? $pos = $_POST['pos'] : $pos = 0; + if ($foreignData === null || ! is_array($foreignData['disp_row'])) { + return $gotopage; + } + + $pageNow = @floor($pos / $this->maxRows) + 1; + $nbTotalPage = @ceil($foreignData['the_total'] / $this->maxRows); + + if ($foreignData['the_total'] > $this->maxRows) { + $gotopage = Util::pageselector( + 'pos', + $this->maxRows, + $pageNow, + $nbTotalPage, + 200, + 5, + 5, + 20, + 10, + __('Page number:') + ); + } + + return $gotopage; + } + + /** + * Function to get foreign limit + * + * @param string|null $foreignShowAll foreign navigation + * + * @return string + */ + public function getForeignLimit(?string $foreignShowAll): ?string + { + if (isset($foreignShowAll) && $foreignShowAll == __('Show all')) { + return null; + } + isset($_POST['pos']) ? $pos = $_POST['pos'] : $pos = 0; + return 'LIMIT ' . $pos . ', ' . $this->maxRows . ' '; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/CentralColumns.php b/srcs/phpmyadmin/libraries/classes/CentralColumns.php new file mode 100644 index 0000000..9e11db0 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/CentralColumns.php @@ -0,0 +1,1207 @@ +dbi = $dbi; + + $this->user = $GLOBALS['cfg']['Server']['user']; + $this->maxRows = (int) $GLOBALS['cfg']['MaxRows']; + $this->charEditing = $GLOBALS['cfg']['CharEditing']; + $this->disableIs = (bool) $GLOBALS['cfg']['Server']['DisableIS']; + + $this->relation = new Relation($this->dbi); + $this->template = new Template(); + } + + /** + * Defines the central_columns parameters for the current user + * + * @return array|bool the central_columns parameters for the current user + * @access public + */ + public function getParams() + { + static $cfgCentralColumns = null; + + if (null !== $cfgCentralColumns) { + return $cfgCentralColumns; + } + + $cfgRelation = $this->relation->getRelationsParam(); + + if ($cfgRelation['centralcolumnswork']) { + $cfgCentralColumns = [ + 'user' => $this->user, + 'db' => $cfgRelation['db'], + 'table' => $cfgRelation['central_columns'], + ]; + } else { + $cfgCentralColumns = false; + } + + return $cfgCentralColumns; + } + + /** + * get $num columns of given database from central columns list + * starting at offset $from + * + * @param string $db selected database + * @param int $from starting offset of first result + * @param int $num maximum number of results to return + * + * @return array list of $num columns present in central columns list + * starting at offset $from for the given database + */ + public function getColumnsList(string $db, int $from = 0, int $num = 25): array + { + $cfgCentralColumns = $this->getParams(); + if (empty($cfgCentralColumns)) { + return []; + } + $pmadb = $cfgCentralColumns['db']; + $this->dbi->selectDb($pmadb, DatabaseInterface::CONNECT_CONTROL); + $central_list_table = $cfgCentralColumns['table']; + //get current values of $db from central column list + if ($num == 0) { + $query = 'SELECT * FROM ' . Util::backquote($central_list_table) . ' ' + . 'WHERE db_name = \'' . $this->dbi->escapeString($db) . '\';'; + } else { + $query = 'SELECT * FROM ' . Util::backquote($central_list_table) . ' ' + . 'WHERE db_name = \'' . $this->dbi->escapeString($db) . '\' ' + . 'LIMIT ' . $from . ', ' . $num . ';'; + } + $has_list = (array) $this->dbi->fetchResult( + $query, + null, + null, + DatabaseInterface::CONNECT_CONTROL + ); + $this->handleColumnExtra($has_list); + return $has_list; + } + + /** + * Get the number of columns present in central list for given db + * + * @param string $db current database + * + * @return int number of columns in central list of columns for $db + */ + public function getCount(string $db): int + { + $cfgCentralColumns = $this->getParams(); + if (empty($cfgCentralColumns)) { + return 0; + } + $pmadb = $cfgCentralColumns['db']; + $this->dbi->selectDb($pmadb, DatabaseInterface::CONNECT_CONTROL); + $central_list_table = $cfgCentralColumns['table']; + $query = 'SELECT count(db_name) FROM ' . + Util::backquote($central_list_table) . ' ' + . 'WHERE db_name = \'' . $this->dbi->escapeString($db) . '\';'; + $res = $this->dbi->fetchResult( + $query, + null, + null, + DatabaseInterface::CONNECT_CONTROL + ); + if (isset($res[0])) { + return (int) $res[0]; + } + + return 0; + } + + /** + * return the existing columns in central list among the given list of columns + * + * @param string $db the selected database + * @param string $cols comma separated list of given columns + * @param boolean $allFields set if need all the fields of existing columns, + * otherwise only column_name is returned + * + * @return array list of columns in central columns among given set of columns + */ + private function findExistingColNames( + string $db, + string $cols, + bool $allFields = false + ): array { + $cfgCentralColumns = $this->getParams(); + if (empty($cfgCentralColumns)) { + return []; + } + $pmadb = $cfgCentralColumns['db']; + $this->dbi->selectDb($pmadb, DatabaseInterface::CONNECT_CONTROL); + $central_list_table = $cfgCentralColumns['table']; + if ($allFields) { + $query = 'SELECT * FROM ' . Util::backquote($central_list_table) . ' ' + . 'WHERE db_name = \'' . $this->dbi->escapeString($db) . '\' AND col_name IN (' . $cols . ');'; + $has_list = (array) $this->dbi->fetchResult( + $query, + null, + null, + DatabaseInterface::CONNECT_CONTROL + ); + $this->handleColumnExtra($has_list); + } else { + $query = 'SELECT col_name FROM ' + . Util::backquote($central_list_table) . ' ' + . 'WHERE db_name = \'' . $this->dbi->escapeString($db) . '\' AND col_name IN (' . $cols . ');'; + $has_list = (array) $this->dbi->fetchResult( + $query, + null, + null, + DatabaseInterface::CONNECT_CONTROL + ); + } + + return $has_list; + } + + /** + * return error message to be displayed if central columns + * configuration storage is not completely configured + * + * @return Message + */ + private function configErrorMessage(): Message + { + return Message::error( + __( + 'The configuration storage is not ready for the central list' + . ' of columns feature.' + ) + ); + } + + /** + * build the insert query for central columns list given PMA storage + * db, central_columns table, column name and corresponding definition to be added + * + * @param string $column column to add into central list + * @param array $def list of attributes of the column being added + * @param string $db PMA configuration storage database name + * @param string $central_list_table central columns configuration storage table name + * + * @return string query string to insert the given column + * with definition into central list + */ + private function getInsertQuery( + string $column, + array $def, + string $db, + string $central_list_table + ): string { + $type = ""; + $length = 0; + $attribute = ""; + if (isset($def['Type'])) { + $extracted_columnspec = Util::extractColumnSpec($def['Type']); + $attribute = trim($extracted_columnspec['attribute']); + $type = $extracted_columnspec['type']; + $length = $extracted_columnspec['spec_in_brackets']; + } + if (isset($def['Attribute'])) { + $attribute = $def['Attribute']; + } + $collation = isset($def['Collation']) ? $def['Collation'] : ""; + $isNull = $def['Null'] == "NO" ? '0' : '1'; + $extra = isset($def['Extra']) ? $def['Extra'] : ""; + $default = isset($def['Default']) ? $def['Default'] : ""; + return 'INSERT INTO ' + . Util::backquote($central_list_table) . ' ' + . 'VALUES ( \'' . $this->dbi->escapeString($db) . '\' ,' + . '\'' . $this->dbi->escapeString($column) . '\',\'' + . $this->dbi->escapeString($type) . '\',' + . '\'' . $this->dbi->escapeString((string) $length) . '\',\'' + . $this->dbi->escapeString($collation) . '\',' + . '\'' . $this->dbi->escapeString($isNull) . '\',' + . '\'' . implode(',', [$extra, $attribute]) + . '\',\'' . $this->dbi->escapeString($default) . '\');'; + } + + /** + * If $isTable is true then unique columns from given tables as $field_select + * are added to central list otherwise the $field_select is considered as + * list of columns and these columns are added to central list if not already added + * + * @param array $field_select if $isTable is true selected tables list + * otherwise selected columns list + * @param bool $isTable if passed array is of tables or columns + * @param string $table if $isTable is false, then table name to + * which columns belong + * + * @return true|Message + */ + public function syncUniqueColumns( + array $field_select, + bool $isTable = true, + ?string $table = null + ) { + $cfgCentralColumns = $this->getParams(); + if (empty($cfgCentralColumns)) { + return $this->configErrorMessage(); + } + $db = $_POST['db']; + $pmadb = $cfgCentralColumns['db']; + $central_list_table = $cfgCentralColumns['table']; + $this->dbi->selectDb($db); + $existingCols = []; + $cols = ""; + $insQuery = []; + $fields = []; + $message = true; + if ($isTable) { + foreach ($field_select as $table) { + $fields[$table] = (array) $this->dbi->getColumns( + $db, + $table, + null, + true + ); + foreach ($fields[$table] as $field => $def) { + $cols .= "'" . $this->dbi->escapeString($field) . "',"; + } + } + + $has_list = $this->findExistingColNames($db, trim($cols, ',')); + foreach ($field_select as $table) { + foreach ($fields[$table] as $field => $def) { + if (! in_array($field, $has_list)) { + $has_list[] = $field; + $insQuery[] = $this->getInsertQuery( + $field, + $def, + $db, + $central_list_table + ); + } else { + $existingCols[] = "'" . $field . "'"; + } + } + } + } else { + if ($table === null) { + $table = $_POST['table']; + } + foreach ($field_select as $column) { + $cols .= "'" . $this->dbi->escapeString($column) . "',"; + } + $has_list = $this->findExistingColNames($db, trim($cols, ',')); + foreach ($field_select as $column) { + if (! in_array($column, $has_list)) { + $has_list[] = $column; + $field = (array) $this->dbi->getColumns( + $db, + $table, + $column, + true + ); + $insQuery[] = $this->getInsertQuery( + $column, + $field, + $db, + $central_list_table + ); + } else { + $existingCols[] = "'" . $column . "'"; + } + } + } + if (! empty($existingCols)) { + $existingCols = implode(",", array_unique($existingCols)); + $message = Message::notice( + sprintf( + __( + 'Could not add %1$s as they already exist in central list!' + ), + htmlspecialchars($existingCols) + ) + ); + $message->addMessage( + Message::notice( + "Please remove them first " + . "from central list if you want to update above columns" + ) + ); + } + $this->dbi->selectDb($pmadb, DatabaseInterface::CONNECT_CONTROL); + if (! empty($insQuery)) { + foreach ($insQuery as $query) { + if (! $this->dbi->tryQuery($query, DatabaseInterface::CONNECT_CONTROL)) { + $message = Message::error(__('Could not add columns!')); + $message->addMessage( + Message::rawError( + $this->dbi->getError(DatabaseInterface::CONNECT_CONTROL) + ) + ); + break; + } + } + } + return $message; + } + + /** + * if $isTable is true it removes all columns of given tables as $field_select from + * central columns list otherwise $field_select is columns list and it removes + * given columns if present in central list + * + * @param string $database Database name + * @param array $field_select if $isTable selected list of tables otherwise + * selected list of columns to remove from central list + * @param bool $isTable if passed array is of tables or columns + * + * @return true|Message + */ + public function deleteColumnsFromList( + string $database, + array $field_select, + bool $isTable = true + ) { + $cfgCentralColumns = $this->getParams(); + if (empty($cfgCentralColumns)) { + return $this->configErrorMessage(); + } + $pmadb = $cfgCentralColumns['db']; + $central_list_table = $cfgCentralColumns['table']; + $this->dbi->selectDb($database); + $message = true; + $colNotExist = []; + $fields = []; + if ($isTable) { + $cols = ''; + foreach ($field_select as $table) { + $fields[$table] = (array) $this->dbi->getColumnNames( + $database, + $table + ); + foreach ($fields[$table] as $col_select) { + $cols .= '\'' . $this->dbi->escapeString($col_select) . '\','; + } + } + $cols = trim($cols, ','); + $has_list = $this->findExistingColNames($database, $cols); + foreach ($field_select as $table) { + foreach ($fields[$table] as $column) { + if (! in_array($column, $has_list)) { + $colNotExist[] = "'" . $column . "'"; + } + } + } + } else { + $cols = ''; + foreach ($field_select as $col_select) { + $cols .= '\'' . $this->dbi->escapeString($col_select) . '\','; + } + $cols = trim($cols, ','); + $has_list = $this->findExistingColNames($database, $cols); + foreach ($field_select as $column) { + if (! in_array($column, $has_list)) { + $colNotExist[] = "'" . $column . "'"; + } + } + } + if (! empty($colNotExist)) { + $colNotExist = implode(",", array_unique($colNotExist)); + $message = Message::notice( + sprintf( + __( + 'Couldn\'t remove Column(s) %1$s ' + . 'as they don\'t exist in central columns list!' + ), + htmlspecialchars($colNotExist) + ) + ); + } + $this->dbi->selectDb($pmadb, DatabaseInterface::CONNECT_CONTROL); + + $query = 'DELETE FROM ' . Util::backquote($central_list_table) . ' ' + . 'WHERE db_name = \'' . $this->dbi->escapeString($database) . '\' AND col_name IN (' . $cols . ');'; + + if (! $this->dbi->tryQuery($query, DatabaseInterface::CONNECT_CONTROL)) { + $message = Message::error(__('Could not remove columns!')); + $message->addHtml('
' . htmlspecialchars($cols) . '
'); + $message->addMessage( + Message::rawError( + $this->dbi->getError(DatabaseInterface::CONNECT_CONTROL) + ) + ); + } + return $message; + } + + /** + * Make the columns of given tables consistent with central list of columns. + * Updates only those columns which are not being referenced. + * + * @param string $db current database + * @param array $selected_tables list of selected tables. + * + * @return true|Message + */ + public function makeConsistentWithList( + string $db, + array $selected_tables + ) { + $message = true; + foreach ($selected_tables as $table) { + $query = 'ALTER TABLE ' . Util::backquote($table); + $has_list = $this->getFromTable($db, $table, true); + $this->dbi->selectDb($db); + foreach ($has_list as $column) { + $column_status = $this->relation->checkChildForeignReferences( + $db, + $table, + $column['col_name'] + ); + //column definition can only be changed if + //it is not referenced by another column + if ($column_status['isEditable']) { + $query .= ' MODIFY ' . Util::backquote($column['col_name']) . ' ' + . $this->dbi->escapeString($column['col_type']); + if ($column['col_length']) { + $query .= '(' . $column['col_length'] . ')'; + } + + $query .= ' ' . $column['col_attribute']; + if ($column['col_isNull']) { + $query .= ' NULL'; + } else { + $query .= ' NOT NULL'; + } + + $query .= ' ' . $column['col_extra']; + if ($column['col_default']) { + if ($column['col_default'] != 'CURRENT_TIMESTAMP' + && $column['col_default'] != 'current_timestamp()') { + $query .= ' DEFAULT \'' . $this->dbi->escapeString( + (string) $column['col_default'] + ) . '\''; + } else { + $query .= ' DEFAULT ' . $this->dbi->escapeString( + $column['col_default'] + ); + } + } + $query .= ','; + } + } + $query = trim($query, " ,") . ";"; + if (! $this->dbi->tryQuery($query)) { + if ($message === true) { + $message = Message::error( + $this->dbi->getError() + ); + } else { + $message->addText( + $this->dbi->getError(), + '
' + ); + } + } + } + return $message; + } + + /** + * return the columns present in central list of columns for a given + * table of a given database + * + * @param string $db given database + * @param string $table given table + * @param boolean $allFields set if need all the fields of existing columns, + * otherwise only column_name is returned + * + * @return array columns present in central list from given table of given db. + */ + public function getFromTable( + string $db, + string $table, + bool $allFields = false + ): array { + $cfgCentralColumns = $this->getParams(); + if (empty($cfgCentralColumns)) { + return []; + } + $this->dbi->selectDb($db); + $fields = (array) $this->dbi->getColumnNames( + $db, + $table + ); + $cols = ''; + foreach ($fields as $col_select) { + $cols .= '\'' . $this->dbi->escapeString((string) $col_select) . '\','; + } + $cols = trim($cols, ','); + $has_list = $this->findExistingColNames($db, $cols, $allFields); + if (! empty($has_list)) { + return (array) $has_list; + } + + return []; + } + + /** + * update a column in central columns list if a edit is requested + * + * @param string $db current database + * @param string $orig_col_name original column name before edit + * @param string $col_name new column name + * @param string $col_type new column type + * @param string $col_attribute new column attribute + * @param string $col_length new column length + * @param int $col_isNull value 1 if new column isNull is true, 0 otherwise + * @param string $collation new column collation + * @param string $col_extra new column extra property + * @param string $col_default new column default value + * + * @return true|Message + */ + public function updateOneColumn( + string $db, + string $orig_col_name, + string $col_name, + string $col_type, + string $col_attribute, + string $col_length, + int $col_isNull, + string $collation, + string $col_extra, + string $col_default + ) { + $cfgCentralColumns = $this->getParams(); + if (empty($cfgCentralColumns)) { + return $this->configErrorMessage(); + } + $centralTable = $cfgCentralColumns['table']; + $this->dbi->selectDb($cfgCentralColumns['db'], DatabaseInterface::CONNECT_CONTROL); + if ($orig_col_name == "") { + $def = []; + $def['Type'] = $col_type; + if ($col_length) { + $def['Type'] .= '(' . $col_length . ')'; + } + $def['Collation'] = $collation; + $def['Null'] = $col_isNull ? __('YES') : __('NO'); + $def['Extra'] = $col_extra; + $def['Attribute'] = $col_attribute; + $def['Default'] = $col_default; + $query = $this->getInsertQuery($col_name, $def, $db, $centralTable); + } else { + $query = 'UPDATE ' . Util::backquote($centralTable) + . ' SET col_type = \'' . $this->dbi->escapeString($col_type) . '\'' + . ', col_name = \'' . $this->dbi->escapeString($col_name) . '\'' + . ', col_length = \'' . $this->dbi->escapeString($col_length) . '\'' + . ', col_isNull = ' . $col_isNull + . ', col_collation = \'' . $this->dbi->escapeString($collation) . '\'' + . ', col_extra = \'' + . implode(',', [$col_extra, $col_attribute]) . '\'' + . ', col_default = \'' . $this->dbi->escapeString($col_default) . '\'' + . ' WHERE db_name = \'' . $this->dbi->escapeString($db) . '\' ' + . 'AND col_name = \'' . $this->dbi->escapeString($orig_col_name) + . '\''; + } + if (! $this->dbi->tryQuery($query, DatabaseInterface::CONNECT_CONTROL)) { + return Message::error( + $this->dbi->getError(DatabaseInterface::CONNECT_CONTROL) + ); + } + return true; + } + + /** + * Update Multiple column in central columns list if a change is requested + * + * @param array $params Request parameters + * @return true|Message + */ + public function updateMultipleColumn(array $params) + { + $columnDefault = $params['field_default_type']; + $columnIsNull = []; + $columnExtra = []; + $numberCentralFields = count($params['orig_col_name']); + for ($i = 0; $i < $numberCentralFields; $i++) { + $columnIsNull[$i] = isset($params['field_null'][$i]) ? 1 : 0; + $columnExtra[$i] = $params['col_extra'][$i] ?? ''; + + if ($columnDefault[$i] === 'NONE') { + $columnDefault[$i] = ''; + } elseif ($columnDefault[$i] === 'USER_DEFINED') { + $columnDefault[$i] = $params['field_default_value'][$i]; + } + + $message = $this->updateOneColumn( + $params['db'], + $params['orig_col_name'][$i], + $params['field_name'][$i], + $params['field_type'][$i], + $params['field_attribute'][$i], + $params['field_length'][$i], + $columnIsNull[$i], + $params['field_collation'][$i], + $columnExtra[$i], + $columnDefault[$i] + ); + if (! is_bool($message)) { + return $message; + } + } + return true; + } + + /** + * Function generate and return the table header for + * multiple edit central columns page + * + * @param array $headers headers list + * + * @return string html for table header in central columns multi edit page + */ + private function getEditTableHeader(array $headers): string + { + return $this->template->render('database/central_columns/edit_table_header', [ + 'headers' => $headers, + ]); + } + + /** + * build html for editing a row in central columns table + * + * @param array $row array contains complete information of a + * particular row of central list table + * @param int $row_num position the row in the table + * + * @return string html of a particular row in the central columns table. + */ + private function getHtmlForEditTableRow(array $row, int $row_num): string + { + $tableHtml = '' + . '' + . '' + . $this->template->render('columns_definitions/column_name', [ + 'column_number' => $row_num, + 'ci' => 0, + 'ci_offset' => 0, + 'column_meta' => [ + 'Field' => $row['col_name'], + ], + 'cfg_relation' => [ + 'centralcolumnswork' => false, + ], + 'max_rows' => $this->maxRows, + ]) + . ''; + $tableHtml .= + '' + . $this->template->render('columns_definitions/column_type', [ + 'column_number' => $row_num, + 'ci' => 1, + 'ci_offset' => 0, + 'type_upper' => mb_strtoupper($row['col_type']), + 'column_meta' => [], + ]) + . ''; + $tableHtml .= + '' + . $this->template->render('columns_definitions/column_length', [ + 'column_number' => $row_num, + 'ci' => 2, + 'ci_offset' => 0, + 'length_values_input_size' => 8, + 'length_to_display' => $row['col_length'], + ]) + . ''; + $meta = []; + if (! isset($row['col_default']) || $row['col_default'] == '') { + $meta['DefaultType'] = 'NONE'; + } elseif ($row['col_default'] == 'CURRENT_TIMESTAMP' + || $row['col_default'] == 'current_timestamp()' + ) { + $meta['DefaultType'] = 'CURRENT_TIMESTAMP'; + } elseif ($row['col_default'] == 'NULL') { + $meta['DefaultType'] = $row['col_default']; + } else { + $meta['DefaultType'] = 'USER_DEFINED'; + $meta['DefaultValue'] = $row['col_default']; + } + $tableHtml .= + '' + . $this->template->render('columns_definitions/column_default', [ + 'column_number' => $row_num, + 'ci' => 3, + 'ci_offset' => 0, + 'type_upper' => mb_strtoupper((string) $row['col_default']), + 'column_meta' => $meta, + 'char_editing' => $this->charEditing, + ]) + . ''; + $tableHtml .= ''; + $tableHtml .= '' . "\n"; + $tableHtml .= ''; + $tableHtml .= + '' + . $this->template->render('columns_definitions/column_attribute', [ + 'column_number' => $row_num, + 'ci' => 5, + 'ci_offset' => 0, + 'extracted_columnspec' => [ + 'attribute' => $row['col_attribute'], + ], + 'column_meta' => [], + 'submit_attribute' => false, + 'attribute_types' => $this->dbi->types->getAttributes(), + ]) + . ''; + $tableHtml .= + '' + . $this->template->render('columns_definitions/column_null', [ + 'column_number' => $row_num, + 'ci' => 6, + 'ci_offset' => 0, + 'column_meta' => [ + 'Null' => $row['col_isNull'], + ], + ]) + . ''; + + $tableHtml .= + '' + . $this->template->render('columns_definitions/column_extra', [ + 'column_number' => $row_num, + 'ci' => 7, + 'ci_offset' => 0, + 'column_meta' => ['Extra' => $row['col_extra']], + ]) + . ''; + $tableHtml .= ''; + return $tableHtml; + } + + /** + * get the list of columns in given database excluding + * the columns present in current table + * + * @param string $db selected database + * @param string $table current table name + * + * @return array encoded list of columns present in central list for the given + * database + */ + public function getListRaw(string $db, string $table): array + { + $cfgCentralColumns = $this->getParams(); + if (empty($cfgCentralColumns)) { + return []; + } + $centralTable = $cfgCentralColumns['table']; + if (empty($table) || $table == '') { + $query = 'SELECT * FROM ' . Util::backquote($centralTable) . ' ' + . 'WHERE db_name = \'' . $this->dbi->escapeString($db) . '\';'; + } else { + $this->dbi->selectDb($db); + $columns = (array) $this->dbi->getColumnNames( + $db, + $table + ); + $cols = ''; + foreach ($columns as $col_select) { + $cols .= '\'' . $this->dbi->escapeString($col_select) . '\','; + } + $cols = trim($cols, ','); + $query = 'SELECT * FROM ' . Util::backquote($centralTable) . ' ' + . 'WHERE db_name = \'' . $this->dbi->escapeString($db) . '\''; + if ($cols) { + $query .= ' AND col_name NOT IN (' . $cols . ')'; + } + $query .= ';'; + } + $this->dbi->selectDb($cfgCentralColumns['db'], DatabaseInterface::CONNECT_CONTROL); + $columns_list = (array) $this->dbi->fetchResult( + $query, + null, + null, + DatabaseInterface::CONNECT_CONTROL + ); + $this->handleColumnExtra($columns_list); + return $columns_list; + } + + /** + * Get HTML for "check all" check box with "with selected" dropdown + * + * @param string $pmaThemeImage pma theme image url + * @param string $text_dir url for text directory + * + * @return string + */ + public function getTableFooter(string $pmaThemeImage, string $text_dir): string + { + $html_output = $this->template->render('select_all', [ + 'pma_theme_image' => $pmaThemeImage, + 'text_dir' => $text_dir, + 'form_name' => 'tableslistcontainer', + ]); + $html_output .= Util::getButtonOrImage( + 'edit_central_columns', + 'mult_submit change_central_columns', + __('Edit'), + 'b_edit', + 'edit central columns' + ); + $html_output .= Util::getButtonOrImage( + 'delete_central_columns', + 'mult_submit', + __('Delete'), + 'b_drop', + 'remove_from_central_columns' + ); + return $html_output; + } + + /** + * function generate and return the table footer for + * multiple edit central columns page + * + * @return string html for table footer in central columns multi edit page + */ + private function getEditTableFooter(): string + { + return '
' + . '' + . '
'; + } + + /** + * Column `col_extra` is used to store both extra and attributes for a column. + * This method separates them. + * + * @param array $columns_list columns list + * + * @return void + */ + private function handleColumnExtra(array &$columns_list): void + { + foreach ($columns_list as &$row) { + $vals = explode(',', $row['col_extra']); + + if (in_array('BINARY', $vals)) { + $row['col_attribute'] = 'BINARY'; + } elseif (in_array('UNSIGNED', $vals)) { + $row['col_attribute'] = 'UNSIGNED'; + } elseif (in_array('UNSIGNED ZEROFILL', $vals)) { + $row['col_attribute'] = 'UNSIGNED ZEROFILL'; + } elseif (in_array('on update CURRENT_TIMESTAMP', $vals)) { + $row['col_attribute'] = 'on update CURRENT_TIMESTAMP'; + } else { + $row['col_attribute'] = ''; + } + + if (in_array('auto_increment', $vals)) { + $row['col_extra'] = 'auto_increment'; + } else { + $row['col_extra'] = ''; + } + } + } + + /** + * Get HTML for editing page central columns + * + * @param array $selected_fld Array containing the selected fields + * @param string $selected_db String containing the name of database + * + * @return string HTML for complete editing page for central columns + */ + public function getHtmlForEditingPage(array $selected_fld, string $selected_db): string + { + $html = '
'; + $header_cells = [ + __('Name'), + __('Type'), + __('Length/Values'), + __('Default'), + __('Collation'), + __('Attributes'), + __('Null'), + __('A_I'), + ]; + $html .= $this->getEditTableHeader($header_cells); + $selected_fld_safe = []; + foreach ($selected_fld as $key) { + $selected_fld_safe[] = $this->dbi->escapeString($key); + } + $columns_list = implode("','", $selected_fld_safe); + $columns_list = "'" . $columns_list . "'"; + $list_detail_cols = $this->findExistingColNames($selected_db, $columns_list, true); + $row_num = 0; + foreach ($list_detail_cols as $row) { + $tableHtmlRow = $this->getHtmlForEditTableRow( + $row, + $row_num + ); + $html .= $tableHtmlRow; + $row_num++; + } + $html .= ''; + $html .= $this->getEditTableFooter(); + $html .= '
'; + return $html; + } + + /** + * get number of columns of given database from central columns list + * starting at offset $from + * + * @param string $db selected database + * @param int $from starting offset of first result + * @param int $num maximum number of results to return + * + * @return int count of $num columns present in central columns list + * starting at offset $from for the given database + */ + public function getColumnsCount(string $db, int $from = 0, int $num = 25): int + { + $cfgCentralColumns = $this->getParams(); + if (empty($cfgCentralColumns)) { + return 0; + } + $pmadb = $cfgCentralColumns['db']; + $this->dbi->selectDb($pmadb, DatabaseInterface::CONNECT_CONTROL); + $central_list_table = $cfgCentralColumns['table']; + //get current values of $db from central column list + $query = 'SELECT COUNT(db_name) FROM ' . Util::backquote($central_list_table) . ' ' + . 'WHERE db_name = \'' . $this->dbi->escapeString($db) . '\'' . + ($num === 0 ? '' : 'LIMIT ' . $from . ', ' . $num) . ';'; + $result = (array) $this->dbi->fetchResult( + $query, + null, + null, + DatabaseInterface::CONNECT_CONTROL + ); + + if (isset($result[0])) { + return (int) $result[0]; + } + + return -1; + } + + /** + * build dropdown select html to select column in selected table, + * include only columns which are not already in central list + * + * @param string $db current database to which selected table belongs + * @param string $selected_tbl selected table + * + * @return string html to select column + */ + public function getHtmlForColumnDropdown($db, $selected_tbl) + { + $existing_cols = $this->getFromTable($db, $selected_tbl); + $this->dbi->selectDb($db); + $columns = (array) $this->dbi->getColumnNames( + $db, + $selected_tbl + ); + $selectColHtml = ""; + foreach ($columns as $column) { + if (! in_array($column, $existing_cols)) { + $selectColHtml .= ''; + } + } + return $selectColHtml; + } + + /** + * build html for adding a new user defined column to central list + * + * @param string $db current database + * @param int $total_rows number of rows in central columns + * @param int $pos offset of first result with complete result set + * @param string $pmaThemeImage table footer theme image directorie + * @param string $text_dir table footer arrow direction + * + * @return string html of the form to let user add a new user defined column to the + * list + */ + public function getHtmlForMain( + string $db, + int $total_rows, + int $pos, + string $pmaThemeImage, + string $text_dir + ): string { + $max_rows = $this->maxRows; + $attribute_types = $this->dbi->types->getAttributes(); + + $tn_pageNow = ($pos / $this->maxRows) + 1; + $tn_nbTotalPage = ceil($total_rows / $this->maxRows); + $tn_page_selector = $tn_nbTotalPage > 1 ? Util::pageselector( + 'pos', + $this->maxRows, + $tn_pageNow, + $tn_nbTotalPage + ) : ''; + $this->dbi->selectDb($db); + $tables = $this->dbi->getTables($db); + $rows_list = $this->getColumnsList($db, $pos, $max_rows); + + $rows_meta = []; + $types_upper = []; + $row_num = 0; + foreach ($rows_list as $row) { + $rows_meta[$row_num] = []; + if (! isset($row['col_default']) || $row['col_default'] == '') { + $rows_meta[$row_num]['DefaultType'] = 'NONE'; + } else { + if ($row['col_default'] == 'CURRENT_TIMESTAMP' + || $row['col_default'] == 'current_timestamp()' + ) { + $rows_meta[$row_num]['DefaultType'] = 'CURRENT_TIMESTAMP'; + } elseif ($row['col_default'] == 'NULL') { + $rows_meta[$row_num]['DefaultType'] = $row['col_default']; + } else { + $rows_meta[$row_num]['DefaultType'] = 'USER_DEFINED'; + $rows_meta[$row_num]['DefaultValue'] = $row['col_default']; + } + } + $types_upper[$row_num] = mb_strtoupper((string) $row['col_type']); + $row_num++; + } + + $charsets = Charsets::getCharsets($this->dbi, $this->disableIs); + $collations = Charsets::getCollations($this->dbi, $this->disableIs); + $charsetsList = []; + /** @var Charset $charset */ + foreach ($charsets as $charset) { + $collationsList = []; + /** @var Collation $collation */ + foreach ($collations[$charset->getName()] as $collation) { + $collationsList[] = [ + 'name' => $collation->getName(), + 'description' => $collation->getDescription(), + ]; + } + $charsetsList[] = [ + 'name' => $charset->getName(), + 'description' => $charset->getDescription(), + 'collations' => $collationsList, + ]; + } + + return $this->template->render('database/central_columns/main', [ + "db" => $db, + "total_rows" => $total_rows, + "max_rows" => $max_rows, + "pos" => $pos, + "char_editing" => $this->charEditing, + "attribute_types" => $attribute_types, + "tn_nbTotalPage" => $tn_nbTotalPage, + "tn_page_selector" => $tn_page_selector, + "tables" => $tables, + "rows_list" => $rows_list, + "rows_meta" => $rows_meta, + "types_upper" => $types_upper, + "pmaThemeImage" => $pmaThemeImage, + "text_dir" => $text_dir, + 'charsets' => $charsetsList, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Charsets.php b/srcs/phpmyadmin/libraries/classes/Charsets.php new file mode 100644 index 0000000..740ab09 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Charsets.php @@ -0,0 +1,210 @@ + 'big5', + 'cp-866' => 'cp866', + 'euc-jp' => 'ujis', + 'euc-kr' => 'euckr', + 'gb2312' => 'gb2312', + 'gbk' => 'gbk', + 'iso-8859-1' => 'latin1', + 'iso-8859-2' => 'latin2', + 'iso-8859-7' => 'greek', + 'iso-8859-8' => 'hebrew', + 'iso-8859-8-i' => 'hebrew', + 'iso-8859-9' => 'latin5', + 'iso-8859-13' => 'latin7', + 'iso-8859-15' => 'latin1', + 'koi8-r' => 'koi8r', + 'shift_jis' => 'sjis', + 'tis-620' => 'tis620', + 'utf-8' => 'utf8', + 'windows-1250' => 'cp1250', + 'windows-1251' => 'cp1251', + 'windows-1252' => 'latin1', + 'windows-1256' => 'cp1256', + 'windows-1257' => 'cp1257', + ]; + + /** + * The charset for the server + * @var Charset|null + */ + private static $serverCharset = null; + + /** + * @var array + */ + private static $charsets = []; + + /** + * @var array> + */ + private static $collations = []; + + /** + * Loads charset data from the server + * + * @param DatabaseInterface $dbi DatabaseInterface instance + * @param boolean $disableIs Disable use of INFORMATION_SCHEMA + * + * @return void + */ + private static function loadCharsets(DatabaseInterface $dbi, bool $disableIs): void + { + /* Data already loaded */ + if (count(self::$charsets) > 0) { + return; + } + + if ($disableIs) { + $sql = 'SHOW CHARACTER SET'; + } else { + $sql = 'SELECT `CHARACTER_SET_NAME` AS `Charset`,' + . ' `DEFAULT_COLLATE_NAME` AS `Default collation`,' + . ' `DESCRIPTION` AS `Description`,' + . ' `MAXLEN` AS `Maxlen`' + . ' FROM `information_schema`.`CHARACTER_SETS`'; + } + $res = $dbi->query($sql); + + self::$charsets = []; + while ($row = $dbi->fetchAssoc($res)) { + self::$charsets[$row['Charset']] = Charset::fromServer($row); + } + $dbi->freeResult($res); + + ksort(self::$charsets, SORT_STRING); + } + + /** + * Loads collation data from the server + * + * @param DatabaseInterface $dbi DatabaseInterface instance + * @param boolean $disableIs Disable use of INFORMATION_SCHEMA + * + * @return void + */ + private static function loadCollations(DatabaseInterface $dbi, bool $disableIs): void + { + /* Data already loaded */ + if (count(self::$collations) > 0) { + return; + } + + if ($disableIs) { + $sql = 'SHOW COLLATION'; + } else { + $sql = 'SELECT `COLLATION_NAME` AS `Collation`,' + . ' `CHARACTER_SET_NAME` AS `Charset`,' + . ' `ID` AS `Id`,' + . ' `IS_DEFAULT` AS `Default`,' + . ' `IS_COMPILED` AS `Compiled`,' + . ' `SORTLEN` AS `Sortlen`' + . ' FROM `information_schema`.`COLLATIONS`'; + } + $res = $dbi->query($sql); + + self::$collations = []; + while ($row = $dbi->fetchAssoc($res)) { + self::$collations[$row['Charset']][$row['Collation']] = Collation::fromServer($row); + } + $dbi->freeResult($res); + + foreach (array_keys(self::$collations) as $charset) { + ksort(self::$collations[$charset], SORT_STRING); + } + } + + /** + * Get current server charset + * + * @param DatabaseInterface $dbi DatabaseInterface instance + * @param boolean $disableIs Disable use of INFORMATION_SCHEMA + * + * @return Charset + */ + public static function getServerCharset(DatabaseInterface $dbi, bool $disableIs): Charset + { + if (self::$serverCharset !== null) { + return self::$serverCharset; + } + self::loadCharsets($dbi, $disableIs); + $serverCharset = $dbi->getVariable('character_set_server'); + if (! is_string($serverCharset)) {// MySQL 5.7.8 fallback, issue #15614 + $serverCharset = $dbi->fetchValue("SELECT @@character_set_server;"); + } + self::$serverCharset = self::$charsets[$serverCharset]; + return self::$serverCharset; + } + + /** + * Get all server charsets + * + * @param DatabaseInterface $dbi DatabaseInterface instance + * @param boolean $disableIs Disable use of INFORMATION_SCHEMA + * + * @return array + */ + public static function getCharsets(DatabaseInterface $dbi, bool $disableIs): array + { + self::loadCharsets($dbi, $disableIs); + return self::$charsets; + } + + /** + * Get all server collations + * + * @param DatabaseInterface $dbi DatabaseInterface instance + * @param boolean $disableIs Disable use of INFORMATION_SCHEMA + * + * @return array + */ + public static function getCollations(DatabaseInterface $dbi, bool $disableIs): array + { + self::loadCollations($dbi, $disableIs); + return self::$collations; + } + + /** + * @param DatabaseInterface $dbi DatabaseInterface instance + * @param bool $disableIs Disable use of INFORMATION_SCHEMA + * @param string|null $name Collation name + * + * @return Collation|null + */ + public static function findCollationByName(DatabaseInterface $dbi, bool $disableIs, ?string $name): ?Collation + { + $pieces = explode('_', (string) $name); + if ($pieces === false || ! isset($pieces[0])) { + return null; + } + $charset = $pieces[0]; + $collations = self::getCollations($dbi, $disableIs); + return $collations[$charset][$name] ?? null; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Charsets/Charset.php b/srcs/phpmyadmin/libraries/classes/Charsets/Charset.php new file mode 100644 index 0000000..0b25790 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Charsets/Charset.php @@ -0,0 +1,103 @@ +name = $name; + $this->description = $description; + $this->defaultCollation = $defaultCollation; + $this->maxLength = $maxLength; + } + + /** + * @param array $state State obtained from the database server + * @return Charset + */ + public static function fromServer(array $state): self + { + return new self( + $state['Charset'] ?? '', + $state['Description'] ?? '', + $state['Default collation'] ?? '', + (int) ($state['Maxlen'] ?? 0) + ); + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return string + */ + public function getDescription(): string + { + return $this->description; + } + + /** + * @return string + */ + public function getDefaultCollation(): string + { + return $this->defaultCollation; + } + + /** + * @return int + */ + public function getMaxLength(): int + { + return $this->maxLength; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Charsets/Collation.php b/srcs/phpmyadmin/libraries/classes/Charsets/Collation.php new file mode 100644 index 0000000..fa2b85f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Charsets/Collation.php @@ -0,0 +1,549 @@ +name = $name; + $this->charset = $charset; + $this->id = $id; + $this->isDefault = $isDefault; + $this->isCompiled = $isCompiled; + $this->sortLength = $sortLength; + $this->padAttribute = $padAttribute; + $this->description = $this->buildDescription(); + } + + /** + * @param array $state State obtained from the database server + * @return self + */ + public static function fromServer(array $state): self + { + return new self( + $state['Collation'] ?? '', + $state['Charset'] ?? '', + (int) ($state['Id'] ?? 0), + isset($state['Default']) && ($state['Default'] === 'Yes' || $state['Default'] === '1'), + isset($state['Compiled']) && ($state['Compiled'] === 'Yes' || $state['Compiled'] === '1'), + (int) ($state['Sortlen'] ?? 0), + $state['Pad_attribute'] ?? '' + ); + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return string + */ + public function getDescription(): string + { + return $this->description; + } + + /** + * @return string + */ + public function getCharset(): string + { + return $this->charset; + } + + /** + * @return int + */ + public function getId(): int + { + return $this->id; + } + + /** + * @return bool + */ + public function isDefault(): bool + { + return $this->isDefault; + } + + /** + * @return bool + */ + public function isCompiled(): bool + { + return $this->isCompiled; + } + + /** + * @return int + */ + public function getSortLength(): int + { + return $this->sortLength; + } + + /** + * @return string + */ + public function getPadAttribute(): string + { + return $this->padAttribute; + } + + /** + * Returns description for given collation + * + * @return string collation description + */ + private function buildDescription(): string + { + $parts = explode('_', $this->getName()); + + $name = __('Unknown'); + $variant = null; + $suffixes = []; + $unicode = false; + $unknown = false; + + $level = 0; + foreach ($parts as $part) { + if ($level == 0) { + /* Next will be language */ + $level = 1; + /* First should be charset */ + switch ($part) { + case 'binary': + $name = _pgettext('Collation', 'Binary'); + break; + // Unicode charsets + case 'utf8mb4': + $variant = 'UCA 4.0.0'; + // Fall through to other unicode + case 'ucs2': + case 'utf8': + case 'utf16': + case 'utf16le': + case 'utf16be': + case 'utf32': + $name = _pgettext('Collation', 'Unicode'); + $unicode = true; + break; + // West European charsets + case 'ascii': + case 'cp850': + case 'dec8': + case 'hp8': + case 'latin1': + case 'macroman': + $name = _pgettext('Collation', 'West European'); + break; + // Central European charsets + case 'cp1250': + case 'cp852': + case 'latin2': + case 'macce': + $name = _pgettext('Collation', 'Central European'); + break; + // Russian charsets + case 'cp866': + case 'koi8r': + $name = _pgettext('Collation', 'Russian'); + break; + // Chinese charsets + case 'gb2312': + case 'gbk': + $name = _pgettext('Collation', 'Simplified Chinese'); + break; + case 'big5': + $name = _pgettext('Collation', 'Traditional Chinese'); + break; + case 'gb18030': + $name = _pgettext('Collation', 'Chinese'); + $unicode = true; + break; + // Japanese charsets + case 'sjis': + case 'ujis': + case 'cp932': + case 'eucjpms': + $name = _pgettext('Collation', 'Japanese'); + break; + // Baltic charsets + case 'cp1257': + case 'latin7': + $name = _pgettext('Collation', 'Baltic'); + break; + // Other + case 'armscii8': + case 'armscii': + $name = _pgettext('Collation', 'Armenian'); + break; + case 'cp1251': + $name = _pgettext('Collation', 'Cyrillic'); + break; + case 'cp1256': + $name = _pgettext('Collation', 'Arabic'); + break; + case 'euckr': + $name = _pgettext('Collation', 'Korean'); + break; + case 'hebrew': + $name = _pgettext('Collation', 'Hebrew'); + break; + case 'geostd8': + $name = _pgettext('Collation', 'Georgian'); + break; + case 'greek': + $name = _pgettext('Collation', 'Greek'); + break; + case 'keybcs2': + $name = _pgettext('Collation', 'Czech-Slovak'); + break; + case 'koi8u': + $name = _pgettext('Collation', 'Ukrainian'); + break; + case 'latin5': + $name = _pgettext('Collation', 'Turkish'); + break; + case 'swe7': + $name = _pgettext('Collation', 'Swedish'); + break; + case 'tis620': + $name = _pgettext('Collation', 'Thai'); + break; + default: + $name = _pgettext('Collation', 'Unknown'); + $unknown = true; + break; + } + continue; + } + if ($level == 1) { + /* Next will be variant unless changed later */ + $level = 4; + /* Locale name or code */ + $found = true; + switch ($part) { + case 'general': + break; + case 'bulgarian': + case 'bg': + $name = _pgettext('Collation', 'Bulgarian'); + break; + case 'chinese': + case 'cn': + case 'zh': + if ($unicode) { + $name = _pgettext('Collation', 'Chinese'); + } + break; + case 'croatian': + case 'hr': + $name = _pgettext('Collation', 'Croatian'); + break; + case 'czech': + case 'cs': + $name = _pgettext('Collation', 'Czech'); + break; + case 'danish': + case 'da': + $name = _pgettext('Collation', 'Danish'); + break; + case 'english': + case 'en': + $name = _pgettext('Collation', 'English'); + break; + case 'esperanto': + case 'eo': + $name = _pgettext('Collation', 'Esperanto'); + break; + case 'estonian': + case 'et': + $name = _pgettext('Collation', 'Estonian'); + break; + case 'german1': + $name = _pgettext('Collation', 'German (dictionary order)'); + break; + case 'german2': + $name = _pgettext('Collation', 'German (phone book order)'); + break; + case 'german': + case 'de': + /* Name is set later */ + $level = 2; + break; + case 'hungarian': + case 'hu': + $name = _pgettext('Collation', 'Hungarian'); + break; + case 'icelandic': + case 'is': + $name = _pgettext('Collation', 'Icelandic'); + break; + case 'japanese': + case 'ja': + $name = _pgettext('Collation', 'Japanese'); + break; + case 'la': + $name = _pgettext('Collation', 'Classical Latin'); + break; + case 'latvian': + case 'lv': + $name = _pgettext('Collation', 'Latvian'); + break; + case 'lithuanian': + case 'lt': + $name = _pgettext('Collation', 'Lithuanian'); + break; + case 'korean': + case 'ko': + $name = _pgettext('Collation', 'Korean'); + break; + case 'myanmar': + case 'my': + $name = _pgettext('Collation', 'Burmese'); + break; + case 'persian': + $name = _pgettext('Collation', 'Persian'); + break; + case 'polish': + case 'pl': + $name = _pgettext('Collation', 'Polish'); + break; + case 'roman': + $name = _pgettext('Collation', 'West European'); + break; + case 'romanian': + case 'ro': + $name = _pgettext('Collation', 'Romanian'); + break; + case 'ru': + $name = _pgettext('Collation', 'Russian'); + break; + case 'si': + case 'sinhala': + $name = _pgettext('Collation', 'Sinhalese'); + break; + case 'slovak': + case 'sk': + $name = _pgettext('Collation', 'Slovak'); + break; + case 'slovenian': + case 'sl': + $name = _pgettext('Collation', 'Slovenian'); + break; + case 'spanish': + $name = _pgettext('Collation', 'Spanish (modern)'); + break; + case 'es': + /* Name is set later */ + $level = 3; + break; + case 'spanish2': + $name = _pgettext('Collation', 'Spanish (traditional)'); + break; + case 'swedish': + case 'sv': + $name = _pgettext('Collation', 'Swedish'); + break; + case 'thai': + case 'th': + $name = _pgettext('Collation', 'Thai'); + break; + case 'turkish': + case 'tr': + $name = _pgettext('Collation', 'Turkish'); + break; + case 'ukrainian': + case 'uk': + $name = _pgettext('Collation', 'Ukrainian'); + break; + case 'vietnamese': + case 'vi': + $name = _pgettext('Collation', 'Vietnamese'); + break; + case 'unicode': + if ($unknown) { + $name = _pgettext('Collation', 'Unicode'); + } + break; + default: + $found = false; + } + if ($found) { + continue; + } + // Not parsed token, fall to next level + } + if ($level == 2) { + /* Next will be variant */ + $level = 4; + /* Germal variant */ + if ($part == 'pb') { + $name = _pgettext('Collation', 'German (phone book order)'); + continue; + } + $name = _pgettext('Collation', 'German (dictionary order)'); + // Not parsed token, fall to next level + } + if ($level == 3) { + /* Next will be variant */ + $level = 4; + /* Spanish variant */ + if ($part == 'trad') { + $name = _pgettext('Collation', 'Spanish (traditional)'); + continue; + } + $name = _pgettext('Collation', 'Spanish (modern)'); + // Not parsed token, fall to next level + } + if ($level == 4) { + /* Next will be suffix */ + $level = 5; + /* Variant */ + $found = true; + switch ($part) { + case '0900': + $variant = 'UCA 9.0.0'; + break; + case '520': + $variant = 'UCA 5.2.0'; + break; + case 'mysql561': + $variant = 'MySQL 5.6.1'; + break; + case 'mysql500': + $variant = 'MySQL 5.0.0'; + break; + default: + $found = false; + } + if ($found) { + continue; + } + // Not parsed token, fall to next level + } + if ($level == 5) { + /* Suffixes */ + switch ($part) { + case 'ci': + $suffixes[] = _pgettext('Collation variant', 'case-insensitive'); + break; + case 'cs': + $suffixes[] = _pgettext('Collation variant', 'case-sensitive'); + break; + case 'ai': + $suffixes[] = _pgettext('Collation variant', 'accent-insensitive'); + break; + case 'as': + $suffixes[] = _pgettext('Collation variant', 'accent-sensitive'); + break; + case 'ks': + $suffixes[] = _pgettext('Collation variant', 'kana-sensitive'); + break; + case 'w2': + case 'l2': + $suffixes[] = _pgettext('Collation variant', 'multi-level'); + break; + case 'bin': + $suffixes[] = _pgettext('Collation variant', 'binary'); + break; + case 'nopad': + $suffixes[] = _pgettext('Collation variant', 'no-pad'); + break; + } + } + } + + $result = $name; + if ($variant !== null) { + $result .= ' (' . $variant . ')'; + } + if (count($suffixes) > 0) { + $result .= ', ' . implode(', ', $suffixes); + } + return $result; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/CheckUserPrivileges.php b/srcs/phpmyadmin/libraries/classes/CheckUserPrivileges.php new file mode 100644 index 0000000..0614675 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/CheckUserPrivileges.php @@ -0,0 +1,372 @@ +dbi = $dbi; + } + + /** + * Extracts details from a result row of a SHOW GRANT query + * + * @param string $row grant row + * + * @return array + */ + public function getItemsFromShowGrantsRow(string $row): array + { + $db_name_offset = mb_strpos($row, ' ON ') + 4; + + $tblname_end_offset = mb_strpos($row, ' TO '); + $tblname_start_offset = false; + + if (($__tblname_start_offset = mb_strpos($row, '`.', $db_name_offset)) + && $__tblname_start_offset + < $tblname_end_offset) { + $tblname_start_offset = $__tblname_start_offset + 1; + } + + if (! $tblname_start_offset) { + $tblname_start_offset = mb_strpos($row, '.', $db_name_offset); + } + + $show_grants_dbname = mb_substr( + $row, + $db_name_offset, + $tblname_start_offset - $db_name_offset + ); + + $show_grants_dbname = Util::unQuote($show_grants_dbname, '`'); + + $show_grants_str = mb_substr( + $row, + 6, + mb_strpos($row, ' ON ') - 6 + ); + + $show_grants_tblname = mb_substr( + $row, + $tblname_start_offset + 1, + $tblname_end_offset - $tblname_start_offset - 1 + ); + $show_grants_tblname = Util::unQuote($show_grants_tblname, '`'); + + return [ + $show_grants_str, + $show_grants_dbname, + $show_grants_tblname, + ]; + } + + /** + * Check if user has required privileges for + * performing 'Adjust privileges' operations + * + * @param string $show_grants_str string containing grants for user + * @param string $show_grants_dbname name of db extracted from grant string + * @param string $show_grants_tblname name of table extracted from grant string + * + * @return void + */ + public function checkRequiredPrivilegesForAdjust( + string $show_grants_str, + string $show_grants_dbname, + string $show_grants_tblname + ): void { + // '... ALL PRIVILEGES ON *.* ...' OR '... ALL PRIVILEGES ON `mysql`.* ..' + // OR + // SELECT, INSERT, UPDATE, DELETE .... ON *.* OR `mysql`.* + if ($show_grants_str == 'ALL' + || $show_grants_str == 'ALL PRIVILEGES' + || (mb_strpos( + $show_grants_str, + 'SELECT, INSERT, UPDATE, DELETE' + ) !== false) + ) { + if ($show_grants_dbname == '*' + && $show_grants_tblname == '*' + ) { + $GLOBALS['col_priv'] = true; + $GLOBALS['db_priv'] = true; + $GLOBALS['proc_priv'] = true; + $GLOBALS['table_priv'] = true; + + if ($show_grants_str == 'ALL PRIVILEGES' + || $show_grants_str == 'ALL' + ) { + $GLOBALS['is_reload_priv'] = true; + } + } + + // check for specific tables in `mysql` db + // Ex. '... ALL PRIVILEGES on `mysql`.`columns_priv` .. ' + if ($show_grants_dbname == 'mysql') { + switch ($show_grants_tblname) { + case "columns_priv": + $GLOBALS['col_priv'] = true; + break; + case "db": + $GLOBALS['db_priv'] = true; + break; + case "procs_priv": + $GLOBALS['proc_priv'] = true; + break; + case "tables_priv": + $GLOBALS['table_priv'] = true; + break; + case "*": + $GLOBALS['col_priv'] = true; + $GLOBALS['db_priv'] = true; + $GLOBALS['proc_priv'] = true; + $GLOBALS['table_priv'] = true; + break; + default: + } + } + } + } + + /** + * sets privilege information extracted from SHOW GRANTS result + * + * Detection for some CREATE privilege. + * + * Since MySQL 4.1.2, we can easily detect current user's grants using $userlink + * (no control user needed) and we don't have to try any other method for + * detection + * + * @todo fix to get really all privileges, not only explicitly defined for this user + * from MySQL manual: (https://dev.mysql.com/doc/refman/5.0/en/show-grants.html) + * SHOW GRANTS displays only the privileges granted explicitly to the named + * account. Other privileges might be available to the account, but they are not + * displayed. For example, if an anonymous account exists, the named account + * might be able to use its privileges, but SHOW GRANTS will not display them. + * + * @return void + */ + private function analyseShowGrant(): void + { + if (Util::cacheExists('is_create_db_priv')) { + $GLOBALS['is_create_db_priv'] = Util::cacheGet( + 'is_create_db_priv' + ); + $GLOBALS['is_reload_priv'] = Util::cacheGet( + 'is_reload_priv' + ); + $GLOBALS['db_to_create'] = Util::cacheGet( + 'db_to_create' + ); + $GLOBALS['dbs_where_create_table_allowed'] = Util::cacheGet( + 'dbs_where_create_table_allowed' + ); + $GLOBALS['dbs_to_test'] = Util::cacheGet( + 'dbs_to_test' + ); + + $GLOBALS['db_priv'] = Util::cacheGet( + 'db_priv' + ); + $GLOBALS['col_priv'] = Util::cacheGet( + 'col_priv' + ); + $GLOBALS['table_priv'] = Util::cacheGet( + 'table_priv' + ); + $GLOBALS['proc_priv'] = Util::cacheGet( + 'proc_priv' + ); + + return; + } + + // defaults + $GLOBALS['is_create_db_priv'] = false; + $GLOBALS['is_reload_priv'] = false; + $GLOBALS['db_to_create'] = ''; + $GLOBALS['dbs_where_create_table_allowed'] = []; + $GLOBALS['dbs_to_test'] = $this->dbi->getSystemSchemas(); + $GLOBALS['proc_priv'] = false; + $GLOBALS['db_priv'] = false; + $GLOBALS['col_priv'] = false; + $GLOBALS['table_priv'] = false; + + $rs_usr = $this->dbi->tryQuery('SHOW GRANTS'); + + if (! $rs_usr) { + return; + } + + $re0 = '(^|(\\\\\\\\)+|[^\\\\])'; // non-escaped wildcards + $re1 = '(^|[^\\\\])(\\\)+'; // escaped wildcards + + while ($row = $this->dbi->fetchRow($rs_usr)) { + list( + $show_grants_str, + $show_grants_dbname, + $show_grants_tblname + ) = $this->getItemsFromShowGrantsRow($row[0]); + + if ($show_grants_dbname == '*') { + if ($show_grants_str != 'USAGE') { + $GLOBALS['dbs_to_test'] = false; + } + } elseif ($GLOBALS['dbs_to_test'] !== false) { + $GLOBALS['dbs_to_test'][] = $show_grants_dbname; + } + + if (mb_strpos($show_grants_str, 'RELOAD') !== false) { + $GLOBALS['is_reload_priv'] = true; + } + + // check for the required privileges for adjust + $this->checkRequiredPrivilegesForAdjust( + $show_grants_str, + $show_grants_dbname, + $show_grants_tblname + ); + + /** + * @todo if we find CREATE VIEW but not CREATE, do not offer + * the create database dialog box + */ + if ($show_grants_str == 'ALL' + || $show_grants_str == 'ALL PRIVILEGES' + || $show_grants_str == 'CREATE' + || strpos($show_grants_str, 'CREATE,') !== false + ) { + if ($show_grants_dbname == '*') { + // a global CREATE privilege + $GLOBALS['is_create_db_priv'] = true; + $GLOBALS['is_reload_priv'] = true; + $GLOBALS['db_to_create'] = ''; + $GLOBALS['dbs_where_create_table_allowed'][] = '*'; + // @todo we should not break here, cause GRANT ALL *.* + // could be revoked by a later rule like GRANT SELECT ON db.* + break; + } else { + // this array may contain wildcards + $GLOBALS['dbs_where_create_table_allowed'][] = $show_grants_dbname; + + $dbname_to_test = Util::backquote($show_grants_dbname); + + if ($GLOBALS['is_create_db_priv']) { + // no need for any more tests if we already know this + continue; + } + + // does this db exist? + if ((preg_match('/' . $re0 . '%|_/', $show_grants_dbname) + && ! preg_match('/\\\\%|\\\\_/', $show_grants_dbname)) + || (! $this->dbi->tryQuery( + 'USE ' . preg_replace( + '/' . $re1 . '(%|_)/', + '\\1\\3', + $dbname_to_test + ) + ) + && mb_substr($this->dbi->getError(), 1, 4) != 1044) + ) { + /** + * Do not handle the underscore wildcard + * (this case must be rare anyway) + */ + $GLOBALS['db_to_create'] = preg_replace( + '/' . $re0 . '%/', + '\\1', + $show_grants_dbname + ); + $GLOBALS['db_to_create'] = preg_replace( + '/' . $re1 . '(%|_)/', + '\\1\\3', + $GLOBALS['db_to_create'] + ); + $GLOBALS['is_create_db_priv'] = true; + + /** + * @todo collect $GLOBALS['db_to_create'] into an array, + * to display a drop-down in the "Create database" dialog + */ + // we don't break, we want all possible databases + //break; + } // end if + } // end elseif + } // end if + } // end while + + $this->dbi->freeResult($rs_usr); + + // must also cacheUnset() them in + // PhpMyAdmin\Plugins\Auth\AuthenticationCookie + Util::cacheSet('is_create_db_priv', $GLOBALS['is_create_db_priv']); + Util::cacheSet('is_reload_priv', $GLOBALS['is_reload_priv']); + Util::cacheSet('db_to_create', $GLOBALS['db_to_create']); + Util::cacheSet( + 'dbs_where_create_table_allowed', + $GLOBALS['dbs_where_create_table_allowed'] + ); + Util::cacheSet('dbs_to_test', $GLOBALS['dbs_to_test']); + + Util::cacheSet('proc_priv', $GLOBALS['proc_priv']); + Util::cacheSet('table_priv', $GLOBALS['table_priv']); + Util::cacheSet('col_priv', $GLOBALS['col_priv']); + Util::cacheSet('db_priv', $GLOBALS['db_priv']); + } + + /** + * Get user's global privileges and some db-specific privileges + * + * @return void + */ + public function getPrivileges(): void + { + $username = ''; + + $current = $this->dbi->getCurrentUserAndHost(); + if (! empty($current)) { + list($username, ) = $current; + } + + // If MySQL is started with --skip-grant-tables + if ($username === '') { + $GLOBALS['is_create_db_priv'] = true; + $GLOBALS['is_reload_priv'] = true; + $GLOBALS['db_to_create'] = ''; + $GLOBALS['dbs_where_create_table_allowed'] = ['*']; + $GLOBALS['dbs_to_test'] = false; + $GLOBALS['db_priv'] = true; + $GLOBALS['col_priv'] = true; + $GLOBALS['table_priv'] = true; + $GLOBALS['proc_priv'] = true; + } else { + $this->analyseShowGrant(); + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config.php b/srcs/phpmyadmin/libraries/classes/Config.php new file mode 100644 index 0000000..0002462 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config.php @@ -0,0 +1,1813 @@ +settings = ['is_setup' => false]; + + // functions need to refresh in case of config file changed goes in + // PhpMyAdmin\Config::load() + $this->load($source); + + // other settings, independent from config file, comes in + $this->checkSystem(); + + $this->base_settings = $this->settings; + } + + /** + * sets system and application settings + * + * @return void + */ + public function checkSystem(): void + { + $this->set('PMA_VERSION', '5.0.1'); + /* Major version */ + $this->set( + 'PMA_MAJOR_VERSION', + implode('.', array_slice(explode('.', $this->get('PMA_VERSION'), 3), 0, 2)) + ); + + $this->checkWebServerOs(); + $this->checkWebServer(); + $this->checkGd2(); + $this->checkClient(); + $this->checkUpload(); + $this->checkUploadSize(); + $this->checkOutputCompression(); + } + + /** + * whether to use gzip output compression or not + * + * @return void + */ + public function checkOutputCompression(): void + { + // If zlib output compression is set in the php configuration file, no + // output buffering should be run + if (ini_get('zlib.output_compression')) { + $this->set('OBGzip', false); + } + + // enable output-buffering (if set to 'auto') + if (strtolower((string) $this->get('OBGzip')) == 'auto') { + $this->set('OBGzip', true); + } + } + + /** + * Sets the client platform based on user agent + * + * @param string $user_agent the user agent + * + * @return void + */ + private function _setClientPlatform(string $user_agent): void + { + if (mb_strstr($user_agent, 'Win')) { + $this->set('PMA_USR_OS', 'Win'); + } elseif (mb_strstr($user_agent, 'Mac')) { + $this->set('PMA_USR_OS', 'Mac'); + } elseif (mb_strstr($user_agent, 'Linux')) { + $this->set('PMA_USR_OS', 'Linux'); + } elseif (mb_strstr($user_agent, 'Unix')) { + $this->set('PMA_USR_OS', 'Unix'); + } elseif (mb_strstr($user_agent, 'OS/2')) { + $this->set('PMA_USR_OS', 'OS/2'); + } else { + $this->set('PMA_USR_OS', 'Other'); + } + } + + /** + * Determines platform (OS), browser and version of the user + * Based on a phpBuilder article: + * + * @see http://www.phpbuilder.net/columns/tim20000821.php + * + * @return void + */ + public function checkClient(): void + { + if (Core::getenv('HTTP_USER_AGENT')) { + $HTTP_USER_AGENT = Core::getenv('HTTP_USER_AGENT'); + } else { + $HTTP_USER_AGENT = ''; + } + + // 1. Platform + $this->_setClientPlatform($HTTP_USER_AGENT); + + // 2. browser and version + // (must check everything else before Mozilla) + + $is_mozilla = preg_match( + '@Mozilla/([0-9]\.[0-9]{1,2})@', + $HTTP_USER_AGENT, + $mozilla_version + ); + + if (preg_match( + '@Opera(/| )([0-9]\.[0-9]{1,2})@', + $HTTP_USER_AGENT, + $log_version + )) { + $this->set('PMA_USR_BROWSER_VER', $log_version[2]); + $this->set('PMA_USR_BROWSER_AGENT', 'OPERA'); + } elseif (preg_match( + '@(MS)?IE ([0-9]{1,2}\.[0-9]{1,2})@', + $HTTP_USER_AGENT, + $log_version + )) { + $this->set('PMA_USR_BROWSER_VER', $log_version[2]); + $this->set('PMA_USR_BROWSER_AGENT', 'IE'); + } elseif (preg_match( + '@Trident/(7)\.0@', + $HTTP_USER_AGENT, + $log_version + )) { + $this->set('PMA_USR_BROWSER_VER', intval($log_version[1]) + 4); + $this->set('PMA_USR_BROWSER_AGENT', 'IE'); + } elseif (preg_match( + '@OmniWeb/([0-9]{1,3})@', + $HTTP_USER_AGENT, + $log_version + )) { + $this->set('PMA_USR_BROWSER_VER', $log_version[1]); + $this->set('PMA_USR_BROWSER_AGENT', 'OMNIWEB'); + // Konqueror 2.2.2 says Konqueror/2.2.2 + // Konqueror 3.0.3 says Konqueror/3 + } elseif (preg_match( + '@(Konqueror/)(.*)(;)@', + $HTTP_USER_AGENT, + $log_version + )) { + $this->set('PMA_USR_BROWSER_VER', $log_version[2]); + $this->set('PMA_USR_BROWSER_AGENT', 'KONQUEROR'); + // must check Chrome before Safari + } elseif ($is_mozilla + && preg_match('@Chrome/([0-9.]*)@', $HTTP_USER_AGENT, $log_version) + ) { + $this->set('PMA_USR_BROWSER_VER', $log_version[1]); + $this->set('PMA_USR_BROWSER_AGENT', 'CHROME'); + // newer Safari + } elseif ($is_mozilla + && preg_match('@Version/(.*) Safari@', $HTTP_USER_AGENT, $log_version) + ) { + $this->set( + 'PMA_USR_BROWSER_VER', + $log_version[1] + ); + $this->set('PMA_USR_BROWSER_AGENT', 'SAFARI'); + // older Safari + } elseif ($is_mozilla + && preg_match('@Safari/([0-9]*)@', $HTTP_USER_AGENT, $log_version) + ) { + $this->set( + 'PMA_USR_BROWSER_VER', + $mozilla_version[1] . '.' . $log_version[1] + ); + $this->set('PMA_USR_BROWSER_AGENT', 'SAFARI'); + // Firefox + } elseif (! mb_strstr($HTTP_USER_AGENT, 'compatible') + && preg_match('@Firefox/([\w.]+)@', $HTTP_USER_AGENT, $log_version) + ) { + $this->set( + 'PMA_USR_BROWSER_VER', + $log_version[1] + ); + $this->set('PMA_USR_BROWSER_AGENT', 'FIREFOX'); + } elseif (preg_match('@rv:1\.9(.*)Gecko@', $HTTP_USER_AGENT)) { + $this->set('PMA_USR_BROWSER_VER', '1.9'); + $this->set('PMA_USR_BROWSER_AGENT', 'GECKO'); + } elseif ($is_mozilla) { + $this->set('PMA_USR_BROWSER_VER', $mozilla_version[1]); + $this->set('PMA_USR_BROWSER_AGENT', 'MOZILLA'); + } else { + $this->set('PMA_USR_BROWSER_VER', 0); + $this->set('PMA_USR_BROWSER_AGENT', 'OTHER'); + } + } + + /** + * Whether GD2 is present + * + * @return void + */ + public function checkGd2(): void + { + if ($this->get('GD2Available') == 'yes') { + $this->set('PMA_IS_GD2', 1); + return; + } + + if ($this->get('GD2Available') == 'no') { + $this->set('PMA_IS_GD2', 0); + return; + } + + if (! function_exists('imagecreatetruecolor')) { + $this->set('PMA_IS_GD2', 0); + return; + } + + if (function_exists('gd_info')) { + $gd_nfo = gd_info(); + if (mb_strstr($gd_nfo["GD Version"], '2.')) { + $this->set('PMA_IS_GD2', 1); + } else { + $this->set('PMA_IS_GD2', 0); + } + } else { + $this->set('PMA_IS_GD2', 0); + } + } + + /** + * Whether the Web server php is running on is IIS + * + * @return void + */ + public function checkWebServer(): void + { + // some versions return Microsoft-IIS, some Microsoft/IIS + // we could use a preg_match() but it's slower + if (Core::getenv('SERVER_SOFTWARE') + && false !== stripos(Core::getenv('SERVER_SOFTWARE'), 'Microsoft') + && false !== stripos(Core::getenv('SERVER_SOFTWARE'), 'IIS') + ) { + $this->set('PMA_IS_IIS', 1); + } else { + $this->set('PMA_IS_IIS', 0); + } + } + + /** + * Whether the os php is running on is windows or not + * + * @return void + */ + public function checkWebServerOs(): void + { + // Default to Unix or Equiv + $this->set('PMA_IS_WINDOWS', 0); + // If PHP_OS is defined then continue + if (defined('PHP_OS')) { + if (false !== stripos(PHP_OS, 'win') && false === stripos(PHP_OS, 'darwin')) { + // Is it some version of Windows + $this->set('PMA_IS_WINDOWS', 1); + } elseif (false !== stripos(PHP_OS, 'OS/2')) { + // Is it OS/2 (No file permissions like Windows) + $this->set('PMA_IS_WINDOWS', 1); + } + } + } + + /** + * detects if Git revision + * @param string $git_location (optional) verified git directory + * @return boolean + */ + public function isGitRevision(&$git_location = null): bool + { + // PMA config check + if (! $this->get('ShowGitRevision')) { + return false; + } + + // caching + if (isset($_SESSION['is_git_revision']) + && array_key_exists('git_location', $_SESSION) + ) { + // Define location using cached value + $git_location = $_SESSION['git_location']; + return $_SESSION['is_git_revision']; + } + + // find out if there is a .git folder + // or a .git file (--separate-git-dir) + $git = '.git'; + if (is_dir($git)) { + if (@is_file($git . '/config')) { + $git_location = $git; + } else { + $_SESSION['git_location'] = null; + $_SESSION['is_git_revision'] = false; + return false; + } + } elseif (is_file($git)) { + $contents = file_get_contents($git); + $gitmatch = []; + // Matches expected format + if (! preg_match( + '/^gitdir: (.*)$/', + $contents, + $gitmatch + )) { + $_SESSION['git_location'] = null; + $_SESSION['is_git_revision'] = false; + return false; + } elseif (@is_dir($gitmatch[1])) { + //Detected git external folder location + $git_location = $gitmatch[1]; + } else { + $_SESSION['git_location'] = null; + $_SESSION['is_git_revision'] = false; + return false; + } + } else { + $_SESSION['git_location'] = null; + $_SESSION['is_git_revision'] = false; + return false; + } + // Define session for caching + $_SESSION['git_location'] = $git_location; + $_SESSION['is_git_revision'] = true; + return true; + } + + /** + * detects Git revision, if running inside repo + * + * @return void + */ + public function checkGitRevision(): void + { + // find out if there is a .git folder + $git_folder = ''; + if (! $this->isGitRevision($git_folder)) { + $this->set('PMA_VERSION_GIT', 0); + return; + } + + if (! $ref_head = @file_get_contents($git_folder . '/HEAD')) { + $this->set('PMA_VERSION_GIT', 0); + return; + } + + if ($common_dir_contents = @file_get_contents($git_folder . '/commondir')) { + $git_folder = $git_folder . DIRECTORY_SEPARATOR . trim($common_dir_contents); + } + + $branch = false; + // are we on any branch? + if (false !== strpos($ref_head, '/')) { + // remove ref: prefix + $ref_head = substr(trim($ref_head), 5); + if (substr($ref_head, 0, 11) === 'refs/heads/') { + $branch = substr($ref_head, 11); + } else { + $branch = basename($ref_head); + } + + $ref_file = $git_folder . '/' . $ref_head; + if (@file_exists($ref_file)) { + $hash = @file_get_contents($ref_file); + if (! $hash) { + $this->set('PMA_VERSION_GIT', 0); + return; + } + $hash = trim($hash); + } else { + // deal with packed refs + $packed_refs = @file_get_contents($git_folder . '/packed-refs'); + if (! $packed_refs) { + $this->set('PMA_VERSION_GIT', 0); + return; + } + // split file to lines + $ref_lines = explode(PHP_EOL, $packed_refs); + foreach ($ref_lines as $line) { + // skip comments + if ($line[0] == '#') { + continue; + } + // parse line + $parts = explode(' ', $line); + // care only about named refs + if (count($parts) != 2) { + continue; + } + // have found our ref? + if ($parts[1] == $ref_head) { + $hash = $parts[0]; + break; + } + } + if (! isset($hash)) { + $this->set('PMA_VERSION_GIT', 0); + // Could not find ref + return; + } + } + } else { + $hash = trim($ref_head); + } + + $commit = false; + if (! preg_match('/^[0-9a-f]{40}$/i', $hash)) { + $commit = false; + } elseif (isset($_SESSION['PMA_VERSION_COMMITDATA_' . $hash])) { + $commit = $_SESSION['PMA_VERSION_COMMITDATA_' . $hash]; + } elseif (function_exists('gzuncompress')) { + $git_file_name = $git_folder . '/objects/' + . substr($hash, 0, 2) . '/' . substr($hash, 2); + if (@file_exists($git_file_name)) { + if (! $commit = @file_get_contents($git_file_name)) { + $this->set('PMA_VERSION_GIT', 0); + return; + } + $commit = explode("\0", gzuncompress($commit), 2); + $commit = explode("\n", $commit[1]); + $_SESSION['PMA_VERSION_COMMITDATA_' . $hash] = $commit; + } else { + $pack_names = []; + // work with packed data + $packs_file = $git_folder . '/objects/info/packs'; + if (@file_exists($packs_file) + && $packs = @file_get_contents($packs_file) + ) { + // File exists. Read it, parse the file to get the names of the + // packs. (to look for them in .git/object/pack directory later) + foreach (explode("\n", $packs) as $line) { + // skip blank lines + if (strlen(trim($line)) == 0) { + continue; + } + // skip non pack lines + if ($line[0] != 'P') { + continue; + } + // parse names + $pack_names[] = substr($line, 2); + } + } else { + // '.git/objects/info/packs' file can be missing + // (atlease in mysGit) + // File missing. May be we can look in the .git/object/pack + // directory for all the .pack files and use that list of + // files instead + $dirIterator = new DirectoryIterator( + $git_folder . '/objects/pack' + ); + foreach ($dirIterator as $file_info) { + $file_name = $file_info->getFilename(); + // if this is a .pack file + if ($file_info->isFile() && substr($file_name, -5) == '.pack' + ) { + $pack_names[] = $file_name; + } + } + } + $hash = strtolower($hash); + foreach ($pack_names as $pack_name) { + $index_name = str_replace('.pack', '.idx', $pack_name); + + // load index + $index_data = @file_get_contents( + $git_folder . '/objects/pack/' . $index_name + ); + if (! $index_data) { + continue; + } + // check format + if (substr($index_data, 0, 4) != "\377tOc") { + continue; + } + // check version + $version = unpack('N', substr($index_data, 4, 4)); + if ($version[1] != 2) { + continue; + } + // parse fanout table + $fanout = unpack( + "N*", + substr($index_data, 8, 256 * 4) + ); + + // find where we should search + $firstbyte = intval(substr($hash, 0, 2), 16); + // array is indexed from 1 and we need to get + // previous entry for start + if ($firstbyte == 0) { + $start = 0; + } else { + $start = $fanout[$firstbyte]; + } + $end = $fanout[$firstbyte + 1]; + + // stupid linear search for our sha + $found = false; + $offset = 8 + (256 * 4); + for ($position = $start; $position < $end; $position++) { + $sha = strtolower( + bin2hex( + substr($index_data, $offset + ($position * 20), 20) + ) + ); + if ($sha == $hash) { + $found = true; + break; + } + } + if (! $found) { + continue; + } + // read pack offset + $offset = 8 + (256 * 4) + (24 * $fanout[256]); + $pack_offset = unpack( + 'N', + substr($index_data, $offset + ($position * 4), 4) + ); + $pack_offset = $pack_offset[1]; + + // open pack file + $pack_file = fopen( + $git_folder . '/objects/pack/' . $pack_name, + 'rb' + ); + if ($pack_file === false) { + continue; + } + // seek to start + fseek($pack_file, $pack_offset); + + // parse header + $header = ord(fread($pack_file, 1)); + $type = ($header >> 4) & 7; + $hasnext = ($header & 128) >> 7; + $size = $header & 0xf; + $offset = 4; + + while ($hasnext) { + $byte = ord(fread($pack_file, 1)); + $size |= ($byte & 0x7f) << $offset; + $hasnext = ($byte & 128) >> 7; + $offset += 7; + } + + // we care only about commit objects + if ($type != 1) { + continue; + } + + // read data + $commit = fread($pack_file, $size); + $commit = gzuncompress($commit); + $commit = explode("\n", $commit); + $_SESSION['PMA_VERSION_COMMITDATA_' . $hash] = $commit; + fclose($pack_file); + } + } + } + + $httpRequest = new HttpRequest(); + + // check if commit exists in Github + if ($commit !== false + && isset($_SESSION['PMA_VERSION_REMOTECOMMIT_' . $hash]) + ) { + $is_remote_commit = $_SESSION['PMA_VERSION_REMOTECOMMIT_' . $hash]; + } else { + $link = 'https://www.phpmyadmin.net/api/commit/' . $hash . '/'; + $is_found = $httpRequest->create($link, 'GET'); + switch ($is_found) { + case false: + $is_remote_commit = false; + $_SESSION['PMA_VERSION_REMOTECOMMIT_' . $hash] = false; + break; + case null: + // no remote link for now, but don't cache this as Github is down + $is_remote_commit = false; + break; + default: + $is_remote_commit = true; + $_SESSION['PMA_VERSION_REMOTECOMMIT_' . $hash] = true; + if ($commit === false) { + // if no local commit data, try loading from Github + $commit_json = json_decode($is_found); + } + break; + } + } + + $is_remote_branch = false; + if ($is_remote_commit && $branch !== false) { + // check if branch exists in Github + if (isset($_SESSION['PMA_VERSION_REMOTEBRANCH_' . $hash])) { + $is_remote_branch = $_SESSION['PMA_VERSION_REMOTEBRANCH_' . $hash]; + } else { + $link = 'https://www.phpmyadmin.net/api/tree/' . $branch . '/'; + $is_found = $httpRequest->create($link, 'GET', true); + switch ($is_found) { + case true: + $is_remote_branch = true; + $_SESSION['PMA_VERSION_REMOTEBRANCH_' . $hash] = true; + break; + case false: + $is_remote_branch = false; + $_SESSION['PMA_VERSION_REMOTEBRANCH_' . $hash] = false; + break; + case null: + // no remote link for now, but don't cache this as Github is down + $is_remote_branch = false; + break; + } + } + } + + if ($commit !== false) { + $author = [ + 'name' => '', + 'email' => '', + 'date' => '', + ]; + $committer = [ + 'name' => '', + 'email' => '', + 'date' => '', + ]; + + do { + $dataline = array_shift($commit); + $datalinearr = explode(' ', $dataline, 2); + $linetype = $datalinearr[0]; + if (in_array($linetype, ['author', 'committer'])) { + $user = $datalinearr[1]; + preg_match('/([^<]+)<([^>]+)> ([0-9]+)( [^ ]+)?/', $user, $user); + $user2 = [ + 'name' => trim($user[1]), + 'email' => trim($user[2]), + 'date' => date('Y-m-d H:i:s', (int) $user[3]), + ]; + if (isset($user[4])) { + $user2['date'] .= $user[4]; + } + $$linetype = $user2; + } + } while ($dataline != ''); + $message = trim(implode(' ', $commit)); + } elseif (isset($commit_json) && isset($commit_json->author) && isset($commit_json->committer) && isset($commit_json->message)) { + $author = [ + 'name' => $commit_json->author->name, + 'email' => $commit_json->author->email, + 'date' => $commit_json->author->date, + ]; + $committer = [ + 'name' => $commit_json->committer->name, + 'email' => $commit_json->committer->email, + 'date' => $commit_json->committer->date, + ]; + $message = trim($commit_json->message); + } else { + $this->set('PMA_VERSION_GIT', 0); + return; + } + + $this->set('PMA_VERSION_GIT', 1); + $this->set('PMA_VERSION_GIT_COMMITHASH', $hash); + $this->set('PMA_VERSION_GIT_BRANCH', $branch); + $this->set('PMA_VERSION_GIT_MESSAGE', $message); + $this->set('PMA_VERSION_GIT_AUTHOR', $author); + $this->set('PMA_VERSION_GIT_COMMITTER', $committer); + $this->set('PMA_VERSION_GIT_ISREMOTECOMMIT', $is_remote_commit); + $this->set('PMA_VERSION_GIT_ISREMOTEBRANCH', $is_remote_branch); + } + + /** + * loads default values from default source + * + * @return boolean success + */ + public function loadDefaults(): bool + { + $cfg = []; + if (! @file_exists($this->default_source)) { + $this->error_config_default_file = true; + return false; + } + $canUseErrorReporting = function_exists('error_reporting'); + $oldErrorReporting = null; + if ($canUseErrorReporting) { + $oldErrorReporting = error_reporting(0); + } + ob_start(); + $GLOBALS['pma_config_loading'] = true; + $eval_result = include $this->default_source; + $GLOBALS['pma_config_loading'] = false; + ob_end_clean(); + if ($canUseErrorReporting) { + error_reporting($oldErrorReporting); + } + + if ($eval_result === false) { + $this->error_config_default_file = true; + return false; + } + + $this->default_source_mtime = filemtime($this->default_source); + + $this->default_server = $cfg['Servers'][1]; + unset($cfg['Servers']); + + $this->default = $cfg; + $this->settings = array_replace_recursive($this->settings, $cfg); + + $this->error_config_default_file = false; + + return true; + } + + /** + * loads configuration from $source, usually the config file + * should be called on object creation + * + * @param string $source config file + * + * @return bool + */ + public function load(?string $source = null): bool + { + $this->loadDefaults(); + + if (null !== $source) { + $this->setSource($source); + } + + if (! $this->checkConfigSource()) { + return false; + } + + $cfg = []; + + /** + * Parses the configuration file, we throw away any errors or + * output. + */ + $canUseErrorReporting = function_exists('error_reporting'); + $oldErrorReporting = null; + if ($canUseErrorReporting) { + $oldErrorReporting = error_reporting(0); + } + ob_start(); + $GLOBALS['pma_config_loading'] = true; + $eval_result = include $this->getSource(); + $GLOBALS['pma_config_loading'] = false; + ob_end_clean(); + if ($canUseErrorReporting) { + error_reporting($oldErrorReporting); + } + + if ($eval_result === false) { + $this->error_config_file = true; + } else { + $this->error_config_file = false; + $this->source_mtime = filemtime($this->getSource()); + } + + /** + * Ignore keys with / as we do not use these + * + * These can be confusing for user configuration layer as it + * flatten array using / and thus don't see difference between + * $cfg['Export/method'] and $cfg['Export']['method'], while rest + * of thre code uses the setting only in latter form. + * + * This could be removed once we consistently handle both values + * in the functional code as well. + * + * It could use array_filter(...ARRAY_FILTER_USE_KEY), but it's not + * supported on PHP 5.5 and HHVM. + */ + $matched_keys = array_filter( + array_keys($cfg), + function ($key) { + return strpos($key, '/') === false; + } + ); + + $cfg = array_intersect_key($cfg, array_flip($matched_keys)); + + /** + * Backward compatibility code + */ + if (! empty($cfg['DefaultTabTable'])) { + $cfg['DefaultTabTable'] = str_replace( + [ + 'tbl_properties.php', + '_properties', + ], + [ + 'tbl_sql.php', + '', + ], + $cfg['DefaultTabTable'] + ); + } + if (! empty($cfg['DefaultTabDatabase'])) { + $cfg['DefaultTabDatabase'] = str_replace( + [ + 'db_details.php', + '_details', + ], + [ + 'db_sql.php', + '', + ], + $cfg['DefaultTabDatabase'] + ); + } + + $this->settings = array_replace_recursive($this->settings, $cfg); + + return true; + } + + /** + * Sets the connection collation + * + * @return void + */ + private function _setConnectionCollation(): void + { + $collation_connection = $this->get('DefaultConnectionCollation'); + if (! empty($collation_connection) + && $collation_connection != $GLOBALS['collation_connection'] + ) { + $GLOBALS['dbi']->setCollation($collation_connection); + } + } + + /** + * Loads user preferences and merges them with current config + * must be called after control connection has been established + * + * @return void + */ + public function loadUserPreferences(): void + { + $userPreferences = new UserPreferences(); + // index.php should load these settings, so that phpmyadmin.css.php + // will have everything available in session cache + $server = isset($GLOBALS['server']) + ? $GLOBALS['server'] + : (! empty($GLOBALS['cfg']['ServerDefault']) + ? $GLOBALS['cfg']['ServerDefault'] + : 0); + $cache_key = 'server_' . $server; + if ($server > 0 && ! defined('PMA_MINIMUM_COMMON')) { + $config_mtime = max($this->default_source_mtime, $this->source_mtime); + // cache user preferences, use database only when needed + if (! isset($_SESSION['cache'][$cache_key]['userprefs']) + || $_SESSION['cache'][$cache_key]['config_mtime'] < $config_mtime + ) { + $prefs = $userPreferences->load(); + $_SESSION['cache'][$cache_key]['userprefs'] + = $userPreferences->apply($prefs['config_data']); + $_SESSION['cache'][$cache_key]['userprefs_mtime'] = $prefs['mtime']; + $_SESSION['cache'][$cache_key]['userprefs_type'] = $prefs['type']; + $_SESSION['cache'][$cache_key]['config_mtime'] = $config_mtime; + } + } elseif ($server == 0 + || ! isset($_SESSION['cache'][$cache_key]['userprefs']) + ) { + $this->set('user_preferences', false); + return; + } + $config_data = $_SESSION['cache'][$cache_key]['userprefs']; + // type is 'db' or 'session' + $this->set( + 'user_preferences', + $_SESSION['cache'][$cache_key]['userprefs_type'] + ); + $this->set( + 'user_preferences_mtime', + $_SESSION['cache'][$cache_key]['userprefs_mtime'] + ); + + // load config array + $this->settings = array_replace_recursive($this->settings, $config_data); + $GLOBALS['cfg'] = array_replace_recursive($GLOBALS['cfg'], $config_data); + if (defined('PMA_MINIMUM_COMMON')) { + return; + } + + // settings below start really working on next page load, but + // changes are made only in index.php so everything is set when + // in frames + + // save theme + /** @var ThemeManager $tmanager */ + $tmanager = ThemeManager::getInstance(); + if ($tmanager->getThemeCookie() || isset($_REQUEST['set_theme'])) { + if ((! isset($config_data['ThemeDefault']) + && $tmanager->theme->getId() != 'original') + || isset($config_data['ThemeDefault']) + && $config_data['ThemeDefault'] != $tmanager->theme->getId() + ) { + // new theme was set in common.inc.php + $this->setUserValue( + null, + 'ThemeDefault', + $tmanager->theme->getId(), + 'original' + ); + } + } else { + // no cookie - read default from settings + if ($this->settings['ThemeDefault'] != $tmanager->theme->getId() + && $tmanager->checkTheme($this->settings['ThemeDefault']) + ) { + $tmanager->setActiveTheme($this->settings['ThemeDefault']); + $tmanager->setThemeCookie(); + } + } + + // save language + if ($this->issetCookie('pma_lang') || isset($_POST['lang'])) { + if ((! isset($config_data['lang']) + && $GLOBALS['lang'] != 'en') + || isset($config_data['lang']) + && $GLOBALS['lang'] != $config_data['lang'] + ) { + $this->setUserValue(null, 'lang', $GLOBALS['lang'], 'en'); + } + } else { + // read language from settings + if (isset($config_data['lang'])) { + $language = LanguageManager::getInstance()->getLanguage( + $config_data['lang'] + ); + if ($language !== false) { + $language->activate(); + $this->setCookie('pma_lang', $language->getCode()); + } + } + } + + // set connection collation + $this->_setConnectionCollation(); + } + + /** + * Sets config value which is stored in user preferences (if available) + * or in a cookie. + * + * If user preferences are not yet initialized, option is applied to + * global config and added to a update queue, which is processed + * by {@link loadUserPreferences()} + * + * @param string|null $cookie_name can be null + * @param string $cfg_path configuration path + * @param mixed $new_cfg_value new value + * @param mixed $default_value default value + * + * @return true|Message + */ + public function setUserValue( + ?string $cookie_name, + string $cfg_path, + $new_cfg_value, + $default_value = null + ) { + $userPreferences = new UserPreferences(); + $result = true; + // use permanent user preferences if possible + $prefs_type = $this->get('user_preferences'); + if ($prefs_type) { + if ($default_value === null) { + $default_value = Core::arrayRead($cfg_path, $this->default); + } + $result = $userPreferences->persistOption($cfg_path, $new_cfg_value, $default_value); + } + if ($prefs_type != 'db' && $cookie_name) { + // fall back to cookies + if ($default_value === null) { + $default_value = Core::arrayRead($cfg_path, $this->settings); + } + $this->setCookie($cookie_name, $new_cfg_value, $default_value); + } + Core::arrayWrite($cfg_path, $GLOBALS['cfg'], $new_cfg_value); + Core::arrayWrite($cfg_path, $this->settings, $new_cfg_value); + return $result; + } + + /** + * Reads value stored by {@link setUserValue()} + * + * @param string $cookie_name cookie name + * @param mixed $cfg_value config value + * + * @return mixed + */ + public function getUserValue(string $cookie_name, $cfg_value) + { + $cookie_exists = isset($_COOKIE) && ! empty($this->getCookie($cookie_name)); + $prefs_type = $this->get('user_preferences'); + if ($prefs_type == 'db') { + // permanent user preferences value exists, remove cookie + if ($cookie_exists) { + $this->removeCookie($cookie_name); + } + } elseif ($cookie_exists) { + return $this->getCookie($cookie_name); + } + // return value from $cfg array + return $cfg_value; + } + + /** + * set source + * + * @param string $source source + * + * @return void + */ + public function setSource(string $source): void + { + $this->source = trim($source); + } + + /** + * check config source + * + * @return boolean whether source is valid or not + */ + public function checkConfigSource(): bool + { + if (! $this->getSource()) { + // no configuration file set at all + return false; + } + + if (! @file_exists($this->getSource())) { + $this->source_mtime = 0; + return false; + } + + if (! @is_readable($this->getSource())) { + // manually check if file is readable + // might be bug #3059806 Supporting running from CIFS/Samba shares + + $contents = false; + $handle = @fopen($this->getSource(), 'r'); + if ($handle !== false) { + $contents = @fread($handle, 1); // reading 1 byte is enough to test + fclose($handle); + } + if ($contents === false) { + $this->source_mtime = 0; + Core::fatalError( + sprintf( + function_exists('__') + ? __('Existing configuration file (%s) is not readable.') + : 'Existing configuration file (%s) is not readable.', + $this->getSource() + ) + ); + return false; + } + } + + return true; + } + + /** + * verifies the permissions on config file (if asked by configuration) + * (must be called after config.inc.php has been merged) + * + * @return void + */ + public function checkPermissions(): void + { + // Check for permissions (on platforms that support it): + if ($this->get('CheckConfigurationPermissions') && @file_exists($this->getSource())) { + $perms = @fileperms($this->getSource()); + if (! ($perms === false) && ($perms & 2)) { + // This check is normally done after loading configuration + $this->checkWebServerOs(); + if ($this->get('PMA_IS_WINDOWS') == 0) { + $this->source_mtime = 0; + Core::fatalError( + __( + 'Wrong permissions on configuration file, ' + . 'should not be world writable!' + ) + ); + } + } + } + } + + /** + * Checks for errors + * (must be called after config.inc.php has been merged) + * + * @return void + */ + public function checkErrors(): void + { + if ($this->error_config_default_file) { + Core::fatalError( + sprintf( + __('Could not load default configuration from: %1$s'), + $this->default_source + ) + ); + } + + if ($this->error_config_file) { + $error = '[strong]' . __('Failed to read configuration file!') . '[/strong]' + . '[br][br]' + . __( + 'This usually means there is a syntax error in it, ' + . 'please check any errors shown below.' + ) + . '[br][br]' + . '[conferr]'; + trigger_error($error, E_USER_ERROR); + } + } + + /** + * returns specific config setting + * + * @param string $setting config setting + * + * @return mixed value + */ + public function get(string $setting) + { + if (isset($this->settings[$setting])) { + return $this->settings[$setting]; + } + return null; + } + + /** + * sets configuration variable + * + * @param string $setting configuration option + * @param mixed $value new value for configuration option + * + * @return void + */ + public function set(string $setting, $value): void + { + if (! isset($this->settings[$setting]) + || $this->settings[$setting] !== $value + ) { + $this->settings[$setting] = $value; + $this->set_mtime = time(); + } + } + + /** + * returns source for current config + * + * @return string config source + */ + public function getSource(): string + { + return $this->source; + } + + /** + * returns a unique value to force a CSS reload if either the config + * or the theme changes + * + * @return int Summary of unix timestamps, to be unique on theme parameters + * change + */ + public function getThemeUniqueValue(): int + { + return (int) ( + $this->source_mtime + + $this->default_source_mtime + + $this->get('user_preferences_mtime') + + $GLOBALS['PMA_Theme']->mtime_info + + $GLOBALS['PMA_Theme']->filesize_info + ); + } + + /** + * checks if upload is enabled + * + * @return void + */ + public function checkUpload(): void + { + if (! ini_get('file_uploads')) { + $this->set('enable_upload', false); + return; + } + + $this->set('enable_upload', true); + // if set "php_admin_value file_uploads Off" in httpd.conf + // ini_get() also returns the string "Off" in this case: + if ('off' == strtolower(ini_get('file_uploads'))) { + $this->set('enable_upload', false); + } + } + + /** + * Maximum upload size as limited by PHP + * Used with permission from Moodle (https://moodle.org/) by Martin Dougiamas + * + * this section generates $max_upload_size in bytes + * + * @return void + */ + public function checkUploadSize(): void + { + if (! $filesize = ini_get('upload_max_filesize')) { + $filesize = "5M"; + } + + if ($postsize = ini_get('post_max_size')) { + $this->set( + 'max_upload_size', + min(Core::getRealSize($filesize), Core::getRealSize($postsize)) + ); + } else { + $this->set('max_upload_size', Core::getRealSize($filesize)); + } + } + + /** + * Checks if protocol is https + * + * This function checks if the https protocol on the active connection. + * + * @return bool + */ + public function isHttps(): bool + { + if (null !== $this->get('is_https')) { + return $this->get('is_https'); + } + + $url = $this->get('PmaAbsoluteUri'); + + $is_https = false; + if (! empty($url) && parse_url($url, PHP_URL_SCHEME) === 'https') { + $is_https = true; + } elseif (strtolower(Core::getenv('HTTP_SCHEME')) == 'https') { + $is_https = true; + } elseif (strtolower(Core::getenv('HTTPS')) == 'on') { + $is_https = true; + } elseif (substr(strtolower(Core::getenv('REQUEST_URI')), 0, 6) == 'https:') { + $is_https = true; + } elseif (strtolower(Core::getenv('HTTP_HTTPS_FROM_LB')) == 'on') { + // A10 Networks load balancer + $is_https = true; + } elseif (strtolower(Core::getenv('HTTP_FRONT_END_HTTPS')) == 'on') { + $is_https = true; + } elseif (strtolower(Core::getenv('HTTP_X_FORWARDED_PROTO')) == 'https') { + $is_https = true; + } elseif (strtolower(Core::getenv('HTTP_CLOUDFRONT_FORWARDED_PROTO')) === 'https') { + // Amazon CloudFront, issue #15621 + $is_https = true; + } elseif (Core::getenv('SERVER_PORT') == 443) { + $is_https = true; + } + + $this->set('is_https', $is_https); + + return $is_https; + } + + /** + * Get phpMyAdmin root path + * + * @return string + */ + public function getRootPath(): string + { + static $cookie_path = null; + + if (null !== $cookie_path && ! defined('TESTSUITE')) { + return $cookie_path; + } + + $url = $this->get('PmaAbsoluteUri'); + + if (! empty($url)) { + $path = parse_url($url, PHP_URL_PATH); + if (! empty($path)) { + if (substr($path, -1) != '/') { + return $path . '/'; + } + return $path; + } + } + + $parsed_url = parse_url($GLOBALS['PMA_PHP_SELF']); + + $parts = explode( + '/', + rtrim(str_replace('\\', '/', $parsed_url['path']), '/') + ); + + /* Remove filename */ + if (substr($parts[count($parts) - 1], -4) == '.php') { + $parts = array_slice($parts, 0, count($parts) - 1); + } + + /* Remove extra path from javascript calls */ + if (defined('PMA_PATH_TO_BASEDIR')) { + $parts = array_slice($parts, 0, count($parts) - 1); + } + + $parts[] = ''; + + return implode('/', $parts); + } + + /** + * enables backward compatibility + * + * @return void + */ + public function enableBc(): void + { + $GLOBALS['cfg'] = $this->settings; + $GLOBALS['default_server'] = $this->default_server; + unset($this->default_server); + $GLOBALS['is_upload'] = $this->get('enable_upload'); + $GLOBALS['max_upload_size'] = $this->get('max_upload_size'); + $GLOBALS['is_https'] = $this->get('is_https'); + + $defines = [ + 'PMA_VERSION', + 'PMA_MAJOR_VERSION', + 'PMA_THEME_VERSION', + 'PMA_THEME_GENERATION', + 'PMA_IS_WINDOWS', + 'PMA_IS_GD2', + 'PMA_USR_OS', + 'PMA_USR_BROWSER_VER', + 'PMA_USR_BROWSER_AGENT', + ]; + + foreach ($defines as $define) { + if (! defined($define)) { + define($define, $this->get($define)); + } + } + } + + /** + * removes cookie + * + * @param string $cookieName name of cookie to remove + * + * @return boolean result of setcookie() + */ + public function removeCookie(string $cookieName): bool + { + $httpCookieName = $this->getCookieName($cookieName); + + if ($this->issetCookie($cookieName)) { + unset($_COOKIE[$httpCookieName]); + } + if (defined('TESTSUITE')) { + return true; + } + return setcookie( + $httpCookieName, + '', + time() - 3600, + $this->getRootPath(), + '', + $this->isHttps() + ); + } + + /** + * sets cookie if value is different from current cookie value, + * or removes if value is equal to default + * + * @param string $cookie name of cookie to remove + * @param mixed $value new cookie value + * @param string $default default value + * @param int $validity validity of cookie in seconds (default is one month) + * @param bool $httponly whether cookie is only for HTTP (and not for scripts) + * + * @return boolean result of setcookie() + */ + public function setCookie( + string $cookie, + $value, + ?string $default = null, + ?int $validity = null, + bool $httponly = true + ): bool { + if (strlen($value) > 0 && null !== $default && $value === $default + ) { + // default value is used + if ($this->issetCookie($cookie)) { + // remove cookie + return $this->removeCookie($cookie); + } + return false; + } + + if (strlen($value) === 0 && $this->issetCookie($cookie)) { + // remove cookie, value is empty + return $this->removeCookie($cookie); + } + + $httpCookieName = $this->getCookieName($cookie); + + if (! $this->issetCookie($cookie) || $this->getCookie($cookie) !== $value) { + // set cookie with new value + /* Calculate cookie validity */ + if ($validity === null) { + /* Valid for one month */ + $validity = time() + 2592000; + } elseif ($validity == 0) { + /* Valid for session */ + $validity = 0; + } else { + $validity = time() + $validity; + } + if (defined('TESTSUITE')) { + $_COOKIE[$httpCookieName] = $value; + return true; + } + return setcookie( + $httpCookieName, + $value, + $validity, + $this->getRootPath(), + '', + $this->isHttps(), + $httponly + ); + } + + // cookie has already $value as value + return true; + } + + /** + * get cookie + * + * @param string $cookieName The name of the cookie to get + * + * @return mixed result of getCookie() + */ + public function getCookie(string $cookieName) + { + if (isset($_COOKIE[$this->getCookieName($cookieName)])) { + return $_COOKIE[$this->getCookieName($cookieName)]; + } else { + return null; + } + } + + /** + * Get the real cookie name + * + * @param string $cookieName The name of the cookie + * @return string + */ + public function getCookieName(string $cookieName): string + { + return $cookieName . ( ($this->isHttps()) ? '_https' : '' ); + } + + /** + * isset cookie + * + * @param string $cookieName The name of the cookie to check + * + * @return bool result of issetCookie() + */ + public function issetCookie(string $cookieName): bool + { + return isset($_COOKIE[$this->getCookieName($cookieName)]); + } + + /** + * Error handler to catch fatal errors when loading configuration + * file + * + * @return void + */ + public static function fatalErrorHandler(): void + { + if (! isset($GLOBALS['pma_config_loading']) + || ! $GLOBALS['pma_config_loading'] + ) { + return; + } + + $error = error_get_last(); + if ($error === null) { + return; + } + + Core::fatalError( + sprintf( + 'Failed to load phpMyAdmin configuration (%s:%s): %s', + Error::relPath($error['file']), + $error['line'], + $error['message'] + ) + ); + } + + /** + * Wrapper for footer/header rendering + * + * @param string $filename File to check and render + * @param string $id Div ID + * + * @return string + */ + private static function _renderCustom(string $filename, string $id): string + { + $retval = ''; + if (@file_exists($filename)) { + $retval .= '
'; + ob_start(); + include $filename; + $retval .= ob_get_contents(); + ob_end_clean(); + $retval .= '
'; + } + return $retval; + } + + /** + * Renders user configured footer + * + * @return string + */ + public static function renderFooter(): string + { + return self::_renderCustom(CUSTOM_FOOTER_FILE, 'pma_footer'); + } + + /** + * Renders user configured footer + * + * @return string + */ + public static function renderHeader(): string + { + return self::_renderCustom(CUSTOM_HEADER_FILE, 'pma_header'); + } + + /** + * Returns temporary dir path + * + * @param string $name Directory name + * + * @return string|null + */ + public function getTempDir(string $name): ?string + { + static $temp_dir = []; + + if (isset($temp_dir[$name]) && ! defined('TESTSUITE')) { + return $temp_dir[$name]; + } + + $path = $this->get('TempDir'); + if (empty($path)) { + $path = null; + } else { + $path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $name; + if (! @is_dir($path)) { + @mkdir($path, 0770, true); + } + if (! @is_dir($path) || ! @is_writable($path)) { + $path = null; + } + } + + $temp_dir[$name] = $path; + return $path; + } + + /** + * Returns temporary directory + * + * @return string|null + */ + public function getUploadTempDir(): ?string + { + // First try configured temp dir + // Fallback to PHP upload_tmp_dir + $dirs = [ + $this->getTempDir('upload'), + ini_get('upload_tmp_dir'), + sys_get_temp_dir(), + ]; + + foreach ($dirs as $dir) { + if (! empty($dir) && @is_writable($dir)) { + return realpath($dir); + } + } + + return null; + } + + /** + * Selects server based on request parameters. + * + * @return integer + */ + public function selectServer(): int + { + $request = empty($_REQUEST['server']) ? 0 : $_REQUEST['server']; + + /** + * Lookup server by name + * (see FAQ 4.8) + */ + if (! is_numeric($request)) { + foreach ($this->settings['Servers'] as $i => $server) { + $verboseToLower = mb_strtolower($server['verbose']); + $serverToLower = mb_strtolower($request); + if ($server['host'] == $request + || $server['verbose'] == $request + || $verboseToLower == $serverToLower + || md5($verboseToLower) === $serverToLower + ) { + $request = $i; + break; + } + } + if (is_string($request)) { + $request = 0; + } + } + + /** + * If no server is selected, make sure that $this->settings['Server'] is empty (so + * that nothing will work), and skip server authentication. + * We do NOT exit here, but continue on without logging into any server. + * This way, the welcome page will still come up (with no server info) and + * present a choice of servers in the case that there are multiple servers + * and '$this->settings['ServerDefault'] = 0' is set. + */ + + if (is_numeric($request) && ! empty($request) && ! empty($this->settings['Servers'][$request])) { + $server = $request; + $this->settings['Server'] = $this->settings['Servers'][$server]; + } else { + if (! empty($this->settings['Servers'][$this->settings['ServerDefault']])) { + $server = $this->settings['ServerDefault']; + $this->settings['Server'] = $this->settings['Servers'][$server]; + } else { + $server = 0; + $this->settings['Server'] = []; + } + } + + return (int) $server; + } + + /** + * Checks whether Servers configuration is valid and possibly apply fixups. + * + * @return void + */ + public function checkServers(): void + { + // Do we have some server? + if (! isset($this->settings['Servers']) || count($this->settings['Servers']) === 0) { + // No server => create one with defaults + $this->settings['Servers'] = [1 => $this->default_server]; + } else { + // We have server(s) => apply default configuration + $new_servers = []; + + foreach ($this->settings['Servers'] as $server_index => $each_server) { + // Detect wrong configuration + if (! is_int($server_index) || $server_index < 1) { + trigger_error( + sprintf(__('Invalid server index: %s'), $server_index), + E_USER_ERROR + ); + } + + $each_server = array_merge($this->default_server, $each_server); + + // Final solution to bug #582890 + // If we are using a socket connection + // and there is nothing in the verbose server name + // or the host field, then generate a name for the server + // in the form of "Server 2", localized of course! + if (empty($each_server['host']) && empty($each_server['verbose'])) { + $each_server['verbose'] = sprintf(__('Server %d'), $server_index); + } + + $new_servers[$server_index] = $each_server; + } + $this->settings['Servers'] = $new_servers; + } + } +} + +if (! defined('TESTSUITE')) { + register_shutdown_function([Config::class, 'fatalErrorHandler']); +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/ConfigFile.php b/srcs/phpmyadmin/libraries/classes/Config/ConfigFile.php new file mode 100644 index 0000000..1e053dd --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/ConfigFile.php @@ -0,0 +1,531 @@ +_defaultCfg; + include ROOT_PATH . 'libraries/config.default.php'; + + // load additional config information + $this->_cfgDb = include ROOT_PATH . 'libraries/config.values.php'; + + // apply default values overrides + if (count($this->_cfgDb['_overrides'])) { + foreach ($this->_cfgDb['_overrides'] as $path => $value) { + Core::arrayWrite($path, $cfg, $value); + } + } + + $this->_baseCfg = $baseConfig; + $this->_isInSetup = $baseConfig === null; + $this->_id = 'ConfigFile' . $GLOBALS['server']; + if (! isset($_SESSION[$this->_id])) { + $_SESSION[$this->_id] = []; + } + } + + /** + * Sets names of config options which will be placed in config file even if + * they are set to their default values (use only full paths) + * + * @param array $keys the names of the config options + * + * @return void + */ + public function setPersistKeys(array $keys) + { + // checking key presence is much faster than searching so move values + // to keys + $this->_persistKeys = array_flip($keys); + } + + /** + * Returns flipped array set by {@link setPersistKeys()} + * + * @return array + */ + public function getPersistKeysMap() + { + return $this->_persistKeys; + } + + /** + * By default ConfigFile allows setting of all configuration keys, use + * this method to set up a filter on {@link set()} method + * + * @param array|null $keys array of allowed keys or null to remove filter + * + * @return void + */ + public function setAllowedKeys($keys) + { + if ($keys === null) { + $this->_setFilter = null; + return; + } + // checking key presence is much faster than searching so move values + // to keys + $this->_setFilter = array_flip($keys); + } + + /** + * Sets path mapping for updating config in + * {@link updateWithGlobalConfig()} or reading + * by {@link getConfig()} or {@link getConfigArray()} + * + * @param array $mapping Contains the mapping of "Server/config options" + * to "Server/1/config options" + * + * @return void + */ + public function setCfgUpdateReadMapping(array $mapping) + { + $this->_cfgUpdateReadMapping = $mapping; + } + + /** + * Resets configuration data + * + * @return void + */ + public function resetConfigData() + { + $_SESSION[$this->_id] = []; + } + + /** + * Sets configuration data (overrides old data) + * + * @param array $cfg Configuration options + * + * @return void + */ + public function setConfigData(array $cfg) + { + $_SESSION[$this->_id] = $cfg; + } + + /** + * Sets config value + * + * @param string $path Path + * @param mixed $value Value + * @param string $canonicalPath Canonical path + * + * @return void + */ + public function set($path, $value, $canonicalPath = null) + { + if ($canonicalPath === null) { + $canonicalPath = $this->getCanonicalPath($path); + } + // apply key whitelist + if ($this->_setFilter !== null + && ! isset($this->_setFilter[$canonicalPath]) + ) { + return; + } + // if the path isn't protected it may be removed + if (isset($this->_persistKeys[$canonicalPath])) { + Core::arrayWrite($path, $_SESSION[$this->_id], $value); + return; + } + + $defaultValue = $this->getDefault($canonicalPath); + $removePath = $value === $defaultValue; + if ($this->_isInSetup) { + // remove if it has a default value or is empty + $removePath = $removePath + || (empty($value) && empty($defaultValue)); + } else { + // get original config values not overwritten by user + // preferences to allow for overwriting options set in + // config.inc.php with default values + $instanceDefaultValue = Core::arrayRead( + $canonicalPath, + $this->_baseCfg + ); + // remove if it has a default value and base config (config.inc.php) + // uses default value + $removePath = $removePath + && ($instanceDefaultValue === $defaultValue); + } + if ($removePath) { + Core::arrayRemove($path, $_SESSION[$this->_id]); + return; + } + + Core::arrayWrite($path, $_SESSION[$this->_id], $value); + } + + /** + * Flattens multidimensional array, changes indices to paths + * (eg. 'key/subkey'). + * Used as array_walk() callback. + * + * @param mixed $value Value + * @param mixed $key Key + * @param mixed $prefix Prefix + * + * @return void + */ + private function _flattenArray($value, $key, $prefix) + { + // no recursion for numeric arrays + if (is_array($value) && ! isset($value[0])) { + $prefix .= $key . '/'; + array_walk($value, [$this, '_flattenArray'], $prefix); + } else { + $this->_flattenArrayResult[$prefix . $key] = $value; + } + } + + /** + * Returns default config in a flattened array + * + * @return array + */ + public function getFlatDefaultConfig() + { + $this->_flattenArrayResult = []; + array_walk($this->_defaultCfg, [$this, '_flattenArray'], ''); + $flatConfig = $this->_flattenArrayResult; + $this->_flattenArrayResult = null; + return $flatConfig; + } + + /** + * Updates config with values read from given array + * (config will contain differences to defaults from config.defaults.php). + * + * @param array $cfg Configuration + * + * @return void + */ + public function updateWithGlobalConfig(array $cfg) + { + // load config array and flatten it + $this->_flattenArrayResult = []; + array_walk($cfg, [$this, '_flattenArray'], ''); + $flatConfig = $this->_flattenArrayResult; + $this->_flattenArrayResult = null; + + // save values map for translating a few user preferences paths, + // should be complemented by code reading from generated config + // to perform inverse mapping + foreach ($flatConfig as $path => $value) { + if (isset($this->_cfgUpdateReadMapping[$path])) { + $path = $this->_cfgUpdateReadMapping[$path]; + } + $this->set($path, $value, $path); + } + } + + /** + * Returns config value or $default if it's not set + * + * @param string $path Path of config file + * @param mixed $default Default values + * + * @return mixed + */ + public function get($path, $default = null) + { + return Core::arrayRead($path, $_SESSION[$this->_id], $default); + } + + /** + * Returns default config value or $default it it's not set ie. it doesn't + * exist in config.default.php ($cfg) and config.values.php + * ($_cfg_db['_overrides']) + * + * @param string $canonicalPath Canonical path + * @param mixed $default Default value + * + * @return mixed + */ + public function getDefault($canonicalPath, $default = null) + { + return Core::arrayRead($canonicalPath, $this->_defaultCfg, $default); + } + + /** + * Returns config value, if it's not set uses the default one; returns + * $default if the path isn't set and doesn't contain a default value + * + * @param string $path Path + * @param mixed $default Default value + * + * @return mixed + */ + public function getValue($path, $default = null) + { + $v = Core::arrayRead($path, $_SESSION[$this->_id], null); + if ($v !== null) { + return $v; + } + $path = $this->getCanonicalPath($path); + return $this->getDefault($path, $default); + } + + /** + * Returns canonical path + * + * @param string $path Path + * + * @return string + */ + public function getCanonicalPath($path) + { + return preg_replace('#^Servers/([\d]+)/#', 'Servers/1/', $path); + } + + /** + * Returns config database entry for $path + * + * @param string $path path of the variable in config db + * @param mixed $default default value + * + * @return mixed + */ + public function getDbEntry($path, $default = null) + { + return Core::arrayRead($path, $this->_cfgDb, $default); + } + + /** + * Returns server count + * + * @return int + */ + public function getServerCount() + { + return isset($_SESSION[$this->_id]['Servers']) + ? count($_SESSION[$this->_id]['Servers']) + : 0; + } + + /** + * Returns server list + * + * @return array|null + */ + public function getServers() + { + return isset($_SESSION[$this->_id]['Servers']) + ? $_SESSION[$this->_id]['Servers'] + : null; + } + + /** + * Returns DSN of given server + * + * @param integer $server server index + * + * @return string + */ + public function getServerDSN($server) + { + if (! isset($_SESSION[$this->_id]['Servers'][$server])) { + return ''; + } + + $path = 'Servers/' . $server; + $dsn = 'mysqli://'; + if ($this->getValue("$path/auth_type") == 'config') { + $dsn .= $this->getValue("$path/user"); + if (! empty($this->getValue("$path/password"))) { + $dsn .= ':***'; + } + $dsn .= '@'; + } + if ($this->getValue("$path/host") != 'localhost') { + $dsn .= $this->getValue("$path/host"); + $port = $this->getValue("$path/port"); + if ($port) { + $dsn .= ':' . $port; + } + } else { + $dsn .= $this->getValue("$path/socket"); + } + return $dsn; + } + + /** + * Returns server name + * + * @param int $id server index + * + * @return string + */ + public function getServerName($id) + { + if (! isset($_SESSION[$this->_id]['Servers'][$id])) { + return ''; + } + $verbose = $this->get("Servers/$id/verbose"); + if (! empty($verbose)) { + return $verbose; + } + $host = $this->get("Servers/$id/host"); + return empty($host) ? 'localhost' : $host; + } + + /** + * Removes server + * + * @param int $server server index + * + * @return void + */ + public function removeServer($server) + { + if (! isset($_SESSION[$this->_id]['Servers'][$server])) { + return; + } + $lastServer = $this->getServerCount(); + + for ($i = $server; $i < $lastServer; $i++) { + $_SESSION[$this->_id]['Servers'][$i] + = $_SESSION[$this->_id]['Servers'][$i + 1]; + } + unset($_SESSION[$this->_id]['Servers'][$lastServer]); + + if (isset($_SESSION[$this->_id]['ServerDefault']) + && $_SESSION[$this->_id]['ServerDefault'] == $lastServer + ) { + unset($_SESSION[$this->_id]['ServerDefault']); + } + } + + /** + * Returns configuration array (full, multidimensional format) + * + * @return array + */ + public function getConfig() + { + $c = $_SESSION[$this->_id]; + foreach ($this->_cfgUpdateReadMapping as $mapTo => $mapFrom) { + // if the key $c exists in $map_to + if (Core::arrayRead($mapTo, $c) !== null) { + Core::arrayWrite($mapTo, $c, Core::arrayRead($mapFrom, $c)); + Core::arrayRemove($mapFrom, $c); + } + } + return $c; + } + + /** + * Returns configuration array (flat format) + * + * @return array + */ + public function getConfigArray() + { + $this->_flattenArrayResult = []; + array_walk($_SESSION[$this->_id], [$this, '_flattenArray'], ''); + $c = $this->_flattenArrayResult; + $this->_flattenArrayResult = null; + + $persistKeys = array_diff( + array_keys($this->_persistKeys), + array_keys($c) + ); + foreach ($persistKeys as $k) { + $c[$k] = $this->getDefault($this->getCanonicalPath($k)); + } + + foreach ($this->_cfgUpdateReadMapping as $mapTo => $mapFrom) { + if (! isset($c[$mapFrom])) { + continue; + } + $c[$mapTo] = $c[$mapFrom]; + unset($c[$mapFrom]); + } + return $c; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Descriptions.php b/srcs/phpmyadmin/libraries/classes/Config/Descriptions.php new file mode 100644 index 0000000..22e7c1f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Descriptions.php @@ -0,0 +1,934 @@ + __('If enabled, user can enter any MySQL server in login form for cookie auth.'), + 'AllowArbitraryServer_name' => __('Allow login to any MySQL server'), + 'ArbitraryServerRegexp_desc' => __( + 'Restricts the MySQL servers the user can enter when a login to an arbitrary ' + . 'MySQL server is enabled by matching the IP or hostname of the MySQL server ' . + 'to the given regular expression.' + ), + 'ArbitraryServerRegexp_name' => __('Restrict login to MySQL server'), + 'AllowThirdPartyFraming_desc' => __( + 'Enabling this allows a page located on a different domain to call phpMyAdmin ' + . 'inside a frame, and is a potential [strong]security hole[/strong] allowing ' + . 'cross-frame scripting (XSS) attacks.' + ), + 'AllowThirdPartyFraming_name' => __('Allow third party framing'), + 'AllowUserDropDatabase_name' => __('Show "Drop database" link to normal users'), + 'blowfish_secret_desc' => __( + 'Secret passphrase used for encrypting cookies in [kbd]cookie[/kbd] ' + . 'authentication.' + ), + 'blowfish_secret_name' => __('Blowfish secret'), + 'BrowseMarkerEnable_desc' => __('Highlight selected rows.'), + 'BrowseMarkerEnable_name' => __('Row marker'), + 'BrowsePointerEnable_desc' => __('Highlight row pointed by the mouse cursor.'), + 'BrowsePointerEnable_name' => __('Highlight pointer'), + 'BZipDump_desc' => __( + 'Enable bzip2 compression for' + . ' import operations.' + ), + 'BZipDump_name' => __('Bzip2'), + 'CharEditing_desc' => __( + 'Defines which type of editing controls should be used for CHAR and VARCHAR ' + . 'columns; [kbd]input[/kbd] - allows limiting of input length, ' + . '[kbd]textarea[/kbd] - allows newlines in columns.' + ), + 'CharEditing_name' => __('CHAR columns editing'), + 'CodemirrorEnable_desc' => __( + 'Use user-friendly editor for editing SQL queries ' + . '(CodeMirror) with syntax highlighting and ' + . 'line numbers.' + ), + 'CodemirrorEnable_name' => __('Enable CodeMirror'), + 'LintEnable_desc' => __( + 'Find any errors in the query before executing it.' + . ' Requires CodeMirror to be enabled.' + ), + 'LintEnable_name' => __('Enable linter'), + 'MinSizeForInputField_desc' => __( + 'Defines the minimum size for input fields generated for CHAR and VARCHAR ' + . 'columns.' + ), + 'MinSizeForInputField_name' => __('Minimum size for input field'), + 'MaxSizeForInputField_desc' => __( + 'Defines the maximum size for input fields generated for CHAR and VARCHAR ' + . 'columns.' + ), + 'MaxSizeForInputField_name' => __('Maximum size for input field'), + 'CharTextareaCols_desc' => __('Number of columns for CHAR/VARCHAR textareas.'), + 'CharTextareaCols_name' => __('CHAR textarea columns'), + 'CharTextareaRows_desc' => __('Number of rows for CHAR/VARCHAR textareas.'), + 'CharTextareaRows_name' => __('CHAR textarea rows'), + 'CheckConfigurationPermissions_name' => __('Check config file permissions'), + 'CompressOnFly_desc' => __( + 'Compress gzip exports on the fly without the need for much memory; if ' + . 'you encounter problems with created gzip files disable this feature.' + ), + 'CompressOnFly_name' => __('Compress on the fly'), + 'Confirm_desc' => __( + 'Whether a warning ("Are your really sure…") should be displayed ' + . 'when you\'re about to lose data.' + ), + 'Confirm_name' => __('Confirm DROP queries'), + 'DBG_sql_desc' => __('Log SQL queries and their execution time, to be displayed in the console'), + 'DBG_sql_name' => __('Debug SQL'), + 'DefaultTabDatabase_desc' => __('Tab that is displayed when entering a database.'), + 'DefaultTabDatabase_name' => __('Default database tab'), + 'DefaultTabServer_desc' => __('Tab that is displayed when entering a server.'), + 'DefaultTabServer_name' => __('Default server tab'), + 'DefaultTabTable_desc' => __('Tab that is displayed when entering a table.'), + 'DefaultTabTable_name' => __('Default table tab'), + 'EnableAutocompleteForTablesAndColumns_desc' => __('Autocomplete of the table and column names in the SQL queries.'), + 'EnableAutocompleteForTablesAndColumns_name' => __('Enable autocomplete for table and column names'), + 'HideStructureActions_desc' => __('Whether the table structure actions should be hidden.'), + 'ShowColumnComments_name' => __('Show column comments'), + 'ShowColumnComments_desc' => __('Whether column comments should be shown in table structure view'), + 'HideStructureActions_name' => __('Hide table structure actions'), + 'DefaultTransformations_Hex_name' => __('Default transformations for Hex'), + 'DefaultTransformations_Hex_desc' => __('Values for options list for default transformations. These will be overwritten if transformation is filled in at table structure page.'), + 'DefaultTransformations_Substring_name' => __('Default transformations for Substring'), + 'DefaultTransformations_Substring_desc' => __('Values for options list for default transformations. These will be overwritten if transformation is filled in at table structure page.'), + 'DefaultTransformations_Bool2Text_name' => __('Default transformations for Bool2Text'), + 'DefaultTransformations_Bool2Text_desc' => __('Values for options list for default transformations. These will be overwritten if transformation is filled in at table structure page.'), + 'DefaultTransformations_External_name' => __('Default transformations for External'), + 'DefaultTransformations_External_desc' => __('Values for options list for default transformations. These will be overwritten if transformation is filled in at table structure page.'), + 'DefaultTransformations_PreApPend_name' => __('Default transformations for PreApPend'), + 'DefaultTransformations_PreApPend_desc' => __('Values for options list for default transformations. These will be overwritten if transformation is filled in at table structure page.'), + 'DefaultTransformations_DateFormat_name' => __('Default transformations for DateFormat'), + 'DefaultTransformations_DateFormat_desc' => __('Values for options list for default transformations. These will be overwritten if transformation is filled in at table structure page.'), + 'DefaultTransformations_Inline_name' => __('Default transformations for Inline'), + 'DefaultTransformations_Inline_desc' => __('Values for options list for default transformations. These will be overwritten if transformation is filled in at table structure page.'), + 'DefaultTransformations_TextImageLink_name' => __('Default transformations for TextImageLink'), + 'DefaultTransformations_TextImageLink_desc' => __('Values for options list for default transformations. These will be overwritten if transformation is filled in at table structure page.'), + 'DefaultTransformations_TextLink_name' => __('Default transformations for TextLink'), + 'DefaultTransformations_TextLink_desc' => __('Values for options list for default transformations. These will be overwritten if transformation is filled in at table structure page.'), + + 'DisplayServersList_desc' => __('Show server listing as a list instead of a drop down.'), + 'DisplayServersList_name' => __('Display servers as a list'), + 'DisableMultiTableMaintenance_desc' => __( + 'Disable the table maintenance mass operations, like optimizing or repairing ' + . 'the selected tables of a database.' + ), + 'DisableMultiTableMaintenance_name' => __('Disable multi table maintenance'), + 'ExecTimeLimit_desc' => __( + 'Set the number of seconds a script is allowed to run ([kbd]0[/kbd] for no ' + . 'limit).' + ), + 'ExecTimeLimit_name' => __('Maximum execution time'), + 'Export_lock_tables_name' => sprintf( + __('Use %s statement'), + htmlspecialchars('LOCK TABLES') + ), + 'Export_asfile_name' => __('Save as file'), + 'Export_charset_name' => __('Character set of the file'), + 'Export_codegen_format_name' => __('Format'), + 'Export_compression_name' => __('Compression'), + 'Export_csv_columns_name' => __('Put columns names in the first row'), + 'Export_csv_enclosed_name' => __('Columns enclosed with'), + 'Export_csv_escaped_name' => __('Columns escaped with'), + 'Export_csv_null_name' => __('Replace NULL with'), + 'Export_csv_removeCRLF_name' => __('Remove CRLF characters within columns'), + 'Export_csv_separator_name' => __('Columns terminated with'), + 'Export_csv_terminated_name' => __('Lines terminated with'), + 'Export_excel_columns_name' => __('Put columns names in the first row'), + 'Export_excel_edition_name' => __('Excel edition'), + 'Export_excel_null_name' => __('Replace NULL with'), + 'Export_excel_removeCRLF_name' => __('Remove CRLF characters within columns'), + 'Export_file_template_database_name' => __('Database name template'), + 'Export_file_template_server_name' => __('Server name template'), + 'Export_file_template_table_name' => __('Table name template'), + 'Export_format_name' => __('Format'), + 'Export_htmlword_columns_name' => __('Put columns names in the first row'), + 'Export_htmlword_null_name' => __('Replace NULL with'), + 'Export_htmlword_structure_or_data_name' => __('Dump table'), + 'Export_latex_caption_name' => __('Include table caption'), + 'Export_latex_columns_name' => __('Put columns names in the first row'), + 'Export_latex_comments_name' => __('Comments'), + 'Export_latex_data_caption_name' => __('Table caption'), + 'Export_latex_data_continued_caption_name' => __('Continued table caption'), + 'Export_latex_data_label_name' => __('Label key'), + 'Export_latex_mime_name' => __('Media (MIME) type'), + 'Export_latex_null_name' => __('Replace NULL with'), + 'Export_latex_relation_name' => __('Relationships'), + 'Export_latex_structure_caption_name' => __('Table caption'), + 'Export_latex_structure_continued_caption_name' => __('Continued table caption'), + 'Export_latex_structure_label_name' => __('Label key'), + 'Export_latex_structure_or_data_name' => __('Dump table'), + 'Export_method_name' => __('Export method'), + 'Export_ods_columns_name' => __('Put columns names in the first row'), + 'Export_ods_null_name' => __('Replace NULL with'), + 'Export_odt_columns_name' => __('Put columns names in the first row'), + 'Export_odt_comments_name' => __('Comments'), + 'Export_odt_mime_name' => __('Media (MIME) type'), + 'Export_odt_null_name' => __('Replace NULL with'), + 'Export_odt_relation_name' => __('Relationships'), + 'Export_odt_structure_or_data_name' => __('Dump table'), + 'Export_onserver_name' => __('Save on server'), + 'Export_onserver_overwrite_name' => __('Overwrite existing file(s)'), + 'Export_as_separate_files_name' => __('Export as separate files'), + 'Export_quick_export_onserver_name' => __('Save on server'), + 'Export_quick_export_onserver_overwrite_name' => __('Overwrite existing file(s)'), + 'Export_remember_file_template_name' => __('Remember file name template'), + 'Export_sql_auto_increment_name' => __('Add AUTO_INCREMENT value'), + 'Export_sql_backquotes_name' => __('Enclose table and column names with backquotes'), + 'Export_sql_compatibility_name' => __('SQL compatibility mode'), + 'Export_sql_dates_name' => __('Creation/Update/Check dates'), + 'Export_sql_delayed_name' => __('Use delayed inserts'), + 'Export_sql_disable_fk_name' => __('Disable foreign key checks'), + 'Export_sql_views_as_tables_name' => __('Export views as tables'), + 'Export_sql_metadata_name' => __('Export related metadata from phpMyAdmin configuration storage'), + 'Export_sql_create_database_name' => sprintf(__('Add %s'), 'CREATE DATABASE / USE'), + 'Export_sql_drop_database_name' => sprintf(__('Add %s'), 'DROP DATABASE'), + 'Export_sql_drop_table_name' => sprintf( + __('Add %s'), + 'DROP TABLE / VIEW / PROCEDURE / FUNCTION / EVENT / TRIGGER' + ), + 'Export_sql_create_table_name' => sprintf(__('Add %s'), 'CREATE TABLE'), + 'Export_sql_create_view_name' => sprintf(__('Add %s'), 'CREATE VIEW'), + 'Export_sql_create_trigger_name' => sprintf(__('Add %s'), 'CREATE TRIGGER'), + 'Export_sql_hex_for_binary_name' => __('Use hexadecimal for BINARY & BLOB'), + 'Export_sql_if_not_exists_name' => __( + 'Add IF NOT EXISTS (less efficient as indexes will be generated during' + . ' table creation)' + ), + 'Export_sql_view_current_user' => __('Exclude definition of current user'), + 'Export_sql_or_replace_view_name' => sprintf(__('%s view'), 'OR REPLACE'), + 'Export_sql_ignore_name' => __('Use ignore inserts'), + 'Export_sql_include_comments_name' => __('Comments'), + 'Export_sql_insert_syntax_name' => __('Syntax to use when inserting data'), + 'Export_sql_max_query_size_name' => __('Maximal length of created query'), + 'Export_sql_mime_name' => __('Media (MIME) type'), + 'Export_sql_procedure_function_name' => sprintf(__('Add %s'), 'CREATE PROCEDURE / FUNCTION / EVENT'), + 'Export_sql_relation_name' => __('Relationships'), + 'Export_sql_structure_or_data_name' => __('Dump table'), + 'Export_sql_type_name' => __('Export type'), + 'Export_sql_use_transaction_name' => __('Enclose export in a transaction'), + 'Export_sql_utc_time_name' => __('Export time in UTC'), + 'Export_texytext_columns_name' => __('Put columns names in the first row'), + 'Export_texytext_null_name' => __('Replace NULL with'), + 'Export_texytext_structure_or_data_name' => __('Dump table'), + 'ForeignKeyDropdownOrder_desc' => __( + 'Sort order for items in a foreign-key dropdown box; [kbd]content[/kbd] is ' + . 'the referenced data, [kbd]id[/kbd] is the key value.' + ), + 'ForeignKeyDropdownOrder_name' => __('Foreign key dropdown order'), + 'ForeignKeyMaxLimit_desc' => __('A dropdown will be used if fewer items are present.'), + 'ForeignKeyMaxLimit_name' => __('Foreign key limit'), + 'DefaultForeignKeyChecks_desc' => __('Default value for foreign key checks checkbox for some queries.'), + 'DefaultForeignKeyChecks_name' => __('Foreign key checks'), + 'Form_Browse_name' => __('Browse mode'), + 'Form_Browse_desc' => __('Customize browse mode.'), + 'Form_CodeGen_name' => 'CodeGen', + 'Form_CodeGen_desc' => __('Customize default options.'), + 'Form_Csv_name' => __('CSV'), + 'Form_Csv_desc' => __('Customize default options.'), + 'Form_Developer_name' => __('Developer'), + 'Form_Developer_desc' => __('Settings for phpMyAdmin developers.'), + 'Form_Edit_name' => __('Edit mode'), + 'Form_Edit_desc' => __('Customize edit mode.'), + 'Form_Export_defaults_name' => __('Export defaults'), + 'Form_Export_defaults_desc' => __('Customize default export options.'), + 'Form_General_name' => __('General'), + 'Form_General_desc' => __('Set some commonly used options.'), + 'Form_Import_defaults_name' => __('Import defaults'), + 'Form_Import_defaults_desc' => __('Customize default common import options.'), + 'Form_Import_export_name' => __('Import / export'), + 'Form_Import_export_desc' => __('Set import and export directories and compression options.'), + 'Form_Latex_name' => __('LaTeX'), + 'Form_Latex_desc' => __('Customize default options.'), + 'Form_Navi_databases_name' => __('Databases'), + 'Form_Navi_databases_desc' => __('Databases display options.'), + 'Form_Navi_panel_name' => __('Navigation panel'), + 'Form_Navi_panel_desc' => __('Customize appearance of the navigation panel.'), + 'Form_Navi_tree_name' => __('Navigation tree'), + 'Form_Navi_tree_desc' => __('Customize the navigation tree.'), + 'Form_Navi_servers_name' => __('Servers'), + 'Form_Navi_servers_desc' => __('Servers display options.'), + 'Form_Navi_tables_name' => __('Tables'), + 'Form_Navi_tables_desc' => __('Tables display options.'), + 'Form_Main_panel_name' => __('Main panel'), + 'Form_Microsoft_Office_name' => __('Microsoft Office'), + 'Form_Microsoft_Office_desc' => __('Customize default options.'), + 'Form_Open_Document_name' => 'OpenDocument', + 'Form_Open_Document_desc' => __('Customize default options.'), + 'Form_Other_core_settings_name' => __('Other core settings'), + 'Form_Other_core_settings_desc' => __('Settings that didn\'t fit anywhere else.'), + 'Form_Page_titles_name' => __('Page titles'), + 'Form_Page_titles_desc' => __( + 'Specify browser\'s title bar text. Refer to ' + . '[doc@faq6-27]documentation[/doc] for magic strings that can be used ' + . 'to get special values.' + ), + 'Form_Security_name' => __('Security'), + 'Form_Security_desc' => __( + 'Please note that phpMyAdmin is just a user interface and its features do not ' + . 'limit MySQL.' + ), + 'Form_Server_name' => __('Basic settings'), + 'Form_Server_auth_name' => __('Authentication'), + 'Form_Server_auth_desc' => __('Authentication settings.'), + 'Form_Server_config_name' => __('Server configuration'), + 'Form_Server_config_desc' => __( + 'Advanced server configuration, do not change these options unless you know ' + . 'what they are for.' + ), + 'Form_Server_desc' => __('Enter server connection parameters.'), + 'Form_Server_pmadb_name' => __('Configuration storage'), + 'Form_Server_pmadb_desc' => __( + 'Configure phpMyAdmin configuration storage to gain access to additional ' + . 'features, see [doc@linked-tables]phpMyAdmin configuration storage[/doc] in ' + . 'documentation.' + ), + 'Form_Server_tracking_name' => __('Changes tracking'), + 'Form_Server_tracking_desc' => __( + 'Tracking of changes made in database. Requires the phpMyAdmin configuration ' + . 'storage.' + ), + 'Form_Sql_name' => __('SQL'), + 'Form_Sql_box_name' => __('SQL Query box'), + 'Form_Sql_box_desc' => __('Customize links shown in SQL Query boxes.'), + 'Form_Sql_desc' => __('Customize default options.'), + 'Form_Sql_queries_name' => __('SQL queries'), + 'Form_Sql_queries_desc' => __('SQL queries settings.'), + 'Form_Startup_name' => __('Startup'), + 'Form_Startup_desc' => __('Customize startup page.'), + 'Form_DbStructure_name' => __('Database structure'), + 'Form_DbStructure_desc' => __('Choose which details to show in the database structure (list of tables).'), + 'Form_TableStructure_name' => __('Table structure'), + 'Form_TableStructure_desc' => __('Settings for the table structure (list of columns).'), + 'Form_Tabs_name' => __('Tabs'), + 'Form_Tabs_desc' => __('Choose how you want tabs to work.'), + 'Form_DisplayRelationalSchema_name' => __('Display relational schema'), + 'Form_DisplayRelationalSchema_desc' => '', + 'PDFDefaultPageSize_name' => __('Paper size'), + 'PDFDefaultPageSize_desc' => '', + 'Form_Databases_name' => __('Databases'), + 'Form_Text_fields_name' => __('Text fields'), + 'Form_Text_fields_desc' => __('Customize text input fields.'), + 'Form_Texy_name' => __('Texy! text'), + 'Form_Texy_desc' => __('Customize default options'), + 'Form_Warnings_name' => __('Warnings'), + 'Form_Warnings_desc' => __('Disable some of the warnings shown by phpMyAdmin.'), + 'Form_Console_name' => __('Console'), + 'GZipDump_desc' => __( + 'Enable gzip compression for import ' + . 'and export operations.' + ), + 'GZipDump_name' => __('GZip'), + 'IconvExtraParams_name' => __('Extra parameters for iconv'), + 'IgnoreMultiSubmitErrors_desc' => __( + 'If enabled, phpMyAdmin continues computing multiple-statement queries even if ' + . 'one of the queries failed.' + ), + 'IgnoreMultiSubmitErrors_name' => __('Ignore multiple statement errors'), + 'Import_allow_interrupt_desc' => __( + 'Allow interrupt of import in case script detects it is close to time limit. ' + . 'This might be a good way to import large files, however it can break ' + . 'transactions.' + ), + 'enable_drag_drop_import_name' => __('Enable drag and drop import'), + 'enable_drag_drop_import_desc' => __('Uncheck the checkbox to disable drag and drop import'), + 'Import_allow_interrupt_name' => __('Partial import: allow interrupt'), + 'Import_charset_name' => __('Character set of the file'), + 'Import_csv_col_names_name' => __('Lines terminated with'), + 'Import_csv_enclosed_name' => __('Columns enclosed with'), + 'Import_csv_escaped_name' => __('Columns escaped with'), + 'Import_csv_ignore_name' => __('Do not abort on INSERT error'), + 'Import_csv_replace_name' => __('Add ON DUPLICATE KEY UPDATE'), + 'Import_csv_replace_desc' => __('Update data when duplicate keys found on import'), + 'Import_csv_terminated_name' => __('Columns terminated with'), + 'Import_format_desc' => __( + 'Default format; be aware that this list depends on location (database, table) ' + . 'and only SQL is always available.' + ), + 'Import_format_name' => __('Format of imported file'), + 'Import_ldi_enclosed_name' => __('Columns enclosed with'), + 'Import_ldi_escaped_name' => __('Columns escaped with'), + 'Import_ldi_ignore_name' => __('Do not abort on INSERT error'), + 'Import_ldi_local_option_name' => __('Use LOCAL keyword'), + 'Import_ldi_replace_name' => __('Add ON DUPLICATE KEY UPDATE'), + 'Import_ldi_replace_desc' => __('Update data when duplicate keys found on import'), + 'Import_ldi_terminated_name' => __('Columns terminated with'), + 'Import_ods_col_names_name' => __('Column names in first row'), + 'Import_ods_empty_rows_name' => __('Do not import empty rows'), + 'Import_ods_recognize_currency_name' => __('Import currencies ($5.00 to 5.00)'), + 'Import_ods_recognize_percentages_name' => __('Import percentages as proper decimals (12.00% to .12)'), + 'Import_skip_queries_desc' => __('Number of queries to skip from start.'), + 'Import_skip_queries_name' => __('Partial import: skip queries'), + 'Import_sql_compatibility_name' => __('SQL compatibility mode'), + 'Import_sql_no_auto_value_on_zero_name' => __('Do not use AUTO_INCREMENT for zero values'), + 'Import_sql_read_as_multibytes_name' => __('Read as multibytes'), + 'InitialSlidersState_name' => __('Initial state for sliders'), + 'InsertRows_desc' => __('How many rows can be inserted at one time.'), + 'InsertRows_name' => __('Number of inserted rows'), + 'LimitChars_desc' => __('Maximum number of characters shown in any non-numeric column on browse view.'), + 'LimitChars_name' => __('Limit column characters'), + 'LoginCookieDeleteAll_desc' => __( + 'If TRUE, logout deletes cookies for all servers; when set to FALSE, logout ' + . 'only occurs for the current server. Setting this to FALSE makes it easy to ' + . 'forget to log out from other servers when connected to multiple servers.' + ), + 'LoginCookieDeleteAll_name' => __('Delete all cookies on logout'), + 'LoginCookieRecall_desc' => __( + 'Define whether the previous login should be recalled or not in ' + . '[kbd]cookie[/kbd] authentication mode.' + ), + 'LoginCookieRecall_name' => __('Recall user name'), + 'LoginCookieStore_desc' => __( + 'Defines how long (in seconds) a login cookie should be stored in browser. ' + . 'The default of 0 means that it will be kept for the existing session only, ' + . 'and will be deleted as soon as you close the browser window. This is ' + . 'recommended for non-trusted environments.' + ), + 'LoginCookieStore_name' => __('Login cookie store'), + 'LoginCookieValidity_desc' => __('Define how long (in seconds) a login cookie is valid.'), + 'LoginCookieValidity_name' => __('Login cookie validity'), + 'LongtextDoubleTextarea_desc' => __('Double size of textarea for LONGTEXT columns.'), + 'LongtextDoubleTextarea_name' => __('Bigger textarea for LONGTEXT'), + 'MaxCharactersInDisplayedSQL_desc' => __('Maximum number of characters used when a SQL query is displayed.'), + 'MaxCharactersInDisplayedSQL_name' => __('Maximum displayed SQL length'), + 'MaxDbList_cmt' => __('Users cannot set a higher value'), + 'MaxDbList_desc' => __('Maximum number of databases displayed in database list.'), + 'MaxDbList_name' => __('Maximum databases'), + 'FirstLevelNavigationItems_desc' => __( + 'The number of items that can be displayed on each page on the first level' + . ' of the navigation tree.' + ), + 'FirstLevelNavigationItems_name' => __('Maximum items on first level'), + 'MaxNavigationItems_desc' => __('The number of items that can be displayed on each page of the navigation tree.'), + 'MaxNavigationItems_name' => __('Maximum items in branch'), + 'MaxRows_desc' => __( + 'Number of rows displayed when browsing a result set. If the result set ' + . 'contains more rows, "Previous" and "Next" links will be ' + . 'shown.' + ), + 'MaxRows_name' => __('Maximum number of rows to display'), + 'MaxTableList_cmt' => __('Users cannot set a higher value'), + 'MaxTableList_desc' => __('Maximum number of tables displayed in table list.'), + 'MaxTableList_name' => __('Maximum tables'), + 'MemoryLimit_desc' => __( + 'The number of bytes a script is allowed to allocate, eg. [kbd]32M[/kbd] ' + . '([kbd]-1[/kbd] for no limit and [kbd]0[/kbd] for no change).' + ), + 'MemoryLimit_name' => __('Memory limit'), + 'ShowDatabasesNavigationAsTree_desc' => __('In the navigation panel, replaces the database tree with a selector'), + 'ShowDatabasesNavigationAsTree_name' => __('Show databases navigation as tree'), + 'NavigationWidth_name' => __('Navigation panel width'), + 'NavigationWidth_desc' => __('Set to 0 to collapse navigation panel.'), + 'NavigationLinkWithMainPanel_desc' => __('Link with main panel by highlighting the current database or table.'), + 'NavigationLinkWithMainPanel_name' => __('Link with main panel'), + 'NavigationDisplayLogo_desc' => __('Show logo in navigation panel.'), + 'NavigationDisplayLogo_name' => __('Display logo'), + 'NavigationLogoLink_desc' => __('URL where logo in the navigation panel will point to.'), + 'NavigationLogoLink_name' => __('Logo link URL'), + 'NavigationLogoLinkWindow_desc' => __( + 'Open the linked page in the main window ([kbd]main[/kbd]) or in a new one ' + . '([kbd]new[/kbd]).' + ), + 'NavigationLogoLinkWindow_name' => __('Logo link target'), + 'NavigationDisplayServers_desc' => __('Display server choice at the top of the navigation panel.'), + 'NavigationDisplayServers_name' => __('Display servers selection'), + 'NavigationTreeDefaultTabTable_name' => __('Target for quick access icon'), + 'NavigationTreeDefaultTabTable2_name' => __('Target for second quick access icon'), + 'NavigationTreeDisplayItemFilterMinimum_desc' => __( + 'Defines the minimum number of items (tables, views, routines and events) to ' + . 'display a filter box.' + ), + 'NavigationTreeDisplayItemFilterMinimum_name' => __('Minimum number of items to display the filter box'), + 'NavigationTreeDisplayDbFilterMinimum_name' => __('Minimum number of databases to display the database filter box'), + 'NavigationTreeEnableGrouping_desc' => __( + 'Group items in the navigation tree (determined by the separator defined in ' . + 'the Databases and Tables tabs above).' + ), + 'NavigationTreeEnableGrouping_name' => __('Group items in the tree'), + 'NavigationTreeDbSeparator_desc' => __('String that separates databases into different tree levels.'), + 'NavigationTreeDbSeparator_name' => __('Database tree separator'), + 'NavigationTreeTableSeparator_desc' => __('String that separates tables into different tree levels.'), + 'NavigationTreeTableSeparator_name' => __('Table tree separator'), + 'NavigationTreeTableLevel_name' => __('Maximum table tree depth'), + 'NavigationTreePointerEnable_desc' => __('Highlight server under the mouse cursor.'), + 'NavigationTreePointerEnable_name' => __('Enable highlighting'), + 'NavigationTreeEnableExpansion_desc' => __('Whether to offer the possibility of tree expansion in the navigation panel.'), + 'NavigationTreeEnableExpansion_name' => __('Enable navigation tree expansion'), + 'NavigationTreeShowTables_name' => __('Show tables in tree'), + 'NavigationTreeShowTables_desc' => __('Whether to show tables under database in the navigation tree'), + 'NavigationTreeShowViews_name' => __('Show views in tree'), + 'NavigationTreeShowViews_desc' => __('Whether to show views under database in the navigation tree'), + 'NavigationTreeShowFunctions_name' => __('Show functions in tree'), + 'NavigationTreeShowFunctions_desc' => __('Whether to show functions under database in the navigation tree'), + 'NavigationTreeShowProcedures_name' => __('Show procedures in tree'), + 'NavigationTreeShowProcedures_desc' => __('Whether to show procedures under database in the navigation tree'), + 'NavigationTreeShowEvents_name' => __('Show events in tree'), + 'NavigationTreeShowEvents_desc' => __('Whether to show events under database in the navigation tree'), + 'NavigationTreeAutoexpandSingleDb_name' => __('Expand single database'), + 'NavigationTreeAutoexpandSingleDb_desc' => __('Whether to expand single database in the navigation tree automatically.'), + 'NumRecentTables_desc' => __('Maximum number of recently used tables; set 0 to disable.'), + 'NumFavoriteTables_desc' => __('Maximum number of favorite tables; set 0 to disable.'), + 'NumRecentTables_name' => __('Recently used tables'), + 'NumFavoriteTables_name' => __('Favorite tables'), + 'RowActionLinks_desc' => __('These are Edit, Copy and Delete links.'), + 'RowActionLinks_name' => __('Where to show the table row links'), + 'RowActionLinksWithoutUnique_desc' => __('Whether to show row links even in the absence of a unique key.'), + 'RowActionLinksWithoutUnique_name' => __('Show row links anyway'), + 'DisableShortcutKeys_name' => __('Disable shortcut keys'), + 'DisableShortcutKeys_desc' => __('Disable shortcut keys'), + 'NaturalOrder_desc' => __('Use natural order for sorting table and database names.'), + 'NaturalOrder_name' => __('Natural order'), + 'TableNavigationLinksMode_desc' => __('Use only icons, only text or both.'), + 'TableNavigationLinksMode_name' => __('Table navigation bar'), + 'OBGzip_desc' => __('Use GZip output buffering for increased speed in HTTP transfers.'), + 'OBGzip_name' => __('GZip output buffering'), + 'Order_desc' => __( + '[kbd]SMART[/kbd] - i.e. descending order for columns of type TIME, DATE, ' + . 'DATETIME and TIMESTAMP, ascending order otherwise.' + ), + 'Order_name' => __('Default sorting order'), + 'PersistentConnections_desc' => __('Use persistent connections to MySQL databases.'), + 'PersistentConnections_name' => __('Persistent connections'), + 'PmaNoRelation_DisableWarning_desc' => __( + 'Disable the default warning that is displayed on the database details ' + . 'Structure page if any of the required tables for the phpMyAdmin ' + . 'configuration storage could not be found.' + ), + 'PmaNoRelation_DisableWarning_name' => __('Missing phpMyAdmin configuration storage tables'), + 'ReservedWordDisableWarning_desc' => __( + 'Disable the default warning that is displayed on the Structure page if column ' + . 'names in a table are reserved MySQL words.' + ), + 'ReservedWordDisableWarning_name' => __('MySQL reserved word warning'), + 'TabsMode_desc' => __('Use only icons, only text or both.'), + 'TabsMode_name' => __('How to display the menu tabs'), + 'ActionLinksMode_desc' => __('Use only icons, only text or both.'), + 'ActionLinksMode_name' => __('How to display various action links'), + 'ProtectBinary_desc' => __('Disallow BLOB and BINARY columns from editing.'), + 'ProtectBinary_name' => __('Protect binary columns'), + 'QueryHistoryDB_desc' => __( + 'Enable if you want DB-based query history (requires phpMyAdmin configuration ' + . 'storage). If disabled, this utilizes JS-routines to display query history ' + . '(lost by window close).' + ), + 'QueryHistoryDB_name' => __('Permanent query history'), + 'QueryHistoryMax_cmt' => __('Users cannot set a higher value'), + 'QueryHistoryMax_desc' => __('How many queries are kept in history.'), + 'QueryHistoryMax_name' => __('Query history length'), + 'RecodingEngine_desc' => __('Select which functions will be used for character set conversion.'), + 'RecodingEngine_name' => __('Recoding engine'), + 'RememberSorting_desc' => __('When browsing tables, the sorting of each table is remembered.'), + 'RememberSorting_name' => __('Remember table\'s sorting'), + 'TablePrimaryKeyOrder_desc' => __('Default sort order for tables with a primary key.'), + 'TablePrimaryKeyOrder_name' => __('Primary key default sort order'), + 'RepeatCells_desc' => __('Repeat the headers every X cells, [kbd]0[/kbd] deactivates this feature.'), + 'RepeatCells_name' => __('Repeat headers'), + 'GridEditing_name' => __('Grid editing: trigger action'), + 'RelationalDisplay_name' => __('Relational display'), + 'RelationalDisplay_desc' => __('For display Options'), + 'SaveCellsAtOnce_name' => __('Grid editing: save all edited cells at once'), + 'SaveDir_desc' => __('Directory where exports can be saved on server.'), + 'SaveDir_name' => __('Save directory'), + 'Servers_AllowDeny_order_desc' => __('Leave blank if not used.'), + 'Servers_AllowDeny_order_name' => __('Host authorization order'), + 'Servers_AllowDeny_rules_desc' => __('Leave blank for defaults.'), + 'Servers_AllowDeny_rules_name' => __('Host authorization rules'), + 'Servers_AllowNoPassword_name' => __('Allow logins without a password'), + 'Servers_AllowRoot_name' => __('Allow root login'), + 'Servers_SessionTimeZone_name' => __('Session timezone'), + 'Servers_SessionTimeZone_desc' => __( + 'Sets the effective timezone; possibly different than the one from your ' + . 'database server' + ), + 'Servers_auth_http_realm_desc' => __('HTTP Basic Auth Realm name to display when doing HTTP Auth.'), + 'Servers_auth_http_realm_name' => __('HTTP Realm'), + 'Servers_auth_type_desc' => __('Authentication method to use.'), + 'Servers_auth_type_name' => __('Authentication type'), + 'Servers_bookmarktable_desc' => __( + 'Leave blank for no [doc@bookmarks@]bookmark[/doc] ' + . 'support, suggested: [kbd]pma__bookmark[/kbd]' + ), + 'Servers_bookmarktable_name' => __('Bookmark table'), + 'Servers_column_info_desc' => __( + 'Leave blank for no column comments/media (MIME) types, suggested: ' + . '[kbd]pma__column_info[/kbd].' + ), + 'Servers_column_info_name' => __('Column information table'), + 'Servers_compress_desc' => __('Compress connection to MySQL server.'), + 'Servers_compress_name' => __('Compress connection'), + 'Servers_controlpass_name' => __('Control user password'), + 'Servers_controluser_desc' => __( + 'A special MySQL user configured with limited permissions, more information ' + . 'available on [doc@linked-tables]documentation[/doc].' + ), + 'Servers_controluser_name' => __('Control user'), + 'Servers_controlhost_desc' => __( + 'An alternate host to hold the configuration storage; leave blank to use the ' + . 'already defined host.' + ), + 'Servers_controlhost_name' => __('Control host'), + 'Servers_controlport_desc' => __( + 'An alternate port to connect to the host that holds the configuration storage; ' + . 'leave blank to use the default port, or the already defined port, if the ' + . 'controlhost equals host.' + ), + 'Servers_controlport_name' => __('Control port'), + 'Servers_hide_db_desc' => __('Hide databases matching regular expression (PCRE).'), + 'Servers_DisableIS_desc' => __( + 'More information on [a@https://github.com/phpmyadmin/phpmyadmin/issues/8970]phpMyAdmin ' + . 'issue tracker[/a] and [a@https://bugs.mysql.com/19588]MySQL Bugs[/a]' + ), + 'Servers_DisableIS_name' => __('Disable use of INFORMATION_SCHEMA'), + 'Servers_hide_db_name' => __('Hide databases'), + 'Servers_history_desc' => __( + 'Leave blank for no SQL query history support, suggested: ' + . '[kbd]pma__history[/kbd].' + ), + 'Servers_history_name' => __('SQL query history table'), + 'Servers_host_desc' => __('Hostname where MySQL server is running.'), + 'Servers_host_name' => __('Server hostname'), + 'Servers_LogoutURL_name' => __('Logout URL'), + 'Servers_MaxTableUiprefs_desc' => __( + 'Limits number of table preferences which are stored in database, the oldest ' + . 'records are automatically removed.' + ), + 'Servers_MaxTableUiprefs_name' => __('Maximal number of table preferences to store'), + 'Servers_savedsearches_name' => __('QBE saved searches table'), + 'Servers_savedsearches_desc' => __( + 'Leave blank for no QBE saved searches support, suggested: ' + . '[kbd]pma__savedsearches[/kbd].' + ), + 'Servers_export_templates_name' => __('Export templates table'), + 'Servers_export_templates_desc' => __( + 'Leave blank for no export template support, suggested: ' + . '[kbd]pma__export_templates[/kbd].' + ), + 'Servers_central_columns_name' => __('Central columns table'), + 'Servers_central_columns_desc' => __( + 'Leave blank for no central columns support, suggested: ' + . '[kbd]pma__central_columns[/kbd].' + ), + 'Servers_only_db_desc' => __( + 'You can use MySQL wildcard characters (% and _), escape them if you want to ' + . 'use their literal instances, i.e. use [kbd]\'my\_db\'[/kbd] and not ' + . '[kbd]\'my_db\'[/kbd].' + ), + 'Servers_only_db_name' => __('Show only listed databases'), + 'Servers_password_desc' => __('Leave empty if not using config auth.'), + 'Servers_password_name' => __('Password for config auth'), + 'Servers_pdf_pages_desc' => __('Leave blank for no PDF schema support, suggested: [kbd]pma__pdf_pages[/kbd].'), + 'Servers_pdf_pages_name' => __('PDF schema: pages table'), + 'Servers_pmadb_desc' => __( + 'Database used for relations, bookmarks, and PDF features. See ' + . '[doc@linked-tables]pmadb[/doc] for complete information. ' + . 'Leave blank for no support. Suggested: [kbd]phpmyadmin[/kbd].' + ), + 'Servers_pmadb_name' => __('Database name'), + 'Servers_port_desc' => __('Port on which MySQL server is listening, leave empty for default.'), + 'Servers_port_name' => __('Server port'), + 'Servers_recent_desc' => __( + 'Leave blank for no "persistent" recently used tables across sessions, ' + . 'suggested: [kbd]pma__recent[/kbd].' + ), + 'Servers_recent_name' => __('Recently used table'), + 'Servers_favorite_desc' => __( + 'Leave blank for no "persistent" favorite tables across sessions, ' + . 'suggested: [kbd]pma__favorite[/kbd].' + ), + 'Servers_favorite_name' => __('Favorites table'), + 'Servers_relation_desc' => __( + 'Leave blank for no ' + . '[doc@relations@]relation-links[/doc] support, ' + . 'suggested: [kbd]pma__relation[/kbd].' + ), + 'Servers_relation_name' => __('Relation table'), + 'Servers_SignonSession_desc' => __( + 'See [doc@authentication-modes]authentication ' + . 'types[/doc] for an example.' + ), + 'Servers_SignonSession_name' => __('Signon session name'), + 'Servers_SignonURL_name' => __('Signon URL'), + 'Servers_socket_desc' => __('Socket on which MySQL server is listening, leave empty for default.'), + 'Servers_socket_name' => __('Server socket'), + 'Servers_ssl_desc' => __('Enable SSL for connection to MySQL server.'), + 'Servers_ssl_name' => __('Use SSL'), + 'Servers_table_coords_desc' => __('Leave blank for no PDF schema support, suggested: [kbd]pma__table_coords[/kbd].'), + 'Servers_table_coords_name' => __('Designer and PDF schema: table coordinates'), + 'Servers_table_info_desc' => __( + 'Table to describe the display columns, leave blank for no support; ' + . 'suggested: [kbd]pma__table_info[/kbd].' + ), + 'Servers_table_info_name' => __('Display columns table'), + 'Servers_table_uiprefs_desc' => __( + 'Leave blank for no "persistent" tables\' UI preferences across sessions, ' + . 'suggested: [kbd]pma__table_uiprefs[/kbd].' + ), + 'Servers_table_uiprefs_name' => __('UI preferences table'), + 'Servers_tracking_add_drop_database_desc' => __( + 'Whether a DROP DATABASE IF EXISTS statement will be added as first line to ' + . 'the log when creating a database.' + ), + 'Servers_tracking_add_drop_database_name' => __('Add DROP DATABASE'), + 'Servers_tracking_add_drop_table_desc' => __( + 'Whether a DROP TABLE IF EXISTS statement will be added as first line to the ' + . 'log when creating a table.' + ), + 'Servers_tracking_add_drop_table_name' => __('Add DROP TABLE'), + 'Servers_tracking_add_drop_view_desc' => __( + 'Whether a DROP VIEW IF EXISTS statement will be added as first line to the ' + . 'log when creating a view.' + ), + 'Servers_tracking_add_drop_view_name' => __('Add DROP VIEW'), + 'Servers_tracking_default_statements_desc' => __('Defines the list of statements the auto-creation uses for new versions.'), + 'Servers_tracking_default_statements_name' => __('Statements to track'), + 'Servers_tracking_desc' => __( + 'Leave blank for no SQL query tracking support, suggested: ' + . '[kbd]pma__tracking[/kbd].' + ), + 'Servers_tracking_name' => __('SQL query tracking table'), + 'Servers_tracking_version_auto_create_desc' => __( + 'Whether the tracking mechanism creates versions for tables and views ' + . 'automatically.' + ), + 'Servers_tracking_version_auto_create_name' => __('Automatically create versions'), + 'Servers_userconfig_desc' => __( + 'Leave blank for no user preferences storage in database, suggested: ' + . '[kbd]pma__userconfig[/kbd].' + ), + 'Servers_userconfig_name' => __('User preferences storage table'), + 'Servers_users_desc' => __( + 'Both this table and the user groups table are required to enable the ' . + 'configurable menus feature; leaving either one of them blank will disable ' . + 'this feature, suggested: [kbd]pma__users[/kbd].' + ), + 'Servers_users_name' => __('Users table'), + 'Servers_usergroups_desc' => __( + 'Both this table and the users table are required to enable the configurable ' . + 'menus feature; leaving either one of them blank will disable this feature, ' . + 'suggested: [kbd]pma__usergroups[/kbd].' + ), + 'Servers_usergroups_name' => __('User groups table'), + 'Servers_navigationhiding_desc' => __( + 'Leave blank to disable the feature to hide and show navigation items, ' . + 'suggested: [kbd]pma__navigationhiding[/kbd].' + ), + 'Servers_navigationhiding_name' => __('Hidden navigation items table'), + 'Servers_user_desc' => __('Leave empty if not using config auth.'), + 'Servers_user_name' => __('User for config auth'), + 'Servers_verbose_desc' => __( + 'A user-friendly description of this server. Leave blank to display the ' . + 'hostname instead.' + ), + 'Servers_verbose_name' => __('Verbose name of this server'), + 'ShowAll_desc' => __('Whether a user should be displayed a "show all (rows)" button.'), + 'ShowAll_name' => __('Allow to display all the rows'), + 'ShowChgPassword_desc' => __( + 'Please note that enabling this has no effect with [kbd]config[/kbd] ' . + 'authentication mode because the password is hard coded in the configuration ' . + 'file; this does not limit the ability to execute the same command directly.' + ), + 'ShowChgPassword_name' => __('Show password change form'), + 'ShowCreateDb_name' => __('Show create database form'), + 'ShowDbStructureComment_desc' => __('Show or hide a column displaying the comments for all tables.'), + 'ShowDbStructureComment_name' => __('Show table comments'), + 'ShowDbStructureCreation_desc' => __('Show or hide a column displaying the Creation timestamp for all tables.'), + 'ShowDbStructureCreation_name' => __('Show creation timestamp'), + 'ShowDbStructureLastUpdate_desc' => __('Show or hide a column displaying the Last update timestamp for all tables.'), + 'ShowDbStructureLastUpdate_name' => __('Show last update timestamp'), + 'ShowDbStructureLastCheck_desc' => __('Show or hide a column displaying the Last check timestamp for all tables.'), + 'ShowDbStructureLastCheck_name' => __('Show last check timestamp'), + 'ShowDbStructureCharset_desc' => __('Show or hide a column displaying the charset for all tables.'), + 'ShowDbStructureCharset_name' => __('Show table charset'), + 'ShowFieldTypesInDataEditView_desc' => __( + 'Defines whether or not type fields should be initially displayed in ' . + 'edit/insert mode.' + ), + 'ShowFieldTypesInDataEditView_name' => __('Show field types'), + 'ShowFunctionFields_desc' => __('Display the function fields in edit/insert mode.'), + 'ShowFunctionFields_name' => __('Show function fields'), + 'ShowHint_desc' => __('Whether to show hint or not.'), + 'ShowHint_name' => __('Show hint'), + 'ShowPhpInfo_desc' => __( + 'Shows link to [a@https://php.net/manual/function.phpinfo.php]phpinfo()[/a] ' . + 'output.' + ), + 'ShowPhpInfo_name' => __('Show phpinfo() link'), + 'ShowServerInfo_name' => __('Show detailed MySQL server information'), + 'ShowSQL_desc' => __('Defines whether SQL queries generated by phpMyAdmin should be displayed.'), + 'ShowSQL_name' => __('Show SQL queries'), + 'RetainQueryBox_desc' => __('Defines whether the query box should stay on-screen after its submission.'), + 'RetainQueryBox_name' => __('Retain query box'), + 'ShowStats_desc' => __('Allow to display database and table statistics (eg. space usage).'), + 'ShowStats_name' => __('Show statistics'), + 'SkipLockedTables_desc' => __('Mark used tables and make it possible to show databases with locked tables.'), + 'SkipLockedTables_name' => __('Skip locked tables'), + 'SQLQuery_Edit_name' => __('Edit'), + 'SQLQuery_Explain_name' => __('Explain SQL'), + 'SQLQuery_Refresh_name' => __('Refresh'), + 'SQLQuery_ShowAsPHP_name' => __('Create PHP code'), + 'SuhosinDisableWarning_desc' => __( + 'Disable the default warning that is displayed on the main page if Suhosin is ' . + 'detected.' + ), + 'SuhosinDisableWarning_name' => __('Suhosin warning'), + 'LoginCookieValidityDisableWarning_desc' => __( + 'Disable the default warning that is displayed on the main page if the value ' . + 'of the PHP setting session.gc_maxlifetime is less than the value of ' . + '`LoginCookieValidity`.' + ), + 'LoginCookieValidityDisableWarning_name' => __('Login cookie validity warning'), + 'TextareaCols_desc' => __( + 'Textarea size (columns) in edit mode, this value will be emphasized for SQL ' . + 'query textareas (*2).' + ), + 'TextareaCols_name' => __('Textarea columns'), + 'TextareaRows_desc' => __( + 'Textarea size (rows) in edit mode, this value will be emphasized for SQL ' . + 'query textareas (*2).' + ), + 'TextareaRows_name' => __('Textarea rows'), + 'TitleDatabase_desc' => __('Title of browser window when a database is selected.'), + 'TitleDatabase_name' => __('Database'), + 'TitleDefault_desc' => __('Title of browser window when nothing is selected.'), + 'TitleDefault_name' => __('Default title'), + 'TitleServer_desc' => __('Title of browser window when a server is selected.'), + 'TitleServer_name' => __('Server'), + 'TitleTable_desc' => __('Title of browser window when a table is selected.'), + 'TitleTable_name' => __('Table'), + 'TrustedProxies_desc' => __( + 'Input proxies as [kbd]IP: trusted HTTP header[/kbd]. The following example ' . + 'specifies that phpMyAdmin should trust a HTTP_X_FORWARDED_FOR ' . + '(X-Forwarded-For) header coming from the proxy 1.2.3.4:[br][kbd]1.2.3.4: ' . + 'HTTP_X_FORWARDED_FOR[/kbd].' + ), + 'TrustedProxies_name' => __('List of trusted proxies for IP allow/deny'), + 'UploadDir_desc' => __('Directory on server where you can upload files for import.'), + 'UploadDir_name' => __('Upload directory'), + 'UseDbSearch_desc' => __('Allow for searching inside the entire database.'), + 'UseDbSearch_name' => __('Use database search'), + 'UserprefsDeveloperTab_desc' => __( + 'When disabled, users cannot set any of the options below, regardless of the ' . + 'checkbox on the right.' + ), + 'UserprefsDeveloperTab_name' => __('Enable the Developer tab in settings'), + 'VersionCheck_desc' => __('Enables check for latest version on main phpMyAdmin page.'), + 'VersionCheck_name' => __('Version check'), + 'ProxyUrl_desc' => __( + 'The url of the proxy to be used when retrieving the information about the ' . + 'latest version of phpMyAdmin or when submitting error reports. You need this ' . + 'if the server where phpMyAdmin is installed does not have direct access to ' . + 'the internet. The format is: "hostname:portnumber".' + ), + 'ProxyUrl_name' => __('Proxy url'), + 'ProxyUser_desc' => __( + 'The username for authenticating with the proxy. By default, no ' . + 'authentication is performed. If a username is supplied, Basic ' . + 'Authentication will be performed. No other types of authentication are ' . + 'currently supported.' + ), + 'ProxyUser_name' => __('Proxy username'), + 'ProxyPass_desc' => __('The password for authenticating with the proxy.'), + 'ProxyPass_name' => __('Proxy password'), + + 'ZipDump_desc' => __('Enable ZIP compression for import and export operations.'), + 'ZipDump_name' => __('ZIP'), + 'CaptchaLoginPublicKey_desc' => __('Enter your public key for your domain reCaptcha service.'), + 'CaptchaLoginPublicKey_name' => __('Public key for reCaptcha'), + 'CaptchaLoginPrivateKey_desc' => __('Enter your private key for your domain reCaptcha service.'), + 'CaptchaLoginPrivateKey_name' => __('Private key for reCaptcha'), + + 'SendErrorReports_desc' => __('Choose the default action when sending error reports.'), + 'SendErrorReports_name' => __('Send error reports'), + + 'ConsoleEnterExecutes_desc' => __( + 'Queries are executed by pressing Enter (instead of Ctrl+Enter). New lines ' . + 'will be inserted with Shift+Enter.' + ), + 'ConsoleEnterExecutes_name' => __('Enter executes queries in console'), + + 'ZeroConf_desc' => __( + 'Enable Zero Configuration mode which lets you setup phpMyAdmin ' + . 'configuration storage tables automatically.' + ), + 'ZeroConf_name' => __('Enable Zero Configuration mode'), + 'Console_StartHistory_name' => __('Show query history at start'), + 'Console_AlwaysExpand_name' => __('Always expand query messages'), + 'Console_CurrentQuery_name' => __('Show current browsing query'), + 'Console_EnterExecutes_name' => __('Execute queries on Enter and insert new line with Shift + Enter'), + 'Console_DarkTheme_name' => __('Switch to dark theme'), + 'Console_Height_name' => __('Console height'), + 'Console_Mode_name' => __('Console mode'), + 'Console_GroupQueries_name' => __('Group queries'), + 'Console_Order_name' => __('Order'), + 'Console_OrderBy_name' => __('Order by'), + 'DefaultConnectionCollation_name' => __('Server connection collation'), + ]; + + $key = $path . '_' . $type; + + return $descriptions[$key] ?? null; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Form.php b/srcs/phpmyadmin/libraries/classes/Config/Form.php new file mode 100644 index 0000000..8c1cbc5 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Form.php @@ -0,0 +1,238 @@ +index = $index; + $this->_configFile = $cf; + $this->loadForm($formName, $form); + } + + /** + * Returns type of given option + * + * @param string $optionName path or field name + * + * @return string|null one of: boolean, integer, double, string, select, array + */ + public function getOptionType($optionName) + { + $key = ltrim( + mb_substr( + $optionName, + (int) mb_strrpos($optionName, '/') + ), + '/' + ); + return isset($this->_fieldsTypes[$key]) + ? $this->_fieldsTypes[$key] + : null; + } + + /** + * Returns allowed values for select fields + * + * @param string $optionPath Option path + * + * @return array + */ + public function getOptionValueList($optionPath) + { + $value = $this->_configFile->getDbEntry($optionPath); + if ($value === null) { + trigger_error("$optionPath - select options not defined", E_USER_ERROR); + return []; + } + if (! is_array($value)) { + trigger_error("$optionPath - not a static value list", E_USER_ERROR); + return []; + } + // convert array('#', 'a', 'b') to array('a', 'b') + if (isset($value[0]) && $value[0] === '#') { + // remove first element ('#') + array_shift($value); + // $value has keys and value names, return it + return $value; + } + + // convert value list array('a', 'b') to array('a' => 'a', 'b' => 'b') + $hasStringKeys = false; + $keys = []; + for ($i = 0, $nb = count($value); $i < $nb; $i++) { + if (! isset($value[$i])) { + $hasStringKeys = true; + break; + } + $keys[] = is_bool($value[$i]) ? (int) $value[$i] : $value[$i]; + } + if (! $hasStringKeys) { + $value = array_combine($keys, $value); + } + + // $value has keys and value names, return it + return $value; + } + + /** + * array_walk callback function, reads path of form fields from + * array (see docs for \PhpMyAdmin\Config\Forms\BaseForm::getForms) + * + * @param mixed $value Value + * @param mixed $key Key + * @param mixed $prefix Prefix + * + * @return void + */ + private function _readFormPathsCallback($value, $key, $prefix) + { + static $groupCounter = 0; + + if (is_array($value)) { + $prefix .= $key . '/'; + array_walk($value, [$this, '_readFormPathsCallback'], $prefix); + return; + } + + if (! is_int($key)) { + $this->default[$prefix . $key] = $value; + $value = $key; + } + // add unique id to group ends + if ($value == ':group:end') { + $value .= ':' . $groupCounter++; + } + $this->fields[] = $prefix . $value; + } + + /** + * Reads form paths to {@link $fields} + * + * @param array $form Form + * + * @return void + */ + protected function readFormPaths(array $form) + { + // flatten form fields' paths and save them to $fields + $this->fields = []; + array_walk($form, [$this, '_readFormPathsCallback'], ''); + + // $this->fields is an array of the form: [0..n] => 'field path' + // change numeric indexes to contain field names (last part of the path) + $paths = $this->fields; + $this->fields = []; + foreach ($paths as $path) { + $key = ltrim( + mb_substr($path, (int) mb_strrpos($path, '/')), + '/' + ); + $this->fields[$key] = $path; + } + // now $this->fields is an array of the form: 'field name' => 'field path' + } + + /** + * Reads fields' types to $this->_fieldsTypes + * + * @return void + */ + protected function readTypes() + { + $cf = $this->_configFile; + foreach ($this->fields as $name => $path) { + if (mb_strpos((string) $name, ':group:') === 0) { + $this->_fieldsTypes[$name] = 'group'; + continue; + } + $v = $cf->getDbEntry($path); + if ($v !== null) { + $type = is_array($v) ? 'select' : $v; + } else { + $type = gettype($cf->getDefault($path)); + } + $this->_fieldsTypes[$name] = $type; + } + } + + /** + * Reads form settings and prepares class to work with given subset of + * config file + * + * @param string $formName Form name + * @param array $form Form + * + * @return void + */ + public function loadForm($formName, array $form) + { + $this->name = $formName; + $this->readFormPaths($form); + $this->readTypes(); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/FormDisplay.php b/srcs/phpmyadmin/libraries/classes/Config/FormDisplay.php new file mode 100644 index 0000000..500706c --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/FormDisplay.php @@ -0,0 +1,924 @@ +formDisplayTemplate = new FormDisplayTemplate($GLOBALS['PMA_Config']); + $this->_jsLangStrings = [ + 'error_nan_p' => __('Not a positive number!'), + 'error_nan_nneg' => __('Not a non-negative number!'), + 'error_incorrect_port' => __('Not a valid port number!'), + 'error_invalid_value' => __('Incorrect value!'), + 'error_value_lte' => __('Value must be less than or equal to %s!'), + ]; + $this->_configFile = $cf; + // initialize validators + Validator::getValidators($this->_configFile); + } + + /** + * Returns {@link ConfigFile} associated with this instance + * + * @return ConfigFile + */ + public function getConfigFile() + { + return $this->_configFile; + } + + /** + * Registers form in form manager + * + * @param string $formName Form name + * @param array $form Form data + * @param int $serverId 0 if new server, validation; >= 1 if editing a server + * + * @return void + */ + public function registerForm($formName, array $form, $serverId = null) + { + $this->_forms[$formName] = new Form( + $formName, + $form, + $this->_configFile, + $serverId + ); + $this->_isValidated = false; + foreach ($this->_forms[$formName]->fields as $path) { + $workPath = $serverId === null + ? $path + : str_replace('Servers/1/', "Servers/$serverId/", $path); + $this->_systemPaths[$workPath] = $path; + $this->_translatedPaths[$workPath] = str_replace('/', '-', $workPath); + } + } + + /** + * Processes forms, returns true on successful save + * + * @param bool $allowPartialSave allows for partial form saving + * on failed validation + * @param bool $checkFormSubmit whether check for $_POST['submit_save'] + * + * @return boolean whether processing was successful + */ + public function process($allowPartialSave = true, $checkFormSubmit = true) + { + if ($checkFormSubmit && ! isset($_POST['submit_save'])) { + return false; + } + + // save forms + if (count($this->_forms) > 0) { + return $this->save(array_keys($this->_forms), $allowPartialSave); + } + return false; + } + + /** + * Runs validation for all registered forms + * + * @return void + */ + private function _validate() + { + if ($this->_isValidated) { + return; + } + + $paths = []; + $values = []; + foreach ($this->_forms as $form) { + /** @var Form $form */ + $paths[] = $form->name; + // collect values and paths + foreach ($form->fields as $path) { + $workPath = array_search($path, $this->_systemPaths); + $values[$path] = $this->_configFile->getValue($workPath); + $paths[] = $path; + } + } + + // run validation + $errors = Validator::validate( + $this->_configFile, + $paths, + $values, + false + ); + + // change error keys from canonical paths to work paths + if (is_array($errors) && count($errors) > 0) { + $this->_errors = []; + foreach ($errors as $path => $errorList) { + $workPath = array_search($path, $this->_systemPaths); + // field error + if (! $workPath) { + // form error, fix path + $workPath = $path; + } + $this->_errors[$workPath] = $errorList; + } + } + $this->_isValidated = true; + } + + /** + * Outputs HTML for the forms under the menu tab + * + * @param bool $showRestoreDefault whether to show "restore default" + * button besides the input field + * @param array $jsDefault stores JavaScript code + * to be displayed + * @param array $js will be updated with javascript code + * @param bool $showButtons whether show submit and reset button + * + * @return string + */ + private function _displayForms( + $showRestoreDefault, + array &$jsDefault, + array &$js, + $showButtons + ) { + $htmlOutput = ''; + $validators = Validator::getValidators($this->_configFile); + + foreach ($this->_forms as $form) { + /** @var Form $form */ + $formErrors = isset($this->_errors[$form->name]) + ? $this->_errors[$form->name] : null; + $htmlOutput .= $this->formDisplayTemplate->displayFieldsetTop( + Descriptions::get("Form_{$form->name}"), + Descriptions::get("Form_{$form->name}", 'desc'), + $formErrors, + ['id' => $form->name] + ); + + foreach ($form->fields as $field => $path) { + $workPath = array_search($path, $this->_systemPaths); + $translatedPath = $this->_translatedPaths[$workPath]; + // always true/false for user preferences display + // otherwise null + $userPrefsAllow = isset($this->_userprefsKeys[$path]) + ? ! isset($this->_userprefsDisallow[$path]) + : null; + // display input + $htmlOutput .= $this->_displayFieldInput( + $form, + $field, + $path, + $workPath, + $translatedPath, + $showRestoreDefault, + $userPrefsAllow, + $jsDefault + ); + // register JS validators for this field + if (isset($validators[$path])) { + $this->formDisplayTemplate->addJsValidate($translatedPath, $validators[$path], $js); + } + } + $htmlOutput .= $this->formDisplayTemplate->displayFieldsetBottom($showButtons); + } + return $htmlOutput; + } + + /** + * Outputs HTML for forms + * + * @param bool $tabbedForm if true, use a form with tabs + * @param bool $showRestoreDefault whether show "restore default" button + * besides the input field + * @param bool $showButtons whether show submit and reset button + * @param string $formAction action attribute for the form + * @param array|null $hiddenFields array of form hidden fields (key: field + * name) + * + * @return string HTML for forms + */ + public function getDisplay( + $tabbedForm = false, + $showRestoreDefault = false, + $showButtons = true, + $formAction = null, + $hiddenFields = null + ) { + static $jsLangSent = false; + + $htmlOutput = ''; + + $js = []; + $jsDefault = []; + + $htmlOutput .= $this->formDisplayTemplate->displayFormTop($formAction, 'post', $hiddenFields); + + if ($tabbedForm) { + $tabs = []; + foreach ($this->_forms as $form) { + $tabs[$form->name] = Descriptions::get("Form_$form->name"); + } + $htmlOutput .= $this->formDisplayTemplate->displayTabsTop($tabs); + } + + // validate only when we aren't displaying a "new server" form + $isNewServer = false; + foreach ($this->_forms as $form) { + /** @var Form $form */ + if ($form->index === 0) { + $isNewServer = true; + break; + } + } + if (! $isNewServer) { + $this->_validate(); + } + + // user preferences + $this->_loadUserprefsInfo(); + + // display forms + $htmlOutput .= $this->_displayForms( + $showRestoreDefault, + $jsDefault, + $js, + $showButtons + ); + + if ($tabbedForm) { + $htmlOutput .= $this->formDisplayTemplate->displayTabsBottom(); + } + $htmlOutput .= $this->formDisplayTemplate->displayFormBottom(); + + // if not already done, send strings used for validation to JavaScript + if (! $jsLangSent) { + $jsLangSent = true; + $jsLang = []; + foreach ($this->_jsLangStrings as $strName => $strValue) { + $jsLang[] = "'$strName': '" . Sanitize::jsFormat($strValue, false) . '\''; + } + $js[] = "$.extend(Messages, {\n\t" + . implode(",\n\t", $jsLang) . '})'; + } + + $js[] = "$.extend(defaultValues, {\n\t" + . implode(",\n\t", $jsDefault) . '})'; + $htmlOutput .= $this->formDisplayTemplate->displayJavascript($js); + + return $htmlOutput; + } + + /** + * Prepares data for input field display and outputs HTML code + * + * @param Form $form Form object + * @param string $field field name as it appears in $form + * @param string $systemPath field path, eg. Servers/1/verbose + * @param string $workPath work path, eg. Servers/4/verbose + * @param string $translatedPath work path changed so that it can be + * used as XHTML id + * @param bool $showRestoreDefault whether show "restore default" button + * besides the input field + * @param bool|null $userPrefsAllow whether user preferences are enabled + * for this field (null - no support, + * true/false - enabled/disabled) + * @param array $jsDefault array which stores JavaScript code + * to be displayed + * + * @return string|null HTML for input field + */ + private function _displayFieldInput( + Form $form, + $field, + $systemPath, + $workPath, + $translatedPath, + $showRestoreDefault, + $userPrefsAllow, + array &$jsDefault + ) { + $name = Descriptions::get($systemPath); + $description = Descriptions::get($systemPath, 'desc'); + + $value = $this->_configFile->get($workPath); + $valueDefault = $this->_configFile->getDefault($systemPath); + $valueIsDefault = false; + if ($value === null || $value === $valueDefault) { + $value = $valueDefault; + $valueIsDefault = true; + } + + $opts = [ + 'doc' => $this->getDocLink($systemPath), + 'show_restore_default' => $showRestoreDefault, + 'userprefs_allow' => $userPrefsAllow, + 'userprefs_comment' => Descriptions::get($systemPath, 'cmt'), + ]; + if (isset($form->default[$systemPath])) { + $opts['setvalue'] = (string) $form->default[$systemPath]; + } + + if (isset($this->_errors[$workPath])) { + $opts['errors'] = $this->_errors[$workPath]; + } + + $type = ''; + switch ($form->getOptionType($field)) { + case 'string': + $type = 'text'; + break; + case 'short_string': + $type = 'short_text'; + break; + case 'double': + case 'integer': + $type = 'number_text'; + break; + case 'boolean': + $type = 'checkbox'; + break; + case 'select': + $type = 'select'; + $opts['values'] = $form->getOptionValueList($form->fields[$field]); + break; + case 'array': + $type = 'list'; + $value = (array) $value; + $valueDefault = (array) $valueDefault; + break; + case 'group': + // :group:end is changed to :group:end:{unique id} in Form class + $htmlOutput = ''; + if (mb_substr($field, 7, 4) != 'end:') { + $htmlOutput .= $this->formDisplayTemplate->displayGroupHeader( + mb_substr($field, 7) + ); + } else { + $this->formDisplayTemplate->displayGroupFooter(); + } + return $htmlOutput; + case 'NULL': + trigger_error("Field $systemPath has no type", E_USER_WARNING); + return null; + } + + // detect password fields + if ($type === 'text' + && (mb_substr($translatedPath, -9) === '-password' + || mb_substr($translatedPath, -4) === 'pass' + || mb_substr($translatedPath, -4) === 'Pass') + ) { + $type = 'password'; + } + + // TrustedProxies requires changes before displaying + if ($systemPath == 'TrustedProxies') { + foreach ($value as $ip => &$v) { + if (! preg_match('/^-\d+$/', $ip)) { + $v = $ip . ': ' . $v; + } + } + } + $this->_setComments($systemPath, $opts); + + // send default value to form's JS + $jsLine = '\'' . $translatedPath . '\': '; + switch ($type) { + case 'text': + case 'short_text': + case 'number_text': + case 'password': + $jsLine .= '\'' . Sanitize::escapeJsString($valueDefault) . '\''; + break; + case 'checkbox': + $jsLine .= $valueDefault ? 'true' : 'false'; + break; + case 'select': + $valueDefaultJs = is_bool($valueDefault) + ? (int) $valueDefault + : $valueDefault; + $jsLine .= '[\'' . Sanitize::escapeJsString($valueDefaultJs) . '\']'; + break; + case 'list': + $jsLine .= '\'' . Sanitize::escapeJsString(implode("\n", $valueDefault)) + . '\''; + break; + } + $jsDefault[] = $jsLine; + + return $this->formDisplayTemplate->displayInput( + $translatedPath, + $name, + $type, + $value, + $description, + $valueIsDefault, + $opts + ); + } + + /** + * Displays errors + * + * @return string|null HTML for errors + */ + public function displayErrors() + { + $this->_validate(); + if (count($this->_errors) === 0) { + return null; + } + + $htmlOutput = ''; + + foreach ($this->_errors as $systemPath => $errorList) { + if (isset($this->_systemPaths[$systemPath])) { + $name = Descriptions::get($this->_systemPaths[$systemPath]); + } else { + $name = Descriptions::get('Form_' . $systemPath); + } + $htmlOutput .= $this->formDisplayTemplate->displayErrors($name, $errorList); + } + + return $htmlOutput; + } + + /** + * Reverts erroneous fields to their default values + * + * @return void + */ + public function fixErrors() + { + $this->_validate(); + if (count($this->_errors) === 0) { + return; + } + + $cf = $this->_configFile; + foreach (array_keys($this->_errors) as $workPath) { + if (! isset($this->_systemPaths[$workPath])) { + continue; + } + $canonicalPath = $this->_systemPaths[$workPath]; + $cf->set($workPath, $cf->getDefault($canonicalPath)); + } + } + + /** + * Validates select field and casts $value to correct type + * + * @param string $value Current value + * @param array $allowed List of allowed values + * + * @return bool + */ + private function _validateSelect(&$value, array $allowed) + { + $valueCmp = is_bool($value) + ? (int) $value + : $value; + foreach ($allowed as $vk => $v) { + // equality comparison only if both values are numeric or not numeric + // (allows to skip 0 == 'string' equalling to true) + // or identity (for string-string) + if (($vk == $value && ! (is_numeric($valueCmp) xor is_numeric($vk))) + || $vk === $value + ) { + // keep boolean value as boolean + if (! is_bool($value)) { + settype($value, gettype($vk)); + } + return true; + } + } + return false; + } + + /** + * Validates and saves form data to session + * + * @param array|string $forms array of form names + * @param bool $allowPartialSave allows for partial form saving on + * failed validation + * + * @return boolean true on success (no errors and all saved) + */ + public function save($forms, $allowPartialSave = true) + { + $result = true; + $forms = (array) $forms; + + $values = []; + $toSave = []; + $isSetupScript = $GLOBALS['PMA_Config']->get('is_setup'); + if ($isSetupScript) { + $this->_loadUserprefsInfo(); + } + + $this->_errors = []; + foreach ($forms as $formName) { + /** @var Form $form */ + if (isset($this->_forms[$formName])) { + $form = $this->_forms[$formName]; + } else { + continue; + } + // get current server id + $changeIndex = $form->index === 0 + ? $this->_configFile->getServerCount() + 1 + : false; + // grab POST values + foreach ($form->fields as $field => $systemPath) { + $workPath = array_search($systemPath, $this->_systemPaths); + $key = $this->_translatedPaths[$workPath]; + $type = $form->getOptionType($field); + + // skip groups + if ($type == 'group') { + continue; + } + + // ensure the value is set + if (! isset($_POST[$key])) { + // checkboxes aren't set by browsers if they're off + if ($type == 'boolean') { + $_POST[$key] = false; + } else { + $this->_errors[$form->name][] = sprintf( + __('Missing data for %s'), + '' . Descriptions::get($systemPath) . '' + ); + $result = false; + continue; + } + } + + // user preferences allow/disallow + if ($isSetupScript + && isset($this->_userprefsKeys[$systemPath]) + ) { + if (isset($this->_userprefsDisallow[$systemPath]) + && isset($_POST[$key . '-userprefs-allow']) + ) { + unset($this->_userprefsDisallow[$systemPath]); + } elseif (! isset($_POST[$key . '-userprefs-allow'])) { + $this->_userprefsDisallow[$systemPath] = true; + } + } + + // cast variables to correct type + switch ($type) { + case 'double': + $_POST[$key] = Util::requestString($_POST[$key]); + settype($_POST[$key], 'float'); + break; + case 'boolean': + case 'integer': + if ($_POST[$key] !== '') { + $_POST[$key] = Util::requestString($_POST[$key]); + settype($_POST[$key], $type); + } + break; + case 'select': + $successfullyValidated = $this->_validateSelect( + $_POST[$key], + $form->getOptionValueList($systemPath) + ); + if (! $successfullyValidated) { + $this->_errors[$workPath][] = __('Incorrect value!'); + $result = false; + // "continue" for the $form->fields foreach-loop + continue 2; + } + break; + case 'string': + case 'short_string': + $_POST[$key] = Util::requestString($_POST[$key]); + break; + case 'array': + // eliminate empty values and ensure we have an array + $postValues = is_array($_POST[$key]) + ? $_POST[$key] + : explode("\n", $_POST[$key]); + $_POST[$key] = []; + $this->_fillPostArrayParameters($postValues, $key); + break; + } + + // now we have value with proper type + $values[$systemPath] = $_POST[$key]; + if ($changeIndex !== false) { + $workPath = str_replace( + "Servers/$form->index/", + "Servers/$changeIndex/", + $workPath + ); + } + $toSave[$workPath] = $systemPath; + } + } + + // save forms + if (! $allowPartialSave && ! empty($this->_errors)) { + // don't look for non-critical errors + $this->_validate(); + return $result; + } + + foreach ($toSave as $workPath => $path) { + // TrustedProxies requires changes before saving + if ($path == 'TrustedProxies') { + $proxies = []; + $i = 0; + foreach ($values[$path] as $value) { + $matches = []; + $match = preg_match( + "/^(.+):(?:[ ]?)(\\w+)$/", + $value, + $matches + ); + if ($match) { + // correct 'IP: HTTP header' pair + $ip = trim($matches[1]); + $proxies[$ip] = trim($matches[2]); + } else { + // save also incorrect values + $proxies["-$i"] = $value; + $i++; + } + } + $values[$path] = $proxies; + } + $this->_configFile->set($workPath, $values[$path], $path); + } + if ($isSetupScript) { + $this->_configFile->set( + 'UserprefsDisallow', + array_keys($this->_userprefsDisallow) + ); + } + + // don't look for non-critical errors + $this->_validate(); + + return $result; + } + + /** + * Tells whether form validation failed + * + * @return boolean + */ + public function hasErrors() + { + return count($this->_errors) > 0; + } + + + /** + * Returns link to documentation + * + * @param string $path Path to documentation + * + * @return string + */ + public function getDocLink($path) + { + $test = mb_substr($path, 0, 6); + if ($test == 'Import' || $test == 'Export') { + return ''; + } + return Util::getDocuLink( + 'config', + 'cfg_' . $this->_getOptName($path) + ); + } + + /** + * Changes path so it can be used in URLs + * + * @param string $path Path + * + * @return string + */ + private function _getOptName($path) + { + return str_replace(['Servers/1/', '/'], ['Servers/', '_'], $path); + } + + /** + * Fills out {@link userprefs_keys} and {@link userprefs_disallow} + * + * @return void + */ + private function _loadUserprefsInfo() + { + if ($this->_userprefsKeys !== null) { + return; + } + + $this->_userprefsKeys = array_flip(UserFormList::getFields()); + // read real config for user preferences display + $userPrefsDisallow = $GLOBALS['PMA_Config']->get('is_setup') + ? $this->_configFile->get('UserprefsDisallow', []) + : $GLOBALS['cfg']['UserprefsDisallow']; + $this->_userprefsDisallow = array_flip($userPrefsDisallow); + } + + /** + * Sets field comments and warnings based on current environment + * + * @param string $systemPath Path to settings + * @param array $opts Chosen options + * + * @return void + */ + private function _setComments($systemPath, array &$opts) + { + // RecodingEngine - mark unavailable types + if ($systemPath == 'RecodingEngine') { + $comment = ''; + if (! function_exists('iconv')) { + $opts['values']['iconv'] .= ' (' . __('unavailable') . ')'; + $comment = sprintf( + __('"%s" requires %s extension'), + 'iconv', + 'iconv' + ); + } + if (! function_exists('recode_string')) { + $opts['values']['recode'] .= ' (' . __('unavailable') . ')'; + $comment .= ($comment ? ", " : '') . sprintf( + __('"%s" requires %s extension'), + 'recode', + 'recode' + ); + } + /* mbstring is always there thanks to polyfill */ + $opts['comment'] = $comment; + $opts['comment_warning'] = true; + } + // ZipDump, GZipDump, BZipDump - check function availability + if ($systemPath == 'ZipDump' + || $systemPath == 'GZipDump' + || $systemPath == 'BZipDump' + ) { + $comment = ''; + $funcs = [ + 'ZipDump' => [ + 'zip_open', + 'gzcompress', + ], + 'GZipDump' => [ + 'gzopen', + 'gzencode', + ], + 'BZipDump' => [ + 'bzopen', + 'bzcompress', + ], + ]; + if (! function_exists($funcs[$systemPath][0])) { + $comment = sprintf( + __( + 'Compressed import will not work due to missing function %s.' + ), + $funcs[$systemPath][0] + ); + } + if (! function_exists($funcs[$systemPath][1])) { + $comment .= ($comment ? '; ' : '') . sprintf( + __( + 'Compressed export will not work due to missing function %s.' + ), + $funcs[$systemPath][1] + ); + } + $opts['comment'] = $comment; + $opts['comment_warning'] = true; + } + if (! $GLOBALS['PMA_Config']->get('is_setup')) { + if ($systemPath == 'MaxDbList' || $systemPath == 'MaxTableList' + || $systemPath == 'QueryHistoryMax' + ) { + $opts['comment'] = sprintf( + __('maximum %s'), + $GLOBALS['cfg'][$systemPath] + ); + } + } + } + + /** + * Copy items of an array to $_POST variable + * + * @param array $postValues List of parameters + * @param string $key Array key + * + * @return void + */ + private function _fillPostArrayParameters(array $postValues, $key) + { + foreach ($postValues as $v) { + $v = Util::requestString($v); + if ($v !== '') { + $_POST[$key][] = $v; + } + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/FormDisplayTemplate.php b/srcs/phpmyadmin/libraries/classes/Config/FormDisplayTemplate.php new file mode 100644 index 0000000..07663b8 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/FormDisplayTemplate.php @@ -0,0 +1,526 @@ +config = $config; + $this->template = new Template(); + } + + /** + * Displays top part of the form + * + * @param string $action default: $_SERVER['REQUEST_URI'] + * @param string $method 'post' or 'get' + * @param array|null $hiddenFields array of form hidden fields (key: field name) + * + * @return string + */ + public function displayFormTop( + $action = null, + $method = 'post', + $hiddenFields = null + ): string { + static $hasCheckPageRefresh = false; + + if ($action === null) { + $action = $_SERVER['REQUEST_URI']; + } + if ($method != 'post') { + $method = 'get'; + } + $htmlOutput = '
'; + $htmlOutput .= ''; + // we do validation on page refresh when browser remembers field values, + // add a field with known value which will be used for checks + if (! $hasCheckPageRefresh) { + $hasCheckPageRefresh = true; + $htmlOutput .= '' . "\n"; + } + $htmlOutput .= Url::getHiddenInputs('', '', 0, 'server') . "\n"; + $htmlOutput .= Url::getHiddenFields((array) $hiddenFields, '', true); + return $htmlOutput; + } + + /** + * Displays form tabs which are given by an array indexed by fieldset id + * ({@link self::displayFieldsetTop}), with values being tab titles. + * + * @param array $tabs tab names + * + * @return string + */ + public function displayTabsTop(array $tabs): string + { + $items = []; + foreach ($tabs as $tabId => $tabName) { + $items[] = [ + 'content' => htmlspecialchars($tabName), + 'url' => [ + 'href' => '#' . $tabId, + ], + ]; + } + + $htmlOutput = $this->template->render('list/unordered', [ + 'class' => 'tabs responsivetable', + 'items' => $items, + ]); + $htmlOutput .= '
'; + $htmlOutput .= '
'; + return $htmlOutput; + } + + /** + * Displays top part of a fieldset + * + * @param string $title title of fieldset + * @param string $description description shown on top of fieldset + * @param array|null $errors error messages to display + * @param array $attributes optional extra attributes of fieldset + * + * @return string + */ + public function displayFieldsetTop( + $title = '', + $description = '', + $errors = null, + array $attributes = [] + ): string { + $this->group = 0; + + $attributes = array_merge(['class' => 'optbox'], $attributes); + + return $this->template->render('config/form_display/fieldset_top', [ + 'attributes' => $attributes, + 'title' => $title, + 'description' => $description, + 'errors' => $errors, + ]); + } + + /** + * Displays input field + * + * $opts keys: + * o doc - (string) documentation link + * o errors - error array + * o setvalue - (string) shows button allowing to set predefined value + * o show_restore_default - (boolean) whether show "restore default" button + * o userprefs_allow - whether user preferences are enabled for this field + * (null - no support, true/false - enabled/disabled) + * o userprefs_comment - (string) field comment + * o values - key - value pairs for '; + break; + case 'password': + $htmlOutput .= ''; + break; + case 'short_text': + // As seen in the reporting server (#15042) we sometimes receive + // an array here. No clue about its origin nor content, so let's avoid + // a notice on htmlspecialchars(). + if (! is_array($value)) { + $htmlOutput .= ''; + } + break; + case 'number_text': + $htmlOutput .= ''; + break; + case 'checkbox': + $htmlOutput .= ''; + break; + case 'select': + $htmlOutput .= ''; + break; + case 'list': + $htmlOutput .= ''; + break; + } + if ($isSetupScript + && isset($opts['userprefs_comment']) + && $opts['userprefs_comment'] + ) { + $htmlOutput .= '' + . $icons['tblops'] . ''; + } + if (isset($opts['setvalue']) && $opts['setvalue']) { + $htmlOutput .= '' . $icons['edit'] . ''; + } + if (isset($opts['show_restore_default']) && $opts['show_restore_default']) { + $htmlOutput .= '' . $icons['reload'] . ''; + } + // this must match with displayErrors() in scripts/config.js + if ($hasErrors) { + $htmlOutput .= "\n
"; + foreach ($opts['errors'] as $error) { + $htmlOutput .= '
' . htmlspecialchars($error) . '
'; + } + $htmlOutput .= '
'; + } + $htmlOutput .= ''; + if ($isSetupScript && isset($opts['userprefs_allow'])) { + $htmlOutput .= ''; + $htmlOutput .= 'group++; + if ($headerText === '') { + return ''; + } + $colspan = $this->config->get('is_setup') ? 3 : 2; + + return $this->template->render('config/form_display/group_header', [ + 'group' => $this->group, + 'colspan' => $colspan, + 'header_text' => $headerText, + ]); + } + + /** + * Display group footer + * + * @return void + */ + public function displayGroupFooter(): void + { + $this->group--; + } + + /** + * Displays bottom part of a fieldset + * + * @param bool $showButtons Whether show submit and reset button + * + * @return string + */ + public function displayFieldsetBottom(bool $showButtons = true): string + { + return $this->template->render('config/form_display/fieldset_bottom', [ + 'show_buttons' => $showButtons, + 'is_setup' => $this->config->get('is_setup'), + ]); + } + + /** + * Closes form tabs + * + * @return string + */ + public function displayTabsBottom(): string + { + return $this->template->render('config/form_display/tabs_bottom'); + } + + /** + * Displays bottom part of the form + * + * @return string + */ + public function displayFormBottom(): string + { + return $this->template->render('config/form_display/form_bottom'); + } + + /** + * Appends JS validation code to $js_array + * + * @param string $fieldId ID of field to validate + * @param string|array $validators validators callback + * @param array $jsArray will be updated with javascript code + * + * @return void + */ + public function addJsValidate($fieldId, $validators, array &$jsArray): void + { + foreach ((array) $validators as $validator) { + $validator = (array) $validator; + $vName = array_shift($validator); + $vArgs = []; + foreach ($validator as $arg) { + $vArgs[] = Sanitize::escapeJsString($arg); + } + $vArgs = $vArgs ? ", ['" . implode("', '", $vArgs) . "']" : ''; + $jsArray[] = "registerFieldValidator('$fieldId', '$vName', true$vArgs)"; + } + } + + /** + * Displays JavaScript code + * + * @param array $jsArray lines of javascript code + * + * @return string + */ + public function displayJavascript(array $jsArray): string + { + if (empty($jsArray)) { + return ''; + } + + return $this->template->render('javascript/display', [ + 'js_array' => $jsArray, + ]); + } + + /** + * Displays error list + * + * @param string $name Name of item with errors + * @param array $errorList List of errors to show + * + * @return string HTML for errors + */ + public function displayErrors($name, array $errorList): string + { + return $this->template->render('config/form_display/errors', [ + 'name' => $name, + 'error_list' => $errorList, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Forms/BaseForm.php b/srcs/phpmyadmin/libraries/classes/Config/Forms/BaseForm.php new file mode 100644 index 0000000..2049070 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Forms/BaseForm.php @@ -0,0 +1,89 @@ += 1 if editing a server + */ + public function __construct(ConfigFile $cf, $serverId = null) + { + parent::__construct($cf); + foreach (static::getForms() as $formName => $form) { + $this->registerForm($formName, $form, $serverId); + } + } + + /** + * List of available forms, each form is described as an array of fields to display. + * Fields MUST have their counterparts in the $cfg array. + * + * To define form field, use the notation below: + * $forms['Form group']['Form name'] = array('Option/path'); + * + * You can assign default values set by special button ("set value: ..."), eg.: + * 'Servers/1/pmadb' => 'phpmyadmin' + * + * To group options, use: + * ':group:' . __('group name') // just define a group + * or + * 'option' => ':group' // group starting from this option + * End group blocks with: + * ':group:end' + * + * @todo This should be abstract, but that does not work in PHP 5 + * + * @return array + */ + public static function getForms() + { + return []; + } + + /** + * Returns list of fields used in the form. + * + * @return string[] + */ + public static function getFields() + { + $names = []; + foreach (static::getForms() as $form) { + foreach ($form as $k => $v) { + $names[] = is_int($k) ? $v : $k; + } + } + return $names; + } + + /** + * Returns name of the form + * + * @todo This should be abstract, but that does not work in PHP 5 + * + * @return string + */ + public static function getName() + { + return ''; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Forms/BaseFormList.php b/srcs/phpmyadmin/libraries/classes/Config/Forms/BaseFormList.php new file mode 100644 index 0000000..f4a5d32 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Forms/BaseFormList.php @@ -0,0 +1,150 @@ +_forms = []; + foreach (static::$all as $form) { + $class = static::get($form); + $this->_forms[] = new $class($cf); + } + } + + /** + * Processes forms, returns true on successful save + * + * @param bool $allowPartialSave allows for partial form saving + * on failed validation + * @param bool $checkFormSubmit whether check for $_POST['submit_save'] + * + * @return boolean whether processing was successful + */ + public function process($allowPartialSave = true, $checkFormSubmit = true) + { + $ret = true; + foreach ($this->_forms as $form) { + $ret = $ret && $form->process($allowPartialSave, $checkFormSubmit); + } + return $ret; + } + + /** + * Displays errors + * + * @return string HTML for errors + */ + public function displayErrors() + { + $ret = ''; + foreach ($this->_forms as $form) { + $ret .= $form->displayErrors(); + } + return $ret; + } + + /** + * Reverts erroneous fields to their default values + * + * @return void + */ + public function fixErrors() + { + foreach ($this->_forms as $form) { + $form->fixErrors(); + } + } + + /** + * Tells whether form validation failed + * + * @return boolean + */ + public function hasErrors() + { + $ret = false; + foreach ($this->_forms as $form) { + $ret = $ret || $form->hasErrors(); + } + return $ret; + } + + /** + * Returns list of fields used in the form. + * + * @return string[] + */ + public static function getFields() + { + $names = []; + foreach (static::$all as $form) { + $class = static::get($form); + $names = array_merge($names, $class::getFields()); + } + return $names; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Forms/Page/BrowseForm.php b/srcs/phpmyadmin/libraries/classes/Config/Forms/Page/BrowseForm.php new file mode 100644 index 0000000..eee578a --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Forms/Page/BrowseForm.php @@ -0,0 +1,30 @@ + MainForm::getForms()['Browse'], + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Forms/Page/DbStructureForm.php b/srcs/phpmyadmin/libraries/classes/Config/Forms/Page/DbStructureForm.php new file mode 100644 index 0000000..4f9a8e4 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Forms/Page/DbStructureForm.php @@ -0,0 +1,30 @@ + MainForm::getForms()['DbStructure'], + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Forms/Page/EditForm.php b/srcs/phpmyadmin/libraries/classes/Config/Forms/Page/EditForm.php new file mode 100644 index 0000000..ad2fd46 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Forms/Page/EditForm.php @@ -0,0 +1,32 @@ + MainForm::getForms()['Edit'], + 'Text_fields' => FeaturesForm::getForms()['Text_fields'], + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Forms/Page/ExportForm.php b/srcs/phpmyadmin/libraries/classes/Config/Forms/Page/ExportForm.php new file mode 100644 index 0000000..584b2fd --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Forms/Page/ExportForm.php @@ -0,0 +1,18 @@ + MainForm::getForms()['TableStructure'], + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/ConfigForm.php b/srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/ConfigForm.php new file mode 100644 index 0000000..6fd4515 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/ConfigForm.php @@ -0,0 +1,32 @@ + [ + 'DefaultLang', + 'ServerDefault', + ], + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/ExportForm.php b/srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/ExportForm.php new file mode 100644 index 0000000..adf7ce4 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/ExportForm.php @@ -0,0 +1,18 @@ + ':group', + 'IconvExtraParams', + ':group:end', + 'ZipDump', + 'GZipDump', + 'BZipDump', + 'CompressOnFly', + ]; + $result['Security'] = [ + 'blowfish_secret', + 'CheckConfigurationPermissions', + 'TrustedProxies', + 'AllowUserDropDatabase', + 'AllowArbitraryServer', + 'ArbitraryServerRegexp', + 'LoginCookieRecall', + 'LoginCookieStore', + 'LoginCookieDeleteAll', + 'CaptchaLoginPublicKey', + 'CaptchaLoginPrivateKey', + ]; + $result['Developer'] = [ + 'UserprefsDeveloperTab', + 'DBG/sql', + ]; + $result['Other_core_settings'] = [ + 'OBGzip', + 'PersistentConnections', + 'ExecTimeLimit', + 'MemoryLimit', + 'UseDbSearch', + 'ProxyUrl', + 'ProxyUser', + 'ProxyPass', + 'AllowThirdPartyFraming', + 'ZeroConf', + ]; + return $result; + // phpcs:enable + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/ImportForm.php b/srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/ImportForm.php new file mode 100644 index 0000000..06adf35 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/ImportForm.php @@ -0,0 +1,18 @@ + [ + 'Servers' => [ + 1 => [ + 'verbose', + 'host', + 'port', + 'socket', + 'ssl', + 'compress', + ], + ], + ], + 'Server_auth' => [ + 'Servers' => [ + 1 => [ + 'auth_type', + ':group:' . __('Config authentication'), + 'user', + 'password', + ':group:end', + ':group:' . __('HTTP authentication'), + 'auth_http_realm', + ':group:end', + ':group:' . __('Signon authentication'), + 'SignonSession', + 'SignonURL', + 'LogoutURL', + ], + ], + ], + 'Server_config' => [ + 'Servers' => [ + 1 => [ + 'only_db', + 'hide_db', + 'AllowRoot', + 'AllowNoPassword', + 'DisableIS', + 'AllowDeny/order', + 'AllowDeny/rules', + 'SessionTimeZone', + ], + ], + ], + 'Server_pmadb' => [ + 'Servers' => [ + 1 => [ + 'pmadb' => 'phpmyadmin', + 'controlhost', + 'controlport', + 'controluser', + 'controlpass', + 'bookmarktable' => 'pma__bookmark', + 'relation' => 'pma__relation', + 'userconfig' => 'pma__userconfig', + 'users' => 'pma__users', + 'usergroups' => 'pma__usergroups', + 'navigationhiding' => 'pma__navigationhiding', + 'table_info' => 'pma__table_info', + 'column_info' => 'pma__column_info', + 'history' => 'pma__history', + 'recent' => 'pma__recent', + 'favorite' => 'pma__favorite', + 'table_uiprefs' => 'pma__table_uiprefs', + 'tracking' => 'pma__tracking', + 'table_coords' => 'pma__table_coords', + 'pdf_pages' => 'pma__pdf_pages', + 'savedsearches' => 'pma__savedsearches', + 'central_columns' => 'pma__central_columns', + 'designer_settings' => 'pma__designer_settings', + 'export_templates' => 'pma__export_templates', + 'MaxTableUiprefs' => 100, + ], + ], + ], + 'Server_tracking' => [ + 'Servers' => [ + 1 => [ + 'tracking_version_auto_create', + 'tracking_default_statements', + 'tracking_add_drop_view', + 'tracking_add_drop_table', + 'tracking_add_drop_database', + ], + ], + ], + ]; + // phpcs:enable + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/SetupFormList.php b/srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/SetupFormList.php new file mode 100644 index 0000000..91edb27 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Forms/Setup/SetupFormList.php @@ -0,0 +1,37 @@ + [ + 'Export/method', + ':group:' . __('Quick'), + 'Export/quick_export_onserver', + 'Export/quick_export_onserver_overwrite', + ':group:end', + ':group:' . __('Custom'), + 'Export/format', + 'Export/compression', + 'Export/charset', + 'Export/lock_tables', + 'Export/as_separate_files', + 'Export/asfile' => ':group', + 'Export/onserver', + 'Export/onserver_overwrite', + ':group:end', + 'Export/file_template_table', + 'Export/file_template_database', + 'Export/file_template_server', + ], + 'Sql' => [ + 'Export/sql_include_comments' => ':group', + 'Export/sql_dates', + 'Export/sql_relation', + 'Export/sql_mime', + ':group:end', + 'Export/sql_use_transaction', + 'Export/sql_disable_fk', + 'Export/sql_views_as_tables', + 'Export/sql_metadata', + 'Export/sql_compatibility', + 'Export/sql_structure_or_data', + ':group:' . __('Structure'), + 'Export/sql_drop_database', + 'Export/sql_create_database', + 'Export/sql_drop_table', + 'Export/sql_create_table' => ':group', + 'Export/sql_if_not_exists', + 'Export/sql_auto_increment', + ':group:end', + 'Export/sql_create_view' => ':group', + 'Export/sql_view_current_user', + 'Export/sql_or_replace_view', + ':group:end', + 'Export/sql_procedure_function', + 'Export/sql_create_trigger', + 'Export/sql_backquotes', + ':group:end', + ':group:' . __('Data'), + 'Export/sql_delayed', + 'Export/sql_ignore', + 'Export/sql_type', + 'Export/sql_insert_syntax', + 'Export/sql_max_query_size', + 'Export/sql_hex_for_binary', + 'Export/sql_utc_time', + ], + 'CodeGen' => [ + 'Export/codegen_format', + ], + 'Csv' => [ + ':group:' . __('CSV'), + 'Export/csv_separator', + 'Export/csv_enclosed', + 'Export/csv_escaped', + 'Export/csv_terminated', + 'Export/csv_null', + 'Export/csv_removeCRLF', + 'Export/csv_columns', + ':group:end', + ':group:' . __('CSV for MS Excel'), + 'Export/excel_null', + 'Export/excel_removeCRLF', + 'Export/excel_columns', + 'Export/excel_edition', + ], + 'Latex' => [ + 'Export/latex_caption', + 'Export/latex_structure_or_data', + ':group:' . __('Structure'), + 'Export/latex_structure_caption', + 'Export/latex_structure_continued_caption', + 'Export/latex_structure_label', + 'Export/latex_relation', + 'Export/latex_comments', + 'Export/latex_mime', + ':group:end', + ':group:' . __('Data'), + 'Export/latex_columns', + 'Export/latex_data_caption', + 'Export/latex_data_continued_caption', + 'Export/latex_data_label', + 'Export/latex_null', + ], + 'Microsoft_Office' => [ + ':group:' . __('Microsoft Word 2000'), + 'Export/htmlword_structure_or_data', + 'Export/htmlword_null', + 'Export/htmlword_columns', + ], + 'Open_Document' => [ + ':group:' . __('OpenDocument Spreadsheet'), + 'Export/ods_columns', + 'Export/ods_null', + ':group:end', + ':group:' . __('OpenDocument Text'), + 'Export/odt_structure_or_data', + ':group:' . __('Structure'), + 'Export/odt_relation', + 'Export/odt_comments', + 'Export/odt_mime', + ':group:end', + ':group:' . __('Data'), + 'Export/odt_columns', + 'Export/odt_null', + ], + 'Texy' => [ + 'Export/texytext_structure_or_data', + ':group:' . __('Data'), + 'Export/texytext_null', + 'Export/texytext_columns', + ], + ]; + // phpcs:enable + } + + /** + * @return string + */ + public static function getName() + { + return __('Export'); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Forms/User/FeaturesForm.php b/srcs/phpmyadmin/libraries/classes/Config/Forms/User/FeaturesForm.php new file mode 100644 index 0000000..58d3c24 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Forms/User/FeaturesForm.php @@ -0,0 +1,95 @@ + [ + 'VersionCheck', + 'NaturalOrder', + 'InitialSlidersState', + 'SkipLockedTables', + 'DisableMultiTableMaintenance', + 'ShowHint', + 'SendErrorReports', + 'ConsoleEnterExecutes', + 'DisableShortcutKeys', + ], + 'Databases' => [ + 'Servers/1/only_db', // saves to Server/only_db + 'Servers/1/hide_db', // saves to Server/hide_db + 'MaxDbList', + 'MaxTableList', + 'DefaultConnectionCollation', + ], + 'Text_fields' => [ + 'CharEditing', + 'MinSizeForInputField', + 'MaxSizeForInputField', + 'CharTextareaCols', + 'CharTextareaRows', + 'TextareaCols', + 'TextareaRows', + 'LongtextDoubleTextarea', + ], + 'Page_titles' => [ + 'TitleDefault', + 'TitleTable', + 'TitleDatabase', + 'TitleServer', + ], + 'Warnings' => [ + 'PmaNoRelation_DisableWarning', + 'SuhosinDisableWarning', + 'LoginCookieValidityDisableWarning', + 'ReservedWordDisableWarning', + ], + 'Console' => [ + 'Console/Mode', + 'Console/StartHistory', + 'Console/AlwaysExpand', + 'Console/CurrentQuery', + 'Console/EnterExecutes', + 'Console/DarkTheme', + 'Console/Height', + 'Console/GroupQueries', + 'Console/OrderBy', + 'Console/Order', + ], + ]; + // skip Developer form if no setting is available + if ($GLOBALS['cfg']['UserprefsDeveloperTab']) { + $result['Developer'] = [ + 'DBG/sql', + ]; + } + return $result; + } + + /** + * @return string + */ + public static function getName() + { + return __('Features'); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Forms/User/ImportForm.php b/srcs/phpmyadmin/libraries/classes/Config/Forms/User/ImportForm.php new file mode 100644 index 0000000..c447567 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Forms/User/ImportForm.php @@ -0,0 +1,73 @@ + [ + 'Import/format', + 'Import/charset', + 'Import/allow_interrupt', + 'Import/skip_queries', + 'enable_drag_drop_import', + ], + 'Sql' => [ + 'Import/sql_compatibility', + 'Import/sql_no_auto_value_on_zero', + 'Import/sql_read_as_multibytes', + ], + 'Csv' => [ + ':group:' . __('CSV'), + 'Import/csv_replace', + 'Import/csv_ignore', + 'Import/csv_terminated', + 'Import/csv_enclosed', + 'Import/csv_escaped', + 'Import/csv_col_names', + ':group:end', + ':group:' . __('CSV using LOAD DATA'), + 'Import/ldi_replace', + 'Import/ldi_ignore', + 'Import/ldi_terminated', + 'Import/ldi_enclosed', + 'Import/ldi_escaped', + 'Import/ldi_local_option', + ], + 'Open_Document' => [ + ':group:' . __('OpenDocument Spreadsheet'), + 'Import/ods_col_names', + 'Import/ods_empty_rows', + 'Import/ods_recognize_percentages', + 'Import/ods_recognize_currency', + ], + + ]; + } + + /** + * @return string + */ + public static function getName() + { + return __('Import'); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Forms/User/MainForm.php b/srcs/phpmyadmin/libraries/classes/Config/Forms/User/MainForm.php new file mode 100644 index 0000000..907bbaa --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Forms/User/MainForm.php @@ -0,0 +1,98 @@ + [ + 'ShowCreateDb', + 'ShowStats', + 'ShowServerInfo', + ], + 'DbStructure' => [ + 'ShowDbStructureCharset', + 'ShowDbStructureComment', + 'ShowDbStructureCreation', + 'ShowDbStructureLastUpdate', + 'ShowDbStructureLastCheck', + ], + 'TableStructure' => [ + 'HideStructureActions', + 'ShowColumnComments', + ':group:' . __('Default transformations'), + 'DefaultTransformations/Hex', + 'DefaultTransformations/Substring', + 'DefaultTransformations/Bool2Text', + 'DefaultTransformations/External', + 'DefaultTransformations/PreApPend', + 'DefaultTransformations/DateFormat', + 'DefaultTransformations/Inline', + 'DefaultTransformations/TextImageLink', + 'DefaultTransformations/TextLink', + ':group:end', + ], + 'Browse' => [ + 'TableNavigationLinksMode', + 'ActionLinksMode', + 'ShowAll', + 'MaxRows', + 'Order', + 'BrowsePointerEnable', + 'BrowseMarkerEnable', + 'GridEditing', + 'SaveCellsAtOnce', + 'RepeatCells', + 'LimitChars', + 'RowActionLinks', + 'RowActionLinksWithoutUnique', + 'TablePrimaryKeyOrder', + 'RememberSorting', + 'RelationalDisplay', + ], + 'Edit' => [ + 'ProtectBinary', + 'ShowFunctionFields', + 'ShowFieldTypesInDataEditView', + 'InsertRows', + 'ForeignKeyDropdownOrder', + 'ForeignKeyMaxLimit', + ], + 'Tabs' => [ + 'TabsMode', + 'DefaultTabServer', + 'DefaultTabDatabase', + 'DefaultTabTable', + ], + 'DisplayRelationalSchema' => [ + 'PDFDefaultPageSize', + ], + ]; + } + + /** + * @return string + */ + public static function getName() + { + return __('Main panel'); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Forms/User/NaviForm.php b/srcs/phpmyadmin/libraries/classes/Config/Forms/User/NaviForm.php new file mode 100644 index 0000000..e2d373c --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Forms/User/NaviForm.php @@ -0,0 +1,74 @@ + [ + 'ShowDatabasesNavigationAsTree', + 'NavigationLinkWithMainPanel', + 'NavigationDisplayLogo', + 'NavigationLogoLink', + 'NavigationLogoLinkWindow', + 'NavigationTreePointerEnable', + 'FirstLevelNavigationItems', + 'NavigationTreeDisplayItemFilterMinimum', + 'NumRecentTables', + 'NumFavoriteTables', + 'NavigationWidth', + ], + 'Navi_tree' => [ + 'MaxNavigationItems', + 'NavigationTreeEnableGrouping', + 'NavigationTreeEnableExpansion', + 'NavigationTreeShowTables', + 'NavigationTreeShowViews', + 'NavigationTreeShowFunctions', + 'NavigationTreeShowProcedures', + 'NavigationTreeShowEvents', + 'NavigationTreeAutoexpandSingleDb', + ], + 'Navi_servers' => [ + 'NavigationDisplayServers', + 'DisplayServersList', + ], + 'Navi_databases' => [ + 'NavigationTreeDisplayDbFilterMinimum', + 'NavigationTreeDbSeparator', + ], + 'Navi_tables' => [ + 'NavigationTreeDefaultTabTable', + 'NavigationTreeDefaultTabTable2', + 'NavigationTreeTableSeparator', + 'NavigationTreeTableLevel', + ], + ]; + } + + /** + * @return string + */ + public static function getName() + { + return __('Navigation panel'); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Forms/User/SqlForm.php b/srcs/phpmyadmin/libraries/classes/Config/Forms/User/SqlForm.php new file mode 100644 index 0000000..3d8ac7a --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Forms/User/SqlForm.php @@ -0,0 +1,54 @@ + [ + 'ShowSQL', + 'Confirm', + 'QueryHistoryMax', + 'IgnoreMultiSubmitErrors', + 'MaxCharactersInDisplayedSQL', + 'RetainQueryBox', + 'CodemirrorEnable', + 'LintEnable', + 'EnableAutocompleteForTablesAndColumns', + 'DefaultForeignKeyChecks', + ], + 'Sql_box' => [ + 'SQLQuery/Edit', + 'SQLQuery/Explain', + 'SQLQuery/ShowAsPHP', + 'SQLQuery/Refresh', + ], + ]; + } + + /** + * @return string + */ + public static function getName() + { + return __('SQL queries'); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Forms/User/UserFormList.php b/srcs/phpmyadmin/libraries/classes/Config/Forms/User/UserFormList.php new file mode 100644 index 0000000..92294ed --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Forms/User/UserFormList.php @@ -0,0 +1,35 @@ +userPreferences = new UserPreferences(); + + $formClass = PageFormList::get($formGroupName); + if ($formClass === null) { + return; + } + + if (isset($_REQUEST['printview']) && $_REQUEST['printview'] == '1') { + return; + } + + if (! empty($elemId)) { + $this->_elemId = $elemId; + } + $this->_groupName = $formGroupName; + + $cf = new ConfigFile($GLOBALS['PMA_Config']->base_settings); + $this->userPreferences->pageInit($cf); + + $formDisplay = new $formClass($cf); + + // Process form + $error = null; + if (isset($_POST['submit_save']) + && $_POST['submit_save'] == $formGroupName + ) { + $this->_processPageSettings($formDisplay, $cf, $error); + } + + // Display forms + $this->_HTML = $this->_getPageSettingsDisplay($formDisplay, $error); + } + + /** + * Process response to form + * + * @param FormDisplay $formDisplay Form + * @param ConfigFile $cf Configuration file + * @param Message|null $error Error message + * + * @return void + */ + private function _processPageSettings(&$formDisplay, &$cf, &$error) + { + if ($formDisplay->process(false) && ! $formDisplay->hasErrors()) { + // save settings + $result = $this->userPreferences->save($cf->getConfigArray()); + if ($result === true) { + // reload page + $response = Response::getInstance(); + Core::sendHeaderLocation( + $response->getFooter()->getSelfUrl() + ); + exit; + } else { + $error = $result; + } + } + } + + /** + * Store errors in _errorHTML + * + * @param FormDisplay $formDisplay Form + * @param Message|null $error Error message + * + * @return void + */ + private function _storeError(&$formDisplay, &$error) + { + $retval = ''; + if ($error) { + $retval .= $error->getDisplay(); + } + if ($formDisplay->hasErrors()) { + // form has errors + $retval .= '
' + . '' . __( + 'Cannot save settings, submitted configuration form contains ' + . 'errors!' + ) . '' + . $formDisplay->displayErrors() + . '
'; + } + $this->_errorHTML = $retval; + } + + /** + * Display page-related settings + * + * @param FormDisplay $formDisplay Form + * @param Message $error Error message + * + * @return string + */ + private function _getPageSettingsDisplay(&$formDisplay, &$error) + { + $response = Response::getInstance(); + + $retval = ''; + + $this->_storeError($formDisplay, $error); + + $retval .= '
'; + $retval .= '
'; + $retval .= $formDisplay->getDisplay( + true, + true, + false, + $response->getFooter()->getSelfUrl(), + [ + 'submit_save' => $this->_groupName, + ] + ); + $retval .= '
'; + $retval .= '
'; + + return $retval; + } + + /** + * Get HTML output + * + * @return string + */ + public function getHTML() + { + return $this->_HTML; + } + + /** + * Get error HTML output + * + * @return string + */ + public function getErrorHTML() + { + return $this->_errorHTML; + } + + /** + * Group to show for Page-related settings + * @param string $formGroupName The name of config form group to display + * @return PageSettings + */ + public static function showGroup($formGroupName) + { + $object = new PageSettings($formGroupName); + + $response = Response::getInstance(); + $response->addHTML($object->getErrorHTML()); + $response->addHTML($object->getHTML()); + + return $object; + } + + /** + * Get HTML for navigation settings + * @return string + */ + public static function getNaviSettings() + { + $object = new PageSettings('Navi', 'pma_navigation_settings'); + + $response = Response::getInstance(); + $response->addHTML($object->getErrorHTML()); + return $object->getHTML(); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/ServerConfigChecks.php b/srcs/phpmyadmin/libraries/classes/Config/ServerConfigChecks.php new file mode 100644 index 0000000..1b0ac9d --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/ServerConfigChecks.php @@ -0,0 +1,583 @@ +cfg = $cfg; + } + + /** + * Perform config checks + * + * @return void + */ + public function performConfigChecks() + { + $blowfishSecret = $this->cfg->get('blowfish_secret'); + $blowfishSecretSet = false; + $cookieAuthUsed = false; + + list($cookieAuthUsed, $blowfishSecret, $blowfishSecretSet) + = $this->performConfigChecksServers( + $cookieAuthUsed, + $blowfishSecret, + $blowfishSecretSet + ); + + $this->performConfigChecksCookieAuthUsed( + $cookieAuthUsed, + $blowfishSecretSet, + $blowfishSecret + ); + + // + // $cfg['AllowArbitraryServer'] + // should be disabled + // + if ($this->cfg->getValue('AllowArbitraryServer')) { + $sAllowArbitraryServerWarn = sprintf( + __( + 'This %soption%s should be disabled as it allows attackers to ' + . 'bruteforce login to any MySQL server. If you feel this is necessary, ' + . 'use %srestrict login to MySQL server%s or %strusted proxies list%s. ' + . 'However, IP-based protection with trusted proxies list may not be ' + . 'reliable if your IP belongs to an ISP where thousands of users, ' + . 'including you, are connected to.' + ), + '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', + '[/a]', + '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', + '[/a]', + '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', + '[/a]' + ); + SetupIndex::messagesSet( + 'notice', + 'AllowArbitraryServer', + Descriptions::get('AllowArbitraryServer'), + Sanitize::sanitizeMessage($sAllowArbitraryServerWarn) + ); + } + + $this->performConfigChecksLoginCookie(); + + $sDirectoryNotice = __( + 'This value should be double checked to ensure that this directory is ' + . 'neither world accessible nor readable or writable by other users on ' + . 'your server.' + ); + + // + // $cfg['SaveDir'] + // should not be world-accessible + // + if ($this->cfg->getValue('SaveDir') != '') { + SetupIndex::messagesSet( + 'notice', + 'SaveDir', + Descriptions::get('SaveDir'), + Sanitize::sanitizeMessage($sDirectoryNotice) + ); + } + + // + // $cfg['TempDir'] + // should not be world-accessible + // + if ($this->cfg->getValue('TempDir') != '') { + SetupIndex::messagesSet( + 'notice', + 'TempDir', + Descriptions::get('TempDir'), + Sanitize::sanitizeMessage($sDirectoryNotice) + ); + } + + $this->performConfigChecksZips(); + } + + /** + * Check config of servers + * + * @param boolean $cookieAuthUsed Cookie auth is used + * @param string $blowfishSecret Blowfish secret + * @param boolean $blowfishSecretSet Blowfish secret set + * + * @return array + */ + protected function performConfigChecksServers( + $cookieAuthUsed, + $blowfishSecret, + $blowfishSecretSet + ) { + $serverCnt = $this->cfg->getServerCount(); + for ($i = 1; $i <= $serverCnt; $i++) { + $cookieAuthServer + = ($this->cfg->getValue("Servers/$i/auth_type") == 'cookie'); + $cookieAuthUsed |= $cookieAuthServer; + $serverName = $this->performConfigChecksServersGetServerName( + $this->cfg->getServerName($i), + $i + ); + $serverName = htmlspecialchars($serverName); + + list($blowfishSecret, $blowfishSecretSet) + = $this->performConfigChecksServersSetBlowfishSecret( + $blowfishSecret, + $cookieAuthServer, + $blowfishSecretSet + ); + + // + // $cfg['Servers'][$i]['ssl'] + // should be enabled if possible + // + if (! $this->cfg->getValue("Servers/$i/ssl")) { + $title = Descriptions::get('Servers/1/ssl') . " ($serverName)"; + SetupIndex::messagesSet( + 'notice', + "Servers/$i/ssl", + $title, + __( + 'You should use SSL connections if your database server ' + . 'supports it.' + ) + ); + } + $sSecurityInfoMsg = Sanitize::sanitizeMessage(sprintf( + __( + 'If you feel this is necessary, use additional protection settings - ' + . '%1$shost authentication%2$s settings and %3$strusted proxies list%4%s. ' + . 'However, IP-based protection may not be reliable if your IP belongs ' + . 'to an ISP where thousands of users, including you, are connected to.' + ), + '[a@' . Url::getCommon(['page' => 'servers', 'mode' => 'edit', 'id' => $i]) . '#tab_Server_config]', + '[/a]', + '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', + '[/a]' + )); + + // + // $cfg['Servers'][$i]['auth_type'] + // warn about full user credentials if 'auth_type' is 'config' + // + if ($this->cfg->getValue("Servers/$i/auth_type") == 'config' + && $this->cfg->getValue("Servers/$i/user") != '' + && $this->cfg->getValue("Servers/$i/password") != '' + ) { + $title = Descriptions::get('Servers/1/auth_type') + . " ($serverName)"; + SetupIndex::messagesSet( + 'notice', + "Servers/$i/auth_type", + $title, + Sanitize::sanitizeMessage(sprintf( + __( + 'You set the [kbd]config[/kbd] authentication type and included ' + . 'username and password for auto-login, which is not a desirable ' + . 'option for live hosts. Anyone who knows or guesses your phpMyAdmin ' + . 'URL can directly access your phpMyAdmin panel. Set %1$sauthentication ' + . 'type%2$s to [kbd]cookie[/kbd] or [kbd]http[/kbd].' + ), + '[a@' . Url::getCommon(['page' => 'servers', 'mode' => 'edit', 'id' => $i]) . '#tab_Server]', + '[/a]' + )) + . ' ' . $sSecurityInfoMsg + ); + } + + // + // $cfg['Servers'][$i]['AllowRoot'] + // $cfg['Servers'][$i]['AllowNoPassword'] + // serious security flaw + // + if ($this->cfg->getValue("Servers/$i/AllowRoot") + && $this->cfg->getValue("Servers/$i/AllowNoPassword") + ) { + $title = Descriptions::get('Servers/1/AllowNoPassword') + . " ($serverName)"; + SetupIndex::messagesSet( + 'notice', + "Servers/$i/AllowNoPassword", + $title, + __('You allow for connecting to the server without a password.') + . ' ' . $sSecurityInfoMsg + ); + } + } + return [ + $cookieAuthUsed, + $blowfishSecret, + $blowfishSecretSet, + ]; + } + + /** + * Set blowfish secret + * + * @param string $blowfishSecret Blowfish secret + * @param boolean $cookieAuthServer Cookie auth is used + * @param boolean $blowfishSecretSet Blowfish secret set + * + * @return array + */ + protected function performConfigChecksServersSetBlowfishSecret( + $blowfishSecret, + $cookieAuthServer, + $blowfishSecretSet + ) { + if ($cookieAuthServer && $blowfishSecret === null) { + $blowfishSecretSet = true; + $this->cfg->set('blowfish_secret', Util::generateRandom(32)); + } + return [ + $blowfishSecret, + $blowfishSecretSet, + ]; + } + + /** + * Define server name + * + * @param string $serverName Server name + * @param int $serverId Server id + * + * @return string Server name + */ + protected function performConfigChecksServersGetServerName( + $serverName, + $serverId + ) { + if ($serverName == 'localhost') { + $serverName .= " [$serverId]"; + return $serverName; + } + return $serverName; + } + + /** + * Perform config checks for zip part. + * + * @return void + */ + protected function performConfigChecksZips() + { + $this->performConfigChecksServerGZipdump(); + $this->performConfigChecksServerBZipdump(); + $this->performConfigChecksServersZipdump(); + } + + /** + * Perform config checks for zip part. + * + * @return void + */ + protected function performConfigChecksServersZipdump() + { + // + // $cfg['ZipDump'] + // requires zip_open in import + // + if ($this->cfg->getValue('ZipDump') && ! $this->functionExists('zip_open')) { + SetupIndex::messagesSet( + 'error', + 'ZipDump_import', + Descriptions::get('ZipDump'), + Sanitize::sanitizeMessage(sprintf( + __( + '%sZip decompression%s requires functions (%s) which are unavailable ' + . 'on this system.' + ), + '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]', + '[/a]', + 'zip_open' + )) + ); + } + + // + // $cfg['ZipDump'] + // requires gzcompress in export + // + if ($this->cfg->getValue('ZipDump') && ! $this->functionExists('gzcompress')) { + SetupIndex::messagesSet( + 'error', + 'ZipDump_export', + Descriptions::get('ZipDump'), + Sanitize::sanitizeMessage(sprintf( + __( + '%sZip compression%s requires functions (%s) which are unavailable on ' + . 'this system.' + ), + '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]', + '[/a]', + 'gzcompress' + )) + ); + } + } + + /** + * Check config of servers + * + * @param boolean $cookieAuthUsed Cookie auth is used + * @param boolean $blowfishSecretSet Blowfish secret set + * @param string $blowfishSecret Blowfish secret + * + * @return void + */ + protected function performConfigChecksCookieAuthUsed( + $cookieAuthUsed, + $blowfishSecretSet, + $blowfishSecret + ) { + // + // $cfg['blowfish_secret'] + // it's required for 'cookie' authentication + // + if ($cookieAuthUsed) { + if ($blowfishSecretSet) { + // 'cookie' auth used, blowfish_secret was generated + SetupIndex::messagesSet( + 'notice', + 'blowfish_secret_created', + Descriptions::get('blowfish_secret'), + Sanitize::sanitizeMessage(__( + 'You didn\'t have blowfish secret set and have enabled ' + . '[kbd]cookie[/kbd] authentication, so a key was automatically ' + . 'generated for you. It is used to encrypt cookies; you don\'t need to ' + . 'remember it.' + )) + ); + } else { + $blowfishWarnings = []; + // check length + if (strlen($blowfishSecret) < 32) { + // too short key + $blowfishWarnings[] = __( + 'Key is too short, it should have at least 32 characters.' + ); + } + // check used characters + $hasDigits = (bool) preg_match('/\d/', $blowfishSecret); + $hasChars = (bool) preg_match('/\S/', $blowfishSecret); + $hasNonword = (bool) preg_match('/\W/', $blowfishSecret); + if (! $hasDigits || ! $hasChars || ! $hasNonword) { + $blowfishWarnings[] = Sanitize::sanitizeMessage( + __( + 'Key should contain letters, numbers [em]and[/em] ' + . 'special characters.' + ) + ); + } + if (! empty($blowfishWarnings)) { + SetupIndex::messagesSet( + 'error', + 'blowfish_warnings' . count($blowfishWarnings), + Descriptions::get('blowfish_secret'), + implode('
', $blowfishWarnings) + ); + } + } + } + } + + /** + * Check configuration for login cookie + * + * @return void + */ + protected function performConfigChecksLoginCookie() + { + // + // $cfg['LoginCookieValidity'] + // value greater than session.gc_maxlifetime will cause + // random session invalidation after that time + $loginCookieValidity = $this->cfg->getValue('LoginCookieValidity'); + if ($loginCookieValidity > ini_get('session.gc_maxlifetime') + ) { + SetupIndex::messagesSet( + 'error', + 'LoginCookieValidity', + Descriptions::get('LoginCookieValidity'), + Sanitize::sanitizeMessage(sprintf( + __( + '%1$sLogin cookie validity%2$s greater than %3$ssession.gc_maxlifetime%4$s may ' + . 'cause random session invalidation (currently session.gc_maxlifetime ' + . 'is %5$d).' + ), + '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', + '[/a]', + '[a@' . Core::getPHPDocLink('session.configuration.php#ini.session.gc-maxlifetime') . ']', + '[/a]', + ini_get('session.gc_maxlifetime') + )) + ); + } + + // + // $cfg['LoginCookieValidity'] + // should be at most 1800 (30 min) + // + if ($loginCookieValidity > 1800) { + SetupIndex::messagesSet( + 'notice', + 'LoginCookieValidity', + Descriptions::get('LoginCookieValidity'), + Sanitize::sanitizeMessage(sprintf( + __( + '%sLogin cookie validity%s should be set to 1800 seconds (30 minutes) ' + . 'at most. Values larger than 1800 may pose a security risk such as ' + . 'impersonation.' + ), + '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', + '[/a]' + )) + ); + } + + // + // $cfg['LoginCookieValidity'] + // $cfg['LoginCookieStore'] + // LoginCookieValidity must be less or equal to LoginCookieStore + // + if (($this->cfg->getValue('LoginCookieStore') != 0) + && ($loginCookieValidity > $this->cfg->getValue('LoginCookieStore')) + ) { + SetupIndex::messagesSet( + 'error', + 'LoginCookieValidity', + Descriptions::get('LoginCookieValidity'), + Sanitize::sanitizeMessage(sprintf( + __( + 'If using [kbd]cookie[/kbd] authentication and %sLogin cookie store%s ' + . 'is not 0, %sLogin cookie validity%s must be set to a value less or ' + . 'equal to it.' + ), + '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', + '[/a]', + '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Security]', + '[/a]' + )) + ); + } + } + + /** + * Check GZipDump configuration + * + * @return void + */ + protected function performConfigChecksServerBZipdump() + { + // + // $cfg['BZipDump'] + // requires bzip2 functions + // + if ($this->cfg->getValue('BZipDump') + && (! $this->functionExists('bzopen') || ! $this->functionExists('bzcompress')) + ) { + $functions = $this->functionExists('bzopen') + ? '' : + 'bzopen'; + $functions .= $this->functionExists('bzcompress') + ? '' + : ($functions ? ', ' : '') . 'bzcompress'; + SetupIndex::messagesSet( + 'error', + 'BZipDump', + Descriptions::get('BZipDump'), + Sanitize::sanitizeMessage( + sprintf( + __( + '%1$sBzip2 compression and decompression%2$s requires functions (%3$s) which ' + . 'are unavailable on this system.' + ), + '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]', + '[/a]', + $functions + ) + ) + ); + } + } + + /** + * Check GZipDump configuration + * + * @return void + */ + protected function performConfigChecksServerGZipdump() + { + // + // $cfg['GZipDump'] + // requires zlib functions + // + if ($this->cfg->getValue('GZipDump') + && (! $this->functionExists('gzopen') || ! $this->functionExists('gzencode')) + ) { + SetupIndex::messagesSet( + 'error', + 'GZipDump', + Descriptions::get('GZipDump'), + Sanitize::sanitizeMessage(sprintf( + __( + '%1$sGZip compression and decompression%2$s requires functions (%3$s) which ' + . 'are unavailable on this system.' + ), + '[a@' . Url::getCommon(['page' => 'form', 'formset' => 'Features']) . '#tab_Import_export]', + '[/a]', + 'gzencode' + )) + ); + } + } + + /** + * Wrapper around function_exists to allow mock in test + * + * @param string $name Function name + * + * @return boolean + */ + protected function functionExists($name) + { + return function_exists($name); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/SpecialSchemaLinks.php b/srcs/phpmyadmin/libraries/classes/Config/SpecialSchemaLinks.php new file mode 100644 index 0000000..e0af136 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/SpecialSchemaLinks.php @@ -0,0 +1,478 @@ + array( + * // Table name + * 'db' => array( + * // Column name + * 'user' => array( + * // Main url param (can be an array where represent sql) + * 'link_param' => 'username', + * // Other url params + * 'link_dependancy_params' => array( + * 0 => array( + * // URL parameter name + * // (can be array where url param has static value) + * 'param_info' => 'hostname', + * // Column name related to url param + * 'column_name' => 'host' + * ) + * ), + * // Page to link + * 'default_page' => './server_privileges.php' + * ) + * ) + * ) + * ); + * + * @return array + */ + public static function get(): array + { + global $cfg; + + $defaultPage = './' . Util::getScriptNameForOption( + $cfg['DefaultTabTable'], + 'table' + ); + + return [ + 'mysql' => [ + 'columns_priv' => [ + 'user' => [ + 'link_param' => 'username', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'hostname', + 'column_name' => 'host', + ], + ], + 'default_page' => './server_privileges.php', + ], + 'table_name' => [ + 'link_param' => 'table', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'Db', + ], + ], + 'default_page' => $defaultPage, + ], + 'column_name' => [ + 'link_param' => 'field', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'Db', + ], + 1 => [ + 'param_info' => 'table', + 'column_name' => 'Table_name', + ], + ], + 'default_page' => './tbl_structure.php?change_column=1', + ], + ], + 'db' => [ + 'user' => [ + 'link_param' => 'username', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'hostname', + 'column_name' => 'host', + ], + ], + 'default_page' => './server_privileges.php', + ], + ], + 'event' => [ + 'name' => [ + 'link_param' => 'item_name', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'db', + ], + ], + 'default_page' => './db_events.php?edit_item=1', + ], + + ], + 'innodb_index_stats' => [ + 'table_name' => [ + 'link_param' => 'table', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'database_name', + ], + ], + 'default_page' => $defaultPage, + ], + 'index_name' => [ + 'link_param' => 'index', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'database_name', + ], + 1 => [ + 'param_info' => 'table', + 'column_name' => 'table_name', + ], + ], + 'default_page' => './tbl_structure.php', + ], + ], + 'innodb_table_stats' => [ + 'table_name' => [ + 'link_param' => 'table', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'database_name', + ], + ], + 'default_page' => $defaultPage, + ], + ], + 'proc' => [ + 'name' => [ + 'link_param' => 'item_name', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'db', + ], + 1 => [ + 'param_info' => 'item_type', + 'column_name' => 'type', + ], + ], + 'default_page' => './db_routines.php?edit_item=1', + ], + 'specific_name' => [ + 'link_param' => 'item_name', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'db', + ], + 1 => [ + 'param_info' => 'item_type', + 'column_name' => 'type', + ], + ], + 'default_page' => './db_routines.php?edit_item=1', + ], + ], + 'proc_priv' => [ + 'user' => [ + 'link_param' => 'username', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'hostname', + 'column_name' => 'Host', + ], + ], + 'default_page' => './server_privileges.php', + ], + 'routine_name' => [ + 'link_param' => 'item_name', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'Db', + ], + 1 => [ + 'param_info' => 'item_type', + 'column_name' => 'Routine_type', + ], + ], + 'default_page' => './db_routines.php?edit_item=1', + ], + ], + 'proxies_priv' => [ + 'user' => [ + 'link_param' => 'username', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'hostname', + 'column_name' => 'Host', + ], + ], + 'default_page' => './server_privileges.php', + ], + ], + 'tables_priv' => [ + 'user' => [ + 'link_param' => 'username', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'hostname', + 'column_name' => 'Host', + ], + ], + 'default_page' => './server_privileges.php', + ], + 'table_name' => [ + 'link_param' => 'table', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'Db', + ], + ], + 'default_page' => $defaultPage, + ], + ], + 'user' => [ + 'user' => [ + 'link_param' => 'username', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'hostname', + 'column_name' => 'host', + ], + ], + 'default_page' => './server_privileges.php', + ], + ], + ], + 'information_schema' => [ + 'columns' => [ + 'table_name' => [ + 'link_param' => 'table', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'table_schema', + ], + ], + 'default_page' => $defaultPage, + ], + 'column_name' => [ + 'link_param' => 'field', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'table_schema', + ], + 1 => [ + 'param_info' => 'table', + 'column_name' => 'table_name', + ], + ], + 'default_page' => './tbl_structure.php?change_column=1', + ], + ], + 'key_column_usage' => [ + 'table_name' => [ + 'link_param' => 'table', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'constraint_schema', + ], + ], + 'default_page' => $defaultPage, + ], + 'column_name' => [ + 'link_param' => 'field', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'table_schema', + ], + 1 => [ + 'param_info' => 'table', + 'column_name' => 'table_name', + ], + ], + 'default_page' => './tbl_structure.php?change_column=1', + ], + 'referenced_table_name' => [ + 'link_param' => 'table', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'referenced_table_schema', + ], + ], + 'default_page' => $defaultPage, + ], + 'referenced_column_name' => [ + 'link_param' => 'field', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'referenced_table_schema', + ], + 1 => [ + 'param_info' => 'table', + 'column_name' => 'referenced_table_name', + ], + ], + 'default_page' => './tbl_structure.php?change_column=1', + ], + ], + 'partitions' => [ + 'table_name' => [ + 'link_param' => 'table', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'table_schema', + ], + ], + 'default_page' => $defaultPage, + ], + ], + 'processlist' => [ + 'user' => [ + 'link_param' => 'username', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'hostname', + 'column_name' => 'host', + ], + ], + 'default_page' => './server_privileges.php', + ], + ], + 'referential_constraints' => [ + 'table_name' => [ + 'link_param' => 'table', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'constraint_schema', + ], + ], + 'default_page' => $defaultPage, + ], + 'referenced_table_name' => [ + 'link_param' => 'table', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'constraint_schema', + ], + ], + 'default_page' => $defaultPage, + ], + ], + 'routines' => [ + 'routine_name' => [ + 'link_param' => 'item_name', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'routine_schema', + ], + 1 => [ + 'param_info' => 'item_type', + 'column_name' => 'routine_type', + ], + ], + 'default_page' => './db_routines.php', + ], + ], + 'schemata' => [ + 'schema_name' => [ + 'link_param' => 'db', + 'default_page' => $defaultPage, + ], + ], + 'statistics' => [ + 'table_name' => [ + 'link_param' => 'table', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'table_schema', + ], + ], + 'default_page' => $defaultPage, + ], + 'column_name' => [ + 'link_param' => 'field', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'table_schema', + ], + 1 => [ + 'param_info' => 'table', + 'column_name' => 'table_name', + ], + ], + 'default_page' => './tbl_structure.php?change_column=1', + ], + ], + 'tables' => [ + 'table_name' => [ + 'link_param' => 'table', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'table_schema', + ], + ], + 'default_page' => $defaultPage, + ], + ], + 'table_constraints' => [ + 'table_name' => [ + 'link_param' => 'table', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'table_schema', + ], + ], + 'default_page' => $defaultPage, + ], + ], + 'views' => [ + 'table_name' => [ + 'link_param' => 'table', + 'link_dependancy_params' => [ + 0 => [ + 'param_info' => 'db', + 'column_name' => 'table_schema', + ], + ], + 'default_page' => $defaultPage, + ], + ], + ], + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Config/Validator.php b/srcs/phpmyadmin/libraries/classes/Config/Validator.php new file mode 100644 index 0000000..8294a90 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Config/Validator.php @@ -0,0 +1,594 @@ +getDbEntry('_validators', []); + if ($GLOBALS['PMA_Config']->get('is_setup')) { + return $validators; + } + + // not in setup script: load additional validators for user + // preferences we need original config values not overwritten + // by user preferences, creating a new PhpMyAdmin\Config instance is a + // better idea than hacking into its code + $uvs = $cf->getDbEntry('_userValidators', []); + foreach ($uvs as $field => $uvList) { + $uvList = (array) $uvList; + foreach ($uvList as &$uv) { + if (! is_array($uv)) { + continue; + } + for ($i = 1, $nb = count($uv); $i < $nb; $i++) { + if (mb_substr($uv[$i], 0, 6) == 'value:') { + $uv[$i] = Core::arrayRead( + mb_substr($uv[$i], 6), + $GLOBALS['PMA_Config']->base_settings + ); + } + } + } + $validators[$field] = isset($validators[$field]) + ? array_merge((array) $validators[$field], $uvList) + : $uvList; + } + return $validators; + } + + /** + * Runs validation $validator_id on values $values and returns error list. + * + * Return values: + * o array, keys - field path or formset id, values - array of errors + * when $isPostSource is true values is an empty array to allow for error list + * cleanup in HTML document + * o false - when no validators match name(s) given by $validator_id + * + * @param ConfigFile $cf Config file instance + * @param string|array $validatorId ID of validator(s) to run + * @param array $values Values to validate + * @param bool $isPostSource tells whether $values are directly from + * POST request + * + * @return bool|array + */ + public static function validate( + ConfigFile $cf, + $validatorId, + array &$values, + $isPostSource + ) { + // find validators + $validatorId = (array) $validatorId; + $validators = static::getValidators($cf); + $vids = []; + foreach ($validatorId as &$vid) { + $vid = $cf->getCanonicalPath($vid); + if (isset($validators[$vid])) { + $vids[] = $vid; + } + } + if (empty($vids)) { + return false; + } + + // create argument list with canonical paths and remember path mapping + $arguments = []; + $keyMap = []; + foreach ($values as $k => $v) { + $k2 = $isPostSource ? str_replace('-', '/', $k) : $k; + $k2 = mb_strpos($k2, '/') + ? $cf->getCanonicalPath($k2) + : $k2; + $keyMap[$k2] = $k; + $arguments[$k2] = $v; + } + + // validate + $result = []; + foreach ($vids as $vid) { + // call appropriate validation functions + foreach ((array) $validators[$vid] as $validator) { + $vdef = (array) $validator; + $vname = array_shift($vdef); + $vname = 'PhpMyAdmin\Config\Validator::' . $vname; + $args = array_merge([$vid, &$arguments], $vdef); + $r = call_user_func_array($vname, $args); + + // merge results + if (! is_array($r)) { + continue; + } + + foreach ($r as $key => $errorList) { + // skip empty values if $isPostSource is false + if (! $isPostSource && empty($errorList)) { + continue; + } + if (! isset($result[$key])) { + $result[$key] = []; + } + $result[$key] = array_merge( + $result[$key], + (array) $errorList + ); + } + } + } + + // restore original paths + $newResult = []; + foreach ($result as $k => $v) { + $k2 = isset($keyMap[$k]) ? $keyMap[$k] : $k; + if (is_array($v)) { + $newResult[$k2] = array_map('htmlspecialchars', $v); + } else { + $newResult[$k2] = htmlspecialchars($v); + } + } + return empty($newResult) ? true : $newResult; + } + + /** + * Test database connection + * + * @param string $host host name + * @param string $port tcp port to use + * @param string $socket socket to use + * @param string $user username to use + * @param string $pass password to use + * @param string $errorKey key to use in return array + * + * @return bool|array + */ + public static function testDBConnection( + $host, + $port, + $socket, + $user, + $pass = null, + $errorKey = 'Server' + ) { + if ($GLOBALS['cfg']['DBG']['demo']) { + // Connection test disabled on the demo server! + return true; + } + + $error = null; + $host = Core::sanitizeMySQLHost($host); + + error_clear_last(); + + if (DatabaseInterface::checkDbExtension('mysqli')) { + $socket = empty($socket) ? null : $socket; + $port = empty($port) ? null : $port; + $extension = 'mysqli'; + } else { + $socket = empty($socket) ? null : ':' . ($socket[0] == '/' ? '' : '/') . $socket; + $port = empty($port) ? null : ':' . $port; + $extension = 'mysql'; + } + + if ($extension == 'mysql') { + $conn = @mysql_connect($host . $port . $socket, $user, $pass); + if (! $conn) { + $error = __('Could not connect to the database server!'); + } else { + mysql_close($conn); + } + } else { + $conn = @mysqli_connect($host, $user, $pass, null, $port, $socket); + if (! $conn) { + $error = __('Could not connect to the database server!'); + } else { + mysqli_close($conn); + } + } + if ($error !== null) { + $lastError = error_get_last(); + if ($lastError !== null) { + $error .= ' - ' . $lastError['message']; + } + } + return $error === null ? true : [$errorKey => $error]; + } + + /** + * Validate server config + * + * @param string $path path to config, not used + * keep this parameter since the method is invoked using + * reflection along with other similar methods + * @param array $values config values + * + * @return array + */ + public static function validateServer($path, array $values) + { + $result = [ + 'Server' => '', + 'Servers/1/user' => '', + 'Servers/1/SignonSession' => '', + 'Servers/1/SignonURL' => '', + ]; + $error = false; + if (empty($values['Servers/1/auth_type'])) { + $values['Servers/1/auth_type'] = ''; + $result['Servers/1/auth_type'] = __('Invalid authentication type!'); + $error = true; + } + if ($values['Servers/1/auth_type'] == 'config' + && empty($values['Servers/1/user']) + ) { + $result['Servers/1/user'] = __( + 'Empty username while using [kbd]config[/kbd] authentication method!' + ); + $error = true; + } + if ($values['Servers/1/auth_type'] == 'signon' + && empty($values['Servers/1/SignonSession']) + ) { + $result['Servers/1/SignonSession'] = __( + 'Empty signon session name ' + . 'while using [kbd]signon[/kbd] authentication method!' + ); + $error = true; + } + if ($values['Servers/1/auth_type'] == 'signon' + && empty($values['Servers/1/SignonURL']) + ) { + $result['Servers/1/SignonURL'] = __( + 'Empty signon URL while using [kbd]signon[/kbd] authentication ' + . 'method!' + ); + $error = true; + } + + if (! $error && $values['Servers/1/auth_type'] == 'config') { + $password = ''; + if (! empty($values['Servers/1/password'])) { + $password = $values['Servers/1/password']; + } + $test = static::testDBConnection( + empty($values['Servers/1/host']) ? '' : $values['Servers/1/host'], + empty($values['Servers/1/port']) ? '' : $values['Servers/1/port'], + empty($values['Servers/1/socket']) ? '' : $values['Servers/1/socket'], + empty($values['Servers/1/user']) ? '' : $values['Servers/1/user'], + $password, + 'Server' + ); + + if ($test !== true) { + $result = array_merge($result, $test); + } + } + return $result; + } + + /** + * Validate pmadb config + * + * @param string $path path to config, not used + * keep this parameter since the method is invoked using + * reflection along with other similar methods + * @param array $values config values + * + * @return array + */ + public static function validatePMAStorage($path, array $values) + { + $result = [ + 'Server_pmadb' => '', + 'Servers/1/controluser' => '', + 'Servers/1/controlpass' => '', + ]; + $error = false; + + if (empty($values['Servers/1/pmadb'])) { + return $result; + } + + $result = []; + if (empty($values['Servers/1/controluser'])) { + $result['Servers/1/controluser'] = __( + 'Empty phpMyAdmin control user while using phpMyAdmin configuration ' + . 'storage!' + ); + $error = true; + } + if (empty($values['Servers/1/controlpass'])) { + $result['Servers/1/controlpass'] = __( + 'Empty phpMyAdmin control user password while using phpMyAdmin ' + . 'configuration storage!' + ); + $error = true; + } + if (! $error) { + $test = static::testDBConnection( + empty($values['Servers/1/host']) ? '' : $values['Servers/1/host'], + empty($values['Servers/1/port']) ? '' : $values['Servers/1/port'], + empty($values['Servers/1/socket']) ? '' : $values['Servers/1/socket'], + empty($values['Servers/1/controluser']) ? '' : $values['Servers/1/controluser'], + empty($values['Servers/1/controlpass']) ? '' : $values['Servers/1/controlpass'], + 'Server_pmadb' + ); + if ($test !== true) { + $result = array_merge($result, $test); + } + } + return $result; + } + + + /** + * Validates regular expression + * + * @param string $path path to config + * @param array $values config values + * + * @return array + */ + public static function validateRegex($path, array $values) + { + $result = [$path => '']; + + if (empty($values[$path])) { + return $result; + } + + error_clear_last(); + + $matches = []; + // in libraries/ListDatabase.php _checkHideDatabase(), + // a '/' is used as the delimiter for hide_db + @preg_match('/' . Util::requestString($values[$path]) . '/', '', $matches); + + $currentError = error_get_last(); + + if ($currentError !== null) { + $error = preg_replace('/^preg_match\(\): /', '', $currentError['message']); + return [$path => $error]; + } + + return $result; + } + + /** + * Validates TrustedProxies field + * + * @param string $path path to config + * @param array $values config values + * + * @return array + */ + public static function validateTrustedProxies($path, array $values) + { + $result = [$path => []]; + + if (empty($values[$path])) { + return $result; + } + + if (is_array($values[$path]) || is_object($values[$path])) { + // value already processed by FormDisplay::save + $lines = []; + foreach ($values[$path] as $ip => $v) { + $v = Util::requestString($v); + $lines[] = preg_match('/^-\d+$/', $ip) + ? $v + : $ip . ': ' . $v; + } + } else { + // AJAX validation + $lines = explode("\n", $values[$path]); + } + foreach ($lines as $line) { + $line = trim($line); + $matches = []; + // we catch anything that may (or may not) be an IP + if (! preg_match("/^(.+):(?:[ ]?)\\w+$/", $line, $matches)) { + $result[$path][] = __('Incorrect value:') . ' ' + . htmlspecialchars($line); + continue; + } + // now let's check whether we really have an IP address + if (filter_var($matches[1], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false + && filter_var($matches[1], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false + ) { + $ip = htmlspecialchars(trim($matches[1])); + $result[$path][] = sprintf(__('Incorrect IP address: %s'), $ip); + continue; + } + } + + return $result; + } + + /** + * Tests integer value + * + * @param string $path path to config + * @param array $values config values + * @param bool $allowNegative allow negative values + * @param bool $allowZero allow zero + * @param int $maxValue max allowed value + * @param string $errorString error message string + * + * @return string empty string if test is successful + */ + public static function validateNumber( + $path, + array $values, + $allowNegative, + $allowZero, + $maxValue, + $errorString + ) { + if (empty($values[$path])) { + return ''; + } + + $value = Util::requestString($values[$path]); + + if (intval($value) != $value + || (! $allowNegative && $value < 0) + || (! $allowZero && $value == 0) + || $value > $maxValue + ) { + return $errorString; + } + + return ''; + } + + /** + * Validates port number + * + * @param string $path path to config + * @param array $values config values + * + * @return array + */ + public static function validatePortNumber($path, array $values) + { + return [ + $path => static::validateNumber( + $path, + $values, + false, + false, + 65535, + __('Not a valid port number!') + ), + ]; + } + + /** + * Validates positive number + * + * @param string $path path to config + * @param array $values config values + * + * @return array + */ + public static function validatePositiveNumber($path, array $values) + { + return [ + $path => static::validateNumber( + $path, + $values, + false, + false, + PHP_INT_MAX, + __('Not a positive number!') + ), + ]; + } + + /** + * Validates non-negative number + * + * @param string $path path to config + * @param array $values config values + * + * @return array + */ + public static function validateNonNegativeNumber($path, array $values) + { + return [ + $path => static::validateNumber( + $path, + $values, + false, + true, + PHP_INT_MAX, + __('Not a non-negative number!') + ), + ]; + } + + /** + * Validates value according to given regular expression + * Pattern and modifiers must be a valid for PCRE and JavaScript RegExp + * + * @param string $path path to config + * @param array $values config values + * @param string $regex regular expression to match + * + * @return array|string + */ + public static function validateByRegex($path, array $values, $regex) + { + if (! isset($values[$path])) { + return ''; + } + $result = preg_match($regex, Util::requestString($values[$path])); + return [$path => $result ? '' : __('Incorrect value!')]; + } + + /** + * Validates upper bound for numeric inputs + * + * @param string $path path to config + * @param array $values config values + * @param int $maxValue maximal allowed value + * + * @return array + */ + public static function validateUpperBound($path, array $values, $maxValue) + { + $result = $values[$path] <= $maxValue; + return [ + $path => $result ? '' : sprintf( + __('Value must be less than or equal to %s!'), + $maxValue + ), + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Console.php b/srcs/phpmyadmin/libraries/classes/Console.php new file mode 100644 index 0000000..50ac77a --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Console.php @@ -0,0 +1,158 @@ +_isEnabled = true; + $this->relation = new Relation($GLOBALS['dbi']); + $this->template = new Template(); + } + + /** + * Set the ajax flag to indicate whether + * we are servicing an ajax request + * + * @param bool $isAjax Whether we are servicing an ajax request + * + * @return void + */ + public function setAjax(bool $isAjax): void + { + $this->_isAjax = $isAjax; + } + + /** + * Disables the rendering of the footer + * + * @return void + */ + public function disable(): void + { + $this->_isEnabled = false; + } + + /** + * Renders the bookmark content + * + * @access public + * @return string + */ + public static function getBookmarkContent(): string + { + $template = new Template(); + $cfgBookmark = Bookmark::getParams($GLOBALS['cfg']['Server']['user']); + if ($cfgBookmark) { + $bookmarks = Bookmark::getList( + $GLOBALS['dbi'], + $GLOBALS['cfg']['Server']['user'] + ); + $count_bookmarks = count($bookmarks); + if ($count_bookmarks > 0) { + $welcomeMessage = sprintf( + _ngettext( + 'Showing %1$d bookmark (both private and shared)', + 'Showing %1$d bookmarks (both private and shared)', + $count_bookmarks + ), + $count_bookmarks + ); + } else { + $welcomeMessage = __('No bookmarks'); + } + unset($count_bookmarks, $private_message, $shared_message); + return $template->render('console/bookmark_content', [ + 'welcome_message' => $welcomeMessage, + 'bookmarks' => $bookmarks, + ]); + } + return ''; + } + + /** + * Returns the list of JS scripts required by console + * + * @return array list of scripts + */ + public function getScripts(): array + { + return ['console.js']; + } + + /** + * Renders the console + * + * @access public + * @return string + */ + public function getDisplay(): string + { + if ((! $this->_isAjax) && $this->_isEnabled) { + $cfgBookmark = Bookmark::getParams( + $GLOBALS['cfg']['Server']['user'] + ); + + $image = Util::getImage('console', __('SQL Query Console')); + $_sql_history = $this->relation->getHistory( + $GLOBALS['cfg']['Server']['user'] + ); + $bookmarkContent = static::getBookmarkContent(); + + return $this->template->render('console/display', [ + 'cfg_bookmark' => $cfgBookmark, + 'image' => $image, + 'sql_history' => $_sql_history, + 'bookmark_content' => $bookmarkContent, + ]); + } + return ''; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/AbstractController.php b/srcs/phpmyadmin/libraries/classes/Controllers/AbstractController.php new file mode 100644 index 0000000..601e79f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/AbstractController.php @@ -0,0 +1,51 @@ +response = $response; + $this->dbi = $dbi; + $this->template = $template; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/AjaxController.php b/srcs/phpmyadmin/libraries/classes/Controllers/AjaxController.php new file mode 100644 index 0000000..0662277 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/AjaxController.php @@ -0,0 +1,97 @@ +config = $config; + } + + /** + * @return array JSON + */ + public function databases(): array + { + global $dblist; + + return ['databases' => $dblist->databases]; + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function tables(array $params): array + { + return ['tables' => $this->dbi->getTables($params['db'])]; + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function columns(array $params): array + { + return [ + 'columns' => $this->dbi->getColumnNames( + $params['db'], + $params['table'] + ), + ]; + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function getConfig(array $params): array + { + return ['value' => $this->config->get($params['key'])]; + } + + /** + * @param array $params Request parameters + * @return true|Message + */ + public function setConfig(array $params) + { + return $this->config->setUserValue( + null, + $params['key'], + json_decode($params['value']) + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/BrowseForeignersController.php b/srcs/phpmyadmin/libraries/classes/Controllers/BrowseForeignersController.php new file mode 100644 index 0000000..24441cd --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/BrowseForeignersController.php @@ -0,0 +1,82 @@ +browseForeigners = $browseForeigners; + $this->relation = $relation; + } + + /** + * @param array $params Request parameters + * @return string HTML + */ + public function index(array $params): string + { + $foreigners = $this->relation->getForeigners( + $params['db'], + $params['table'] + ); + $foreignLimit = $this->browseForeigners->getForeignLimit( + $params['foreign_showAll'] + ); + $foreignData = $this->relation->getForeignData( + $foreigners, + $params['field'], + true, + $params['foreign_filter'] ?? '', + $foreignLimit ?? null, + true + ); + + return $this->browseForeigners->getHtmlForRelationalFieldSelection( + $params['db'], + $params['table'], + $params['field'], + $foreignData, + $params['fieldkey'] ?? '', + $params['data'] ?? '' + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Database/AbstractController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Database/AbstractController.php new file mode 100644 index 0000000..31b752b --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Database/AbstractController.php @@ -0,0 +1,42 @@ +db = $db; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Database/CentralColumnsController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Database/CentralColumnsController.php new file mode 100644 index 0000000..97798ea --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Database/CentralColumnsController.php @@ -0,0 +1,195 @@ +centralColumns = $centralColumns; + } + + /** + * @param array $params Request parameters + * @return string HTML + */ + public function index(array $params): string + { + global $pmaThemeImage, $text_dir; + + if (! empty($params['total_rows']) + && Core::isValid($params['total_rows'], 'integer') + ) { + $totalRows = (int) $params['total_rows']; + } else { + $totalRows = $this->centralColumns->getCount($this->db); + } + + $pos = 0; + if (Core::isValid($params['pos'], 'integer')) { + $pos = (int) $params['pos']; + } + + return $this->centralColumns->getHtmlForMain( + $this->db, + $totalRows, + $pos, + $pmaThemeImage, + $text_dir + ); + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function getColumnList(array $params): array + { + return $this->centralColumns->getListRaw( + $this->db, + $params['cur_table'] ?? '' + ); + } + + /** + * @param array $params Request parameters + * @return string HTML + */ + public function populateColumns(array $params): string + { + return $this->centralColumns->getHtmlForColumnDropdown( + $this->db, + $params['selectedTable'] + ); + } + + /** + * @param array $params Request parameters + * @return true|Message + */ + public function editSave(array $params) + { + $columnDefault = $params['col_default']; + if ($columnDefault === 'NONE' && $params['col_default_sel'] !== 'USER_DEFINED') { + $columnDefault = ''; + } + return $this->centralColumns->updateOneColumn( + $this->db, + $params['orig_col_name'], + $params['col_name'], + $params['col_type'], + $params['col_attribute'], + $params['col_length'], + isset($params['col_isNull']) ? 1 : 0, + $params['collation'], + $params['col_extra'] ?? '', + $columnDefault + ); + } + + /** + * @param array $params Request parameters + * @return true|Message + */ + public function addNewColumn(array $params) + { + $columnDefault = $params['col_default']; + if ($columnDefault === 'NONE' && $params['col_default_sel'] !== 'USER_DEFINED') { + $columnDefault = ''; + } + return $this->centralColumns->updateOneColumn( + $this->db, + '', + $params['col_name'], + $params['col_type'], + $params['col_attribute'], + $params['col_length'], + isset($params['col_isNull']) ? 1 : 0, + $params['collation'], + $params['col_extra'] ?? '', + $columnDefault + ); + } + + /** + * @param array $params Request parameters + * @return true|Message + */ + public function addColumn(array $params) + { + return $this->centralColumns->syncUniqueColumns( + [$params['column-select']], + false, + $params['table-select'] + ); + } + + /** + * @param array $params Request parameters + * @return string HTML + */ + public function editPage(array $params): string + { + return $this->centralColumns->getHtmlForEditingPage( + $params['selected_fld'], + $params['db'] + ); + } + + /** + * @param array $params Request parameters + * @return true|Message + */ + public function updateMultipleColumn(array $params) + { + return $this->centralColumns->updateMultipleColumn($params); + } + + /** + * @param array $params Request parameters + * @return true|Message + */ + public function deleteSave(array $params) + { + $name = []; + parse_str($params['col_name'], $name); + return $this->centralColumns->deleteColumnsFromList( + $params['db'], + $name['selected_fld'], + false + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Database/DataDictionaryController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Database/DataDictionaryController.php new file mode 100644 index 0000000..ba424b6 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Database/DataDictionaryController.php @@ -0,0 +1,156 @@ +relation = $relation; + $this->transformations = $transformations; + } + + /** + * @return string HTML + */ + public function index(): string + { + $cfgRelation = $this->relation->getRelationsParam(); + + $comment = $this->relation->getDbComment($this->db); + + $this->dbi->selectDb($this->db); + $tablesNames = $this->dbi->getTables($this->db); + + $tables = []; + foreach ($tablesNames as $tableName) { + $showComment = (string) $this->dbi->getTable( + $this->db, + $tableName + )->getStatusInfo('TABLE_COMMENT'); + + list(, $primaryKeys, , ) = Util::processIndexData( + $this->dbi->getTableIndexes($this->db, $tableName) + ); + + list($foreigners, $hasRelation) = $this->relation->getRelationsAndStatus( + ! empty($cfgRelation['relation']), + $this->db, + $tableName + ); + + $columnsComments = $this->relation->getComments($this->db, $tableName); + + $columns = $this->dbi->getColumns($this->db, $tableName); + $rows = []; + foreach ($columns as $row) { + $extractedColumnSpec = Util::extractColumnSpec($row['Type']); + + $relation = ''; + if ($hasRelation) { + $foreigner = $this->relation->searchColumnInForeigners( + $foreigners, + $row['Field'] + ); + if ($foreigner !== false && $foreigner !== []) { + $relation = $foreigner['foreign_table']; + $relation .= ' -> '; + $relation .= $foreigner['foreign_field']; + } + } + + $mime = ''; + if ($cfgRelation['mimework']) { + $mimeMap = $this->transformations->getMime( + $this->db, + $tableName, + true + ); + if (isset($mimeMap[$row['Field']])) { + $mime = str_replace( + '_', + '/', + $mimeMap[$row['Field']]['mimetype'] + ); + } + } + + $rows[$row['Field']] = [ + 'name' => $row['Field'], + 'has_primary_key' => isset($primaryKeys[$row['Field']]), + 'type' => $extractedColumnSpec['type'], + 'print_type' => $extractedColumnSpec['print_type'], + 'is_nullable' => $row['Null'] !== '' && $row['Null'] !== 'NO', + 'default' => $row['Default'] ?? null, + 'comment' => $columnsComments[$row['Field']] ?? '', + 'mime' => $mime, + 'relation' => $relation, + ]; + } + + $indexesTable = ''; + if (count(Index::getFromTable($tableName, $this->db)) > 0) { + $indexesTable = Index::getHtmlForIndexes( + $tableName, + $this->db, + true + ); + } + + $tables[$tableName] = [ + 'name' => $tableName, + 'comment' => $showComment, + 'has_relation' => $hasRelation, + 'has_mime' => $cfgRelation['mimework'], + 'columns' => $rows, + 'indexes_table' => $indexesTable, + ]; + } + + return $this->template->render('database/data_dictionary/index', [ + 'database' => $this->db, + 'comment' => $comment, + 'tables' => $tables, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Database/EventsController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Database/EventsController.php new file mode 100644 index 0000000..e2b0121 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Database/EventsController.php @@ -0,0 +1,43 @@ +dbi); + $events->main(); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Database/MultiTableQueryController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Database/MultiTableQueryController.php new file mode 100644 index 0000000..b4e1375 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Database/MultiTableQueryController.php @@ -0,0 +1,61 @@ +dbi, $template, $this->db); + + return $queryInstance->getFormHtml(); + } + + /** + * @param array $params Request parameters + * @return void + */ + public function displayResults(array $params): void + { + global $pmaThemeImage; + + MultiTableQuery::displayResults( + $params['sql_query'], + $params['db'], + $pmaThemeImage + ); + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function table(array $params): array + { + $constrains = $this->dbi->getForeignKeyConstrains( + $params['db'], + $params['tables'] + ); + + return ['foreignKeyConstrains' => $constrains]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Database/RoutinesController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Database/RoutinesController.php new file mode 100644 index 0000000..4106ccc --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Database/RoutinesController.php @@ -0,0 +1,44 @@ +dbi); + $routines->main($params['type']); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Database/SqlController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Database/SqlController.php new file mode 100644 index 0000000..963f5bc --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Database/SqlController.php @@ -0,0 +1,49 @@ +getHtml( + true, + false, + isset($params['delimiter']) + ? htmlspecialchars($params['delimiter']) + : ';' + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Database/StructureController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Database/StructureController.php new file mode 100644 index 0000000..5407f19 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Database/StructureController.php @@ -0,0 +1,1088 @@ +relation = $relation; + $this->replication = $replication; + } + + /** + * Retrieves database information for further use + * + * @param string $subPart Page part name + * + * @return void + */ + private function getDatabaseInfo(string $subPart): void + { + list( + $tables, + $numTables, + $totalNumTables, + , + $isShowStats, + $dbIsSystemSchema, + , + , + $position + ) = Util::getDbInfo($this->db, $subPart); + + $this->tables = $tables; + $this->numTables = $numTables; + $this->position = $position; + $this->dbIsSystemSchema = $dbIsSystemSchema; + $this->totalNumTables = $totalNumTables; + $this->isShowStats = $isShowStats; + } + + /** + * Index action + * + * @param array $params Request parameters + * @return string HTML + */ + public function index(array $params): string + { + global $cfg; + + // Drops/deletes/etc. multiple tables if required + if ((! empty($params['submit_mult']) && isset($params['selected_tbl'])) + || isset($params['mult_btn']) + ) { + $this->multiSubmitAction(); + } + + // Gets the database structure + $this->getDatabaseInfo('_structure'); + + // Checks if there are any tables to be shown on current page. + // If there are no tables, the user is redirected to the last page + // having any. + if ($this->totalNumTables > 0 && $this->position > $this->totalNumTables) { + $uri = './db_structure.php' . Url::getCommonRaw([ + 'db' => $this->db, + 'pos' => max(0, $this->totalNumTables - $cfg['MaxTableList']), + 'reload' => 1, + ]); + Core::sendHeaderLocation($uri); + } + + include_once ROOT_PATH . 'libraries/replication.inc.php'; + + PageSettings::showGroup('DbStructure'); + + if ($this->numTables > 0) { + $urlParams = [ + 'pos' => $this->position, + 'db' => $this->db, + ]; + if (isset($params['sort'])) { + $urlParams['sort'] = $params['sort']; + } + if (isset($params['sort_order'])) { + $urlParams['sort_order'] = $params['sort_order']; + } + $listNavigator = Util::getListNavigator( + $this->totalNumTables, + $this->position, + $urlParams, + 'db_structure.php', + 'frame_content', + $cfg['MaxTableList'] + ); + + $tableList = $this->displayTableList(); + } + + $createTable = ''; + if (empty($this->dbIsSystemSchema)) { + $createTable = CreateTable::getHtml($this->db); + } + + return $this->template->render('database/structure/index', [ + 'database' => $this->db, + 'has_tables' => $this->numTables > 0, + 'list_navigator_html' => $listNavigator ?? '', + 'table_list_html' => $tableList ?? '', + 'is_system_schema' => ! empty($this->dbIsSystemSchema), + 'create_table_html' => $createTable, + ]); + } + + /** + * Add or remove favorite tables + * + * @param array $params Request parameters + * @return array|null JSON + */ + public function addRemoveFavoriteTablesAction(array $params): ?array + { + global $cfg; + + $favoriteInstance = RecentFavoriteTable::getInstance('favorite'); + if (isset($params['favoriteTables'])) { + $favoriteTables = json_decode($params['favoriteTables'], true); + } else { + $favoriteTables = []; + } + // Required to keep each user's preferences separate. + $user = sha1($cfg['Server']['user']); + + // Request for Synchronization of favorite tables. + if (isset($params['sync_favorite_tables'])) { + $cfgRelation = $this->relation->getRelationsParam(); + if ($cfgRelation['favoritework']) { + return $this->synchronizeFavoriteTables($favoriteInstance, $user, $favoriteTables); + } + return null; + } + $changes = true; + $titles = Util::buildActionTitles(); + $favoriteTable = $params['favorite_table']; + $alreadyFavorite = $this->checkFavoriteTable($favoriteTable); + + if (isset($params['remove_favorite'])) { + if ($alreadyFavorite) { + // If already in favorite list, remove it. + $favoriteInstance->remove($this->db, $favoriteTable); + $alreadyFavorite = false; // for favorite_anchor template + } + } elseif (isset($params['add_favorite'])) { + if (! $alreadyFavorite) { + $numTables = count($favoriteInstance->getTables()); + if ($numTables == $cfg['NumFavoriteTables']) { + $changes = false; + } else { + // Otherwise add to favorite list. + $favoriteInstance->add($this->db, $favoriteTable); + $alreadyFavorite = true; // for favorite_anchor template + } + } + } + + $favoriteTables[$user] = $favoriteInstance->getTables(); + + $json = []; + $json['changes'] = $changes; + if (! $changes) { + $json['message'] = $this->template->render('components/error_message', [ + 'msg' => __("Favorite List is full!"), + ]); + return $json; + } + // Check if current table is already in favorite list. + $favoriteParams = [ + 'db' => $this->db, + 'ajax_request' => true, + 'favorite_table' => $favoriteTable, + ($alreadyFavorite ? 'remove' : 'add') . '_favorite' => true, + ]; + + $json['user'] = $user; + $json['favoriteTables'] = json_encode($favoriteTables); + $json['list'] = $favoriteInstance->getHtmlList(); + $json['anchor'] = $this->template->render('database/structure/favorite_anchor', [ + 'table_name_hash' => md5($favoriteTable), + 'db_table_name_hash' => md5($this->db . "." . $favoriteTable), + 'fav_params' => $favoriteParams, + 'already_favorite' => $alreadyFavorite, + 'titles' => $titles, + ]); + + return $json; + } + + /** + * Handles request for real row count on database level view page. + * + * @param array $params Request parameters + * @return array JSON + */ + public function handleRealRowCountRequestAction(array $params): array + { + // If there is a request to update all table's row count. + if (! isset($params['real_row_count_all'])) { + // Get the real row count for the table. + $realRowCount = $this->dbi + ->getTable($this->db, (string) $params['table']) + ->getRealRowCountTable(); + // Format the number. + $realRowCount = Util::formatNumber($realRowCount, 0); + + return ['real_row_count' => $realRowCount]; + } + + // Array to store the results. + $realRowCountAll = []; + // Iterate over each table and fetch real row count. + foreach ($this->tables as $table) { + $rowCount = $this->dbi + ->getTable($this->db, $table['TABLE_NAME']) + ->getRealRowCountTable(); + $realRowCountAll[] = [ + 'table' => $table['TABLE_NAME'], + 'row_count' => $rowCount, + ]; + } + + return ['real_row_count_all' => json_encode($realRowCountAll)]; + } + + /** + * Handles actions related to multiple tables + * + * @return void + */ + public function multiSubmitAction(): void + { + $action = 'db_structure.php'; + $err_url = 'db_structure.php' . Url::getCommon( + ['db' => $this->db] + ); + + // see bug #2794840; in this case, code path is: + // db_structure.php -> libraries/mult_submits.inc.php -> sql.php + // -> db_structure.php and if we got an error on the multi submit, + // we must display it here and not call again mult_submits.inc.php + if (! isset($_POST['error']) || false === $_POST['error']) { + include ROOT_PATH . 'libraries/mult_submits.inc.php'; + } + if (empty($_POST['message'])) { + $_POST['message'] = Message::success(); + } + } + + /** + * Displays the list of tables + * + * @return string HTML + */ + protected function displayTableList(): string + { + $html = ''; + + // filtering + $html .= $this->template->render('filter', ['filter_value' => '']); + + $i = $sum_entries = 0; + $overhead_check = false; + $create_time_all = ''; + $update_time_all = ''; + $check_time_all = ''; + $num_columns = $GLOBALS['cfg']['PropertiesNumColumns'] > 1 + ? ceil($this->numTables / $GLOBALS['cfg']['PropertiesNumColumns']) + 1 + : 0; + $row_count = 0; + $sum_size = 0; + $overhead_size = 0; + + $hidden_fields = []; + $overall_approx_rows = false; + $structure_table_rows = []; + foreach ($this->tables as $keyname => $current_table) { + // Get valid statistics whatever is the table type + + $drop_query = ''; + $drop_message = ''; + $overhead = ''; + $input_class = ['checkall']; + + $table_is_view = false; + // Sets parameters for links + $tbl_url_query = Url::getCommon( + [ + 'db' => $this->db, + 'table' => $current_table['TABLE_NAME'], + ] + ); + // do not list the previous table's size info for a view + + list($current_table, $formatted_size, $unit, $formatted_overhead, + $overhead_unit, $overhead_size, $table_is_view, $sum_size) + = $this->getStuffForEngineTypeTable( + $current_table, + $sum_size, + $overhead_size + ); + + $curTable = $this->dbi + ->getTable($this->db, $current_table['TABLE_NAME']); + if (! $curTable->isMerge()) { + $sum_entries += $current_table['TABLE_ROWS']; + } + + $collationDefinition = '---'; + if (isset($current_table['Collation'])) { + $tableCollation = Charsets::findCollationByName( + $this->dbi, + $GLOBALS['cfg']['Server']['DisableIS'], + $current_table['Collation'] + ); + if ($tableCollation !== null) { + $collationDefinition = '' + . $tableCollation->getName() . ''; + } + } + + if ($this->isShowStats) { + $overhead = '-'; + if ($formatted_overhead != '') { + $overhead = '' + . '' . $formatted_overhead . ' ' + . '' . $overhead_unit . '' + . '' . "\n"; + $overhead_check = true; + $input_class[] = 'tbl-overhead'; + } + } + + if ($GLOBALS['cfg']['ShowDbStructureCharset']) { + $charset = ''; + if (isset($tableCollation)) { + $charset = $tableCollation->getCharset(); + } + } + + if ($GLOBALS['cfg']['ShowDbStructureCreation']) { + $create_time = isset($current_table['Create_time']) + ? $current_table['Create_time'] : ''; + if ($create_time + && (! $create_time_all + || $create_time < $create_time_all) + ) { + $create_time_all = $create_time; + } + } + + if ($GLOBALS['cfg']['ShowDbStructureLastUpdate']) { + $update_time = isset($current_table['Update_time']) + ? $current_table['Update_time'] : ''; + if ($update_time + && (! $update_time_all + || $update_time < $update_time_all) + ) { + $update_time_all = $update_time; + } + } + + if ($GLOBALS['cfg']['ShowDbStructureLastCheck']) { + $check_time = isset($current_table['Check_time']) + ? $current_table['Check_time'] : ''; + if ($check_time + && (! $check_time_all + || $check_time < $check_time_all) + ) { + $check_time_all = $check_time; + } + } + + $truename = $current_table['TABLE_NAME']; + + $i++; + + $row_count++; + if ($table_is_view) { + $hidden_fields[] = ''; + } + + /* + * Always activate links for Browse, Search and Empty, even if + * the icons are greyed, because + * 1. for views, we don't know the number of rows at this point + * 2. for tables, another source could have populated them since the + * page was generated + * + * I could have used the PHP ternary conditional operator but I find + * the code easier to read without this operator. + */ + $may_have_rows = $current_table['TABLE_ROWS'] > 0 || $table_is_view; + $titles = Util::buildActionTitles(); + + if (! $this->dbIsSystemSchema) { + $drop_query = sprintf( + 'DROP %s %s', + $table_is_view || $current_table['ENGINE'] == null ? 'VIEW' + : 'TABLE', + Util::backquote( + $current_table['TABLE_NAME'] + ) + ); + $drop_message = sprintf( + ($table_is_view || $current_table['ENGINE'] == null + ? __('View %s has been dropped.') + : __('Table %s has been dropped.')), + str_replace( + ' ', + ' ', + htmlspecialchars($current_table['TABLE_NAME']) + ) + ); + } + + if ($num_columns > 0 + && $this->numTables > $num_columns + && ($row_count % $num_columns) == 0 + ) { + $row_count = 1; + + $html .= $this->template->render('database/structure/table_header', [ + 'db' => $this->db, + 'db_is_system_schema' => $this->dbIsSystemSchema, + 'replication' => $GLOBALS['replication_info']['slave']['status'], + 'properties_num_columns' => $GLOBALS['cfg']['PropertiesNumColumns'], + 'is_show_stats' => $GLOBALS['is_show_stats'], + 'show_charset' => $GLOBALS['cfg']['ShowDbStructureCharset'], + 'show_comment' => $GLOBALS['cfg']['ShowDbStructureComment'], + 'show_creation' => $GLOBALS['cfg']['ShowDbStructureCreation'], + 'show_last_update' => $GLOBALS['cfg']['ShowDbStructureLastUpdate'], + 'show_last_check' => $GLOBALS['cfg']['ShowDbStructureLastCheck'], + 'num_favorite_tables' => $GLOBALS['cfg']['NumFavoriteTables'], + 'structure_table_rows' => $structure_table_rows, + ]); + $structure_table_rows = []; + } + + list($approx_rows, $show_superscript) = $this->isRowCountApproximated( + $current_table, + $table_is_view + ); + + list($do, $ignored) = $this->getReplicationStatus($truename); + + $structure_table_rows[] = [ + 'table_name_hash' => md5($current_table['TABLE_NAME']), + 'db_table_name_hash' => md5($this->db . '.' . $current_table['TABLE_NAME']), + 'db' => $this->db, + 'curr' => $i, + 'input_class' => implode(' ', $input_class), + 'table_is_view' => $table_is_view, + 'current_table' => $current_table, + 'browse_table_title' => $may_have_rows ? $titles['Browse'] : $titles['NoBrowse'], + 'search_table_title' => $may_have_rows ? $titles['Search'] : $titles['NoSearch'], + 'browse_table_label_title' => htmlspecialchars($current_table['TABLE_COMMENT']), + 'browse_table_label_truename' => $truename, + 'empty_table_sql_query' => urlencode( + 'TRUNCATE ' . Util::backquote( + $current_table['TABLE_NAME'] + ) + ), + 'empty_table_message_to_show' => urlencode( + sprintf( + __('Table %s has been emptied.'), + htmlspecialchars( + $current_table['TABLE_NAME'] + ) + ) + ), + 'empty_table_title' => $may_have_rows ? $titles['Empty'] : $titles['NoEmpty'], + 'tracking_icon' => $this->getTrackingIcon($truename), + 'server_slave_status' => $GLOBALS['replication_info']['slave']['status'], + 'tbl_url_query' => $tbl_url_query, + 'db_is_system_schema' => $this->dbIsSystemSchema, + 'titles' => $titles, + 'drop_query' => $drop_query, + 'drop_message' => $drop_message, + 'collation' => $collationDefinition, + 'formatted_size' => $formatted_size, + 'unit' => $unit, + 'overhead' => $overhead, + 'create_time' => isset($create_time) && $create_time + ? Util::localisedDate(strtotime($create_time)) : '-', + 'update_time' => isset($update_time) && $update_time + ? Util::localisedDate(strtotime($update_time)) : '-', + 'check_time' => isset($check_time) && $check_time + ? Util::localisedDate(strtotime($check_time)) : '-', + 'charset' => isset($charset) + ? $charset : '', + 'is_show_stats' => $this->isShowStats, + 'ignored' => $ignored, + 'do' => $do, + 'approx_rows' => $approx_rows, + 'show_superscript' => $show_superscript, + 'already_favorite' => $this->checkFavoriteTable( + $current_table['TABLE_NAME'] + ), + 'num_favorite_tables' => $GLOBALS['cfg']['NumFavoriteTables'], + 'properties_num_columns' => $GLOBALS['cfg']['PropertiesNumColumns'], + 'limit_chars' => $GLOBALS['cfg']['LimitChars'], + 'show_charset' => $GLOBALS['cfg']['ShowDbStructureCharset'], + 'show_comment' => $GLOBALS['cfg']['ShowDbStructureComment'], + 'show_creation' => $GLOBALS['cfg']['ShowDbStructureCreation'], + 'show_last_update' => $GLOBALS['cfg']['ShowDbStructureLastUpdate'], + 'show_last_check' => $GLOBALS['cfg']['ShowDbStructureLastCheck'], + ]; + + $overall_approx_rows = $overall_approx_rows || $approx_rows; + } + + $databaseCollation = []; + $databaseCharset = ''; + $collation = Charsets::findCollationByName( + $this->dbi, + $GLOBALS['cfg']['Server']['DisableIS'], + $this->dbi->getDbCollation($this->db) + ); + if ($collation !== null) { + $databaseCollation = [ + 'name' => $collation->getName(), + 'description' => $collation->getDescription(), + ]; + $databaseCharset = $collation->getCharset(); + } + + // table form + $html .= $this->template->render('database/structure/table_header', [ + 'db' => $this->db, + 'db_is_system_schema' => $this->dbIsSystemSchema, + 'replication' => $GLOBALS['replication_info']['slave']['status'], + 'properties_num_columns' => $GLOBALS['cfg']['PropertiesNumColumns'], + 'is_show_stats' => $GLOBALS['is_show_stats'], + 'show_charset' => $GLOBALS['cfg']['ShowDbStructureCharset'], + 'show_comment' => $GLOBALS['cfg']['ShowDbStructureComment'], + 'show_creation' => $GLOBALS['cfg']['ShowDbStructureCreation'], + 'show_last_update' => $GLOBALS['cfg']['ShowDbStructureLastUpdate'], + 'show_last_check' => $GLOBALS['cfg']['ShowDbStructureLastCheck'], + 'num_favorite_tables' => $GLOBALS['cfg']['NumFavoriteTables'], + 'structure_table_rows' => $structure_table_rows, + 'body_for_table_summary' => [ + 'num_tables' => $this->numTables, + 'server_slave_status' => $GLOBALS['replication_info']['slave']['status'], + 'db_is_system_schema' => $this->dbIsSystemSchema, + 'sum_entries' => $sum_entries, + 'database_collation' => $databaseCollation, + 'is_show_stats' => $this->isShowStats, + 'database_charset' => $databaseCharset, + 'sum_size' => $sum_size, + 'overhead_size' => $overhead_size, + 'create_time_all' => $create_time_all ? Util::localisedDate(strtotime($create_time_all)) : '-', + 'update_time_all' => $update_time_all ? Util::localisedDate(strtotime($update_time_all)) : '-', + 'check_time_all' => $check_time_all ? Util::localisedDate(strtotime($check_time_all)) : '-', + 'approx_rows' => $overall_approx_rows, + 'num_favorite_tables' => $GLOBALS['cfg']['NumFavoriteTables'], + 'db' => $GLOBALS['db'], + 'properties_num_columns' => $GLOBALS['cfg']['PropertiesNumColumns'], + 'dbi' => $this->dbi, + 'show_charset' => $GLOBALS['cfg']['ShowDbStructureCharset'], + 'show_comment' => $GLOBALS['cfg']['ShowDbStructureComment'], + 'show_creation' => $GLOBALS['cfg']['ShowDbStructureCreation'], + 'show_last_update' => $GLOBALS['cfg']['ShowDbStructureLastUpdate'], + 'show_last_check' => $GLOBALS['cfg']['ShowDbStructureLastCheck'], + ], + 'check_all_tables' => [ + 'pma_theme_image' => $GLOBALS['pmaThemeImage'], + 'text_dir' => $GLOBALS['text_dir'], + 'overhead_check' => $overhead_check, + 'db_is_system_schema' => $this->dbIsSystemSchema, + 'hidden_fields' => $hidden_fields, + 'disable_multi_table' => $GLOBALS['cfg']['DisableMultiTableMaintenance'], + 'central_columns_work' => $GLOBALS['cfgRelation']['centralcolumnswork'], + ], + ]); + + return $html; + } + + /** + * Returns the tracking icon if the table is tracked + * + * @param string $table table name + * + * @return string HTML for tracking icon + */ + protected function getTrackingIcon(string $table): string + { + $tracking_icon = ''; + if (Tracker::isActive()) { + $is_tracked = Tracker::isTracked($this->db, $table); + if ($is_tracked + || Tracker::getVersion($this->db, $table) > 0 + ) { + $tracking_icon = $this->template->render('database/structure/tracking_icon', [ + 'db' => $this->db, + 'table' => $table, + 'is_tracked' => $is_tracked, + ]); + } + } + return $tracking_icon; + } + + /** + * Returns whether the row count is approximated + * + * @param array $current_table array containing details about the table + * @param boolean $table_is_view whether the table is a view + * + * @return array + */ + protected function isRowCountApproximated( + array $current_table, + bool $table_is_view + ): array { + $approx_rows = false; + $show_superscript = ''; + + // there is a null value in the ENGINE + // - when the table needs to be repaired, or + // - when it's a view + // so ensure that we'll display "in use" below for a table + // that needs to be repaired + if (isset($current_table['TABLE_ROWS']) + && ($current_table['ENGINE'] != null || $table_is_view) + ) { + // InnoDB/TokuDB table: we did not get an accurate row count + $approx_rows = ! $table_is_view + && in_array($current_table['ENGINE'], ['InnoDB', 'TokuDB']) + && ! $current_table['COUNTED']; + + if ($table_is_view + && $current_table['TABLE_ROWS'] >= $GLOBALS['cfg']['MaxExactCountViews'] + ) { + $approx_rows = true; + $show_superscript = Util::showHint( + Sanitize::sanitizeMessage( + sprintf( + __( + 'This view has at least this number of ' + . 'rows. Please refer to %sdocumentation%s.' + ), + '[doc@cfg_MaxExactCountViews]', + '[/doc]' + ) + ) + ); + } + } + + return [ + $approx_rows, + $show_superscript, + ]; + } + + /** + * Returns the replication status of the table. + * + * @param string $table table name + * + * @return array + */ + protected function getReplicationStatus(string $table): array + { + $do = $ignored = false; + if ($GLOBALS['replication_info']['slave']['status']) { + $nbServSlaveDoDb = count( + $GLOBALS['replication_info']['slave']['Do_DB'] + ); + $nbServSlaveIgnoreDb = count( + $GLOBALS['replication_info']['slave']['Ignore_DB'] + ); + $searchDoDBInTruename = array_search( + $table, + $GLOBALS['replication_info']['slave']['Do_DB'] + ); + $searchDoDBInDB = array_search( + $this->db, + $GLOBALS['replication_info']['slave']['Do_DB'] + ); + + $do = (is_string($searchDoDBInTruename) && strlen($searchDoDBInTruename) > 0) + || (is_string($searchDoDBInDB) && strlen($searchDoDBInDB) > 0) + || ($nbServSlaveDoDb == 0 && $nbServSlaveIgnoreDb == 0) + || $this->hasTable( + $GLOBALS['replication_info']['slave']['Wild_Do_Table'], + $table + ); + + $searchDb = array_search( + $this->db, + $GLOBALS['replication_info']['slave']['Ignore_DB'] + ); + $searchTable = array_search( + $table, + $GLOBALS['replication_info']['slave']['Ignore_Table'] + ); + $ignored = (is_string($searchTable) && strlen($searchTable) > 0) + || (is_string($searchDb) && strlen($searchDb) > 0) + || $this->hasTable( + $GLOBALS['replication_info']['slave']['Wild_Ignore_Table'], + $table + ); + } + + return [ + $do, + $ignored, + ]; + } + + /** + * Synchronize favorite tables + * + * + * @param RecentFavoriteTable $favoriteInstance Instance of this class + * @param string $user The user hash + * @param array $favoriteTables Existing favorites + * + * @return array + */ + protected function synchronizeFavoriteTables( + RecentFavoriteTable $favoriteInstance, + string $user, + array $favoriteTables + ): array { + $favoriteInstanceTables = $favoriteInstance->getTables(); + + if (empty($favoriteInstanceTables) + && isset($favoriteTables[$user]) + ) { + foreach ($favoriteTables[$user] as $key => $value) { + $favoriteInstance->add($value['db'], $value['table']); + } + } + $favoriteTables[$user] = $favoriteInstance->getTables(); + + $json = [ + 'favoriteTables' => json_encode($favoriteTables), + 'list' => $favoriteInstance->getHtmlList(), + ]; + $serverId = $GLOBALS['server']; + // Set flag when localStorage and pmadb(if present) are in sync. + $_SESSION['tmpval']['favorites_synced'][$serverId] = true; + + return $json; + } + + /** + * Function to check if a table is already in favorite list. + * + * @param string $currentTable current table + * + * @return bool + */ + protected function checkFavoriteTable(string $currentTable): bool + { + // ensure $_SESSION['tmpval']['favoriteTables'] is initialized + RecentFavoriteTable::getInstance('favorite'); + foreach ($_SESSION['tmpval']['favoriteTables'][$GLOBALS['server']] as $value) { + if ($value['db'] == $this->db && $value['table'] == $currentTable) { + return true; + } + } + return false; + } + + /** + * Find table with truename + * + * @param array $db DB to look into + * @param string $truename Table name + * + * @return bool + */ + protected function hasTable(array $db, $truename) + { + foreach ($db as $db_table) { + if ($this->db == $this->replication->extractDbOrTable($db_table) + && preg_match( + '@^' . + preg_quote(mb_substr($this->replication->extractDbOrTable($db_table, 'table'), 0, -1), '@') . '@', + $truename + ) + ) { + return true; + } + } + return false; + } + + /** + * Get the value set for ENGINE table, + * + * @param array $current_table current table + * @param integer $sum_size total table size + * @param integer $overhead_size overhead size + * + * @return array + * @internal param bool $table_is_view whether table is view or not + */ + protected function getStuffForEngineTypeTable( + array $current_table, + $sum_size, + $overhead_size + ) { + $formatted_size = '-'; + $unit = ''; + $formatted_overhead = ''; + $overhead_unit = ''; + $table_is_view = false; + + switch ($current_table['ENGINE']) { + // MyISAM, ISAM or Heap table: Row count, data size and index size + // are accurate; data size is accurate for ARCHIVE + case 'MyISAM': + case 'ISAM': + case 'HEAP': + case 'MEMORY': + case 'ARCHIVE': + case 'Aria': + case 'Maria': + list($current_table, $formatted_size, $unit, $formatted_overhead, + $overhead_unit, $overhead_size, $sum_size) + = $this->getValuesForAriaTable( + $current_table, + $sum_size, + $overhead_size, + $formatted_size, + $unit, + $formatted_overhead, + $overhead_unit + ); + break; + case 'InnoDB': + case 'PBMS': + case 'TokuDB': + // InnoDB table: Row count is not accurate but data and index sizes are. + // PBMS table in Drizzle: TABLE_ROWS is taken from table cache, + // so it may be unavailable + list($current_table, $formatted_size, $unit, $sum_size) + = $this->getValuesForInnodbTable( + $current_table, + $sum_size + ); + break; + // Mysql 5.0.x (and lower) uses MRG_MyISAM + // and MySQL 5.1.x (and higher) uses MRG_MYISAM + // Both are aliases for MERGE + case 'MRG_MyISAM': + case 'MRG_MYISAM': + case 'MERGE': + case 'BerkeleyDB': + // Merge or BerkleyDB table: Only row count is accurate. + if ($this->isShowStats) { + $formatted_size = ' - '; + $unit = ''; + } + break; + // for a view, the ENGINE is sometimes reported as null, + // or on some servers it's reported as "SYSTEM VIEW" + case null: + case 'SYSTEM VIEW': + // possibly a view, do nothing + break; + default: + // Unknown table type. + if ($this->isShowStats) { + $formatted_size = __('unknown'); + $unit = ''; + } + } // end switch + + if ($current_table['TABLE_TYPE'] == 'VIEW' + || $current_table['TABLE_TYPE'] == 'SYSTEM VIEW' + ) { + // countRecords() takes care of $cfg['MaxExactCountViews'] + $current_table['TABLE_ROWS'] = $this->dbi + ->getTable($this->db, $current_table['TABLE_NAME']) + ->countRecords(true); + $table_is_view = true; + } + + return [ + $current_table, + $formatted_size, + $unit, + $formatted_overhead, + $overhead_unit, + $overhead_size, + $table_is_view, + $sum_size, + ]; + } + + /** + * Get values for ARIA/MARIA tables + * + * @param array $current_table current table + * @param integer $sum_size sum size + * @param integer $overhead_size overhead size + * @param integer $formatted_size formatted size + * @param string $unit unit + * @param integer $formatted_overhead overhead formatted + * @param string $overhead_unit overhead unit + * + * @return array + */ + protected function getValuesForAriaTable( + array $current_table, + $sum_size, + $overhead_size, + $formatted_size, + $unit, + $formatted_overhead, + $overhead_unit + ) { + if ($this->dbIsSystemSchema) { + $current_table['Rows'] = $this->dbi + ->getTable($this->db, $current_table['Name']) + ->countRecords(); + } + + if ($this->isShowStats) { + $tblsize = $current_table['Data_length'] + + $current_table['Index_length']; + $sum_size += $tblsize; + list($formatted_size, $unit) = Util::formatByteDown( + $tblsize, + 3, + $tblsize > 0 ? 1 : 0 + ); + if (isset($current_table['Data_free']) + && $current_table['Data_free'] > 0 + ) { + list($formatted_overhead, $overhead_unit) + = Util::formatByteDown( + $current_table['Data_free'], + 3, + ($current_table['Data_free'] > 0 ? 1 : 0) + ); + $overhead_size += $current_table['Data_free']; + } + } + return [ + $current_table, + $formatted_size, + $unit, + $formatted_overhead, + $overhead_unit, + $overhead_size, + $sum_size, + ]; + } + + /** + * Get values for InnoDB table + * + * @param array $current_table current table + * @param integer $sum_size sum size + * + * @return array + */ + protected function getValuesForInnodbTable( + array $current_table, + $sum_size + ) { + $formatted_size = $unit = ''; + + if ((in_array($current_table['ENGINE'], ['InnoDB', 'TokuDB']) + && $current_table['TABLE_ROWS'] < $GLOBALS['cfg']['MaxExactCount']) + || ! isset($current_table['TABLE_ROWS']) + ) { + $current_table['COUNTED'] = true; + $current_table['TABLE_ROWS'] = $this->dbi + ->getTable($this->db, $current_table['TABLE_NAME']) + ->countRecords(true); + } else { + $current_table['COUNTED'] = false; + } + + if ($this->isShowStats) { + $tblsize = $current_table['Data_length'] + + $current_table['Index_length']; + $sum_size += $tblsize; + list($formatted_size, $unit) = Util::formatByteDown( + $tblsize, + 3, + ($tblsize > 0 ? 1 : 0) + ); + } + + return [ + $current_table, + $formatted_size, + $unit, + $sum_size, + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Database/TriggersController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Database/TriggersController.php new file mode 100644 index 0000000..fd47457 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Database/TriggersController.php @@ -0,0 +1,43 @@ +dbi); + $triggers->main(); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/HomeController.php b/srcs/phpmyadmin/libraries/classes/Controllers/HomeController.php new file mode 100644 index 0000000..c0fe1ba --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/HomeController.php @@ -0,0 +1,517 @@ +config = $config; + $this->themeManager = $themeManager; + } + + + /** + * @return string HTML + */ + public function index(): string + { + global $cfg, $server, $collation_connection, $message; + + $languageManager = LanguageManager::getInstance(); + + if (! empty($message)) { + $displayMessage = Util::getMessage($message); + unset($message); + } + if (isset($_SESSION['partial_logout'])) { + $partialLogout = Message::success(__( + 'You were logged out from one server, to logout completely ' + . 'from phpMyAdmin, you need to logout from all servers.' + ))->getDisplay(); + unset($_SESSION['partial_logout']); + } + + $syncFavoriteTables = RecentFavoriteTable::getInstance('favorite') + ->getHtmlSyncFavoriteTables(); + + $hasServer = $server > 0 || count($cfg['Servers']) > 1; + if ($hasServer) { + $hasServerSelection = $cfg['ServerDefault'] == 0 + || (! $cfg['NavigationDisplayServers'] + && (count($cfg['Servers']) > 1 + || ($server == 0 && count($cfg['Servers']) === 1))); + if ($hasServerSelection) { + $serverSelection = Select::render(true, true); + } + + if ($server > 0) { + $checkUserPrivileges = new CheckUserPrivileges($this->dbi); + $checkUserPrivileges->getPrivileges(); + + if (($cfg['Server']['auth_type'] != 'config') && $cfg['ShowChgPassword']) { + $changePassword = $this->template->render('list/item', [ + 'content' => Util::getImage('s_passwd') . ' ' . __( + 'Change password' + ), + 'id' => 'li_change_password', + 'class' => 'no_bullets', + 'url' => [ + 'href' => 'user_password.php' . Url::getCommon(), + 'target' => null, + 'id' => 'change_password_anchor', + 'class' => 'ajax', + ], + 'mysql_help_page' => null, + ]); + } + + $charsets = Charsets::getCharsets($this->dbi, $cfg['Server']['DisableIS']); + $collations = Charsets::getCollations($this->dbi, $cfg['Server']['DisableIS']); + $charsetsList = []; + /** @var Charset $charset */ + foreach ($charsets as $charset) { + $collationsList = []; + /** @var Collation $collation */ + foreach ($collations[$charset->getName()] as $collation) { + $collationsList[] = [ + 'name' => $collation->getName(), + 'description' => $collation->getDescription(), + 'is_selected' => $collation_connection === $collation->getName(), + ]; + } + $charsetsList[] = [ + 'name' => $charset->getName(), + 'description' => $charset->getDescription(), + 'collations' => $collationsList, + ]; + } + + $userPreferences = $this->template->render('list/item', [ + 'content' => Util::getImage('b_tblops') . ' ' . __( + 'More settings' + ), + 'id' => 'li_user_preferences', + 'class' => 'no_bullets', + 'url' => [ + 'href' => 'prefs_manage.php' . Url::getCommon(), + 'target' => null, + 'id' => null, + 'class' => null, + ], + 'mysql_help_page' => null, + ]); + } + } + + $languageSelector = ''; + if (empty($cfg['Lang']) && $languageManager->hasChoice()) { + $languageSelector = $languageManager->getSelectorDisplay($this->template); + } + + $themeSelection = ''; + if ($cfg['ThemeManager']) { + $themeSelection = $this->themeManager->getHtmlSelectBox(); + } + + $databaseServer = []; + if ($server > 0 && $cfg['ShowServerInfo']) { + $hostInfo = ''; + if (! empty($cfg['Server']['verbose'])) { + $hostInfo .= $cfg['Server']['verbose']; + if ($cfg['ShowServerInfo']) { + $hostInfo .= ' ('; + } + } + if ($cfg['ShowServerInfo'] || empty($cfg['Server']['verbose'])) { + $hostInfo .= $this->dbi->getHostInfo(); + } + if (! empty($cfg['Server']['verbose']) && $cfg['ShowServerInfo']) { + $hostInfo .= ')'; + } + + $serverCharset = Charsets::getServerCharset($this->dbi, $cfg['Server']['DisableIS']); + $databaseServer = [ + 'host' => $hostInfo, + 'type' => Util::getServerType(), + 'connection' => Util::getServerSSL(), + 'version' => $this->dbi->getVersionString() . ' - ' . $this->dbi->getVersionComment(), + 'protocol' => $this->dbi->getProtoInfo(), + 'user' => $this->dbi->fetchValue('SELECT USER();'), + 'charset' => $serverCharset->getDescription() . ' (' . $serverCharset->getName() . ')', + ]; + } + + $webServer = []; + if ($cfg['ShowServerInfo']) { + $webServer['software'] = $_SERVER['SERVER_SOFTWARE']; + + if ($server > 0) { + $clientVersion = $this->dbi->getClientInfo(); + if (preg_match('#\d+\.\d+\.\d+#', $clientVersion)) { + $clientVersion = 'libmysql - ' . $clientVersion; + } + + $webServer['database'] = $clientVersion; + $webServer['php_extensions'] = Util::listPHPExtensions(); + $webServer['php_version'] = PHP_VERSION; + } + } + if ($cfg['ShowPhpInfo']) { + $phpInfo = $this->template->render('list/item', [ + 'content' => __('Show PHP information'), + 'id' => 'li_phpinfo', + 'class' => null, + 'url' => [ + 'href' => 'phpinfo.php' . Url::getCommon(), + 'target' => '_blank', + 'id' => null, + 'class' => null, + ], + 'mysql_help_page' => null, + ]); + } + + $relation = new Relation($this->dbi); + if ($server > 0) { + $cfgRelation = $relation->getRelationsParam(); + if (! $cfgRelation['allworks'] + && $cfg['PmaNoRelation_DisableWarning'] == false + ) { + $messageText = __( + 'The phpMyAdmin configuration storage is not completely ' + . 'configured, some extended features have been deactivated. ' + . '%sFind out why%s. ' + ); + if ($cfg['ZeroConf'] == true) { + $messageText .= '
' . + __( + 'Or alternately go to \'Operations\' tab of any database ' + . 'to set it up there.' + ); + } + $messageInstance = Message::notice($messageText); + $messageInstance->addParamHtml(''); + $messageInstance->addParamHtml(''); + /* Show error if user has configured something, notice elsewhere */ + if (! empty($cfg['Servers'][$server]['pmadb'])) { + $messageInstance->isError(true); + } + $configStorageMessage = $messageInstance->getDisplay(); + } + } + + $this->checkRequirements(); + + return $this->template->render('home/index', [ + 'message' => $displayMessage ?? '', + 'partial_logout' => $partialLogout ?? '', + 'is_git_revision' => $this->config->isGitRevision(), + 'server' => $server, + 'sync_favorite_tables' => $syncFavoriteTables, + 'has_server' => $hasServer, + 'is_demo' => $cfg['DBG']['demo'], + 'has_server_selection' => $hasServerSelection ?? false, + 'server_selection' => $serverSelection ?? '', + 'change_password' => $changePassword ?? '', + 'charsets' => $charsetsList ?? [], + 'language_selector' => $languageSelector, + 'theme_selection' => $themeSelection, + 'user_preferences' => $userPreferences ?? '', + 'database_server' => $databaseServer, + 'web_server' => $webServer, + 'php_info' => $phpInfo ?? '', + 'is_version_checked' => $cfg['VersionCheck'], + 'phpmyadmin_version' => PMA_VERSION, + 'config_storage_message' => $configStorageMessage ?? '', + ]); + } + + /** + * @param array $params Request parameters + * @return void + */ + public function setTheme(array $params): void + { + $this->themeManager->setActiveTheme($params['set_theme']); + $this->themeManager->setThemeCookie(); + + $userPreferences = new UserPreferences(); + $preferences = $userPreferences->load(); + $preferences['config_data']['ThemeDefault'] = $params['set_theme']; + $userPreferences->save($preferences['config_data']); + } + + /** + * @param array $params Request parameters + * @return void + */ + public function setCollationConnection(array $params): void + { + $this->config->setUserValue( + null, + 'DefaultConnectionCollation', + $params['collation_connection'], + 'utf8mb4_unicode_ci' + ); + } + + /** + * @return array JSON + */ + public function reloadRecentTablesList(): array + { + return [ + 'list' => RecentFavoriteTable::getInstance('recent')->getHtmlList(), + ]; + } + + /** + * @return string HTML + */ + public function gitRevision(): string + { + return (new GitRevision( + $this->response, + $this->config, + $this->template + ))->display(); + } + + /** + * @return void + */ + private function checkRequirements(): void + { + global $cfg, $server, $lang; + + /** + * mbstring is used for handling multibytes inside parser, so it is good + * to tell user something might be broken without it, see bug #1063149. + */ + if (! extension_loaded('mbstring')) { + trigger_error( + __( + 'The mbstring PHP extension was not found and you seem to be using' + . ' a multibyte charset. Without the mbstring extension phpMyAdmin' + . ' is unable to split strings correctly and it may result in' + . ' unexpected results.' + ), + E_USER_WARNING + ); + } + + /** + * Missing functionality + */ + if (! extension_loaded('curl') && ! ini_get('allow_url_fopen')) { + trigger_error( + __( + 'The curl extension was not found and allow_url_fopen is ' + . 'disabled. Due to this some features such as error reporting ' + . 'or version check are disabled.' + ) + ); + } + + if ($cfg['LoginCookieValidityDisableWarning'] == false) { + /** + * Check whether session.gc_maxlifetime limits session validity. + */ + $gc_time = (int) ini_get('session.gc_maxlifetime'); + if ($gc_time < $cfg['LoginCookieValidity']) { + trigger_error( + __( + 'Your PHP parameter [a@https://secure.php.net/manual/en/session.' . + 'configuration.php#ini.session.gc-maxlifetime@_blank]session.' . + 'gc_maxlifetime[/a] is lower than cookie validity configured ' . + 'in phpMyAdmin, because of this, your login might expire sooner ' . + 'than configured in phpMyAdmin.' + ), + E_USER_WARNING + ); + } + } + + /** + * Check whether LoginCookieValidity is limited by LoginCookieStore. + */ + if ($cfg['LoginCookieStore'] != 0 + && $cfg['LoginCookieStore'] < $cfg['LoginCookieValidity'] + ) { + trigger_error( + __( + 'Login cookie store is lower than cookie validity configured in ' . + 'phpMyAdmin, because of this, your login will expire sooner than ' . + 'configured in phpMyAdmin.' + ), + E_USER_WARNING + ); + } + + /** + * Warning if using the default MySQL controluser account + */ + if ($server != 0 + && isset($cfg['Server']['controluser']) && $cfg['Server']['controluser'] == 'pma' + && isset($cfg['Server']['controlpass']) && $cfg['Server']['controlpass'] == 'pmapass' + ) { + trigger_error( + __( + 'Your server is running with default values for the ' . + 'controluser and password (controlpass) and is open to ' . + 'intrusion; you really should fix this security weakness' . + ' by changing the password for controluser \'pma\'.' + ), + E_USER_WARNING + ); + } + + /** + * Check if user does not have defined blowfish secret and it is being used. + */ + if (! empty($_SESSION['encryption_key'])) { + if (empty($cfg['blowfish_secret'])) { + trigger_error( + __( + 'The configuration file now needs a secret passphrase (blowfish_secret).' + ), + E_USER_WARNING + ); + } elseif (strlen($cfg['blowfish_secret']) < 32) { + trigger_error( + __( + 'The secret passphrase in configuration (blowfish_secret) is too short.' + ), + E_USER_WARNING + ); + } + } + + /** + * Check for existence of config directory which should not exist in + * production environment. + */ + if (@file_exists(ROOT_PATH . 'config')) { + trigger_error( + __( + 'Directory [code]config[/code], which is used by the setup script, ' . + 'still exists in your phpMyAdmin directory. It is strongly ' . + 'recommended to remove it once phpMyAdmin has been configured. ' . + 'Otherwise the security of your server may be compromised by ' . + 'unauthorized people downloading your configuration.' + ), + E_USER_WARNING + ); + } + + /** + * Warning about Suhosin only if its simulation mode is not enabled + */ + if ($cfg['SuhosinDisableWarning'] == false + && ini_get('suhosin.request.max_value_length') + && ini_get('suhosin.simulation') == '0' + ) { + trigger_error( + sprintf( + __( + 'Server running with Suhosin. Please refer ' . + 'to %sdocumentation%s for possible issues.' + ), + '[doc@faq1-38]', + '[/doc]' + ), + E_USER_WARNING + ); + } + + /* Missing template cache */ + if ($this->config->getTempDir('twig') === null) { + trigger_error( + sprintf( + __( + 'The $cfg[\'TempDir\'] (%s) is not accessible. ' . + 'phpMyAdmin is not able to cache templates and will ' . + 'be slow because of this.' + ), + $this->config->get('TempDir') + ), + E_USER_WARNING + ); + } + + /** + * Warning about incomplete translations. + * + * The data file is created while creating release by ./scripts/remove-incomplete-mo + */ + if (@file_exists(ROOT_PATH . 'libraries/language_stats.inc.php')) { + include ROOT_PATH . 'libraries/language_stats.inc.php'; + /* + * This message is intentionally not translated, because we're + * handling incomplete translations here and focus on english + * speaking users. + */ + if (isset($GLOBALS['language_stats'][$lang]) + && $GLOBALS['language_stats'][$lang] < $cfg['TranslationWarningThreshold'] + ) { + trigger_error( + 'You are using an incomplete translation, please help to make it ' + . 'better by [a@https://www.phpmyadmin.net/translate/' + . '@_blank]contributing[/a].', + E_USER_NOTICE + ); + } + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/BinlogController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/BinlogController.php new file mode 100644 index 0000000..859559c --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/BinlogController.php @@ -0,0 +1,149 @@ +binaryLogs = $this->dbi->fetchResult( + 'SHOW MASTER LOGS', + 'Log_name', + null, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + } + + /** + * Index action + * + * @param array $params Request params + * + * @return string + */ + public function indexAction(array $params): string + { + global $cfg, $pmaThemeImage; + + include_once ROOT_PATH . 'libraries/server_common.inc.php'; + + $position = ! empty($params['pos']) ? (int) $params['pos'] : 0; + + $urlParams = []; + if (isset($params['log']) + && array_key_exists($params['log'], $this->binaryLogs) + ) { + $urlParams['log'] = $params['log']; + } + + $isFullQuery = false; + if (! empty($params['is_full_query'])) { + $isFullQuery = true; + $urlParams['is_full_query'] = 1; + } + + $sqlQuery = $this->getSqlQuery( + $params['log'] ?? '', + $position, + (int) $cfg['MaxRows'] + ); + $result = $this->dbi->query($sqlQuery); + + $numRows = 0; + if (isset($result) && $result) { + $numRows = $this->dbi->numRows($result); + } + + $previousParams = $urlParams; + $fullQueriesParams = $urlParams; + $nextParams = $urlParams; + if ($position > 0) { + $fullQueriesParams['pos'] = $position; + if ($position > $cfg['MaxRows']) { + $previousParams['pos'] = $position - $cfg['MaxRows']; + } + } + $fullQueriesParams['is_full_query'] = 1; + if ($isFullQuery) { + unset($fullQueriesParams['is_full_query']); + } + if ($numRows >= $cfg['MaxRows']) { + $nextParams['pos'] = $position + $cfg['MaxRows']; + } + + $values = []; + while ($value = $this->dbi->fetchAssoc($result)) { + $values[] = $value; + } + + return $this->template->render('server/binlog/index', [ + 'url_params' => $urlParams, + 'binary_logs' => $this->binaryLogs, + 'log' => $params['log'], + 'sql_message' => Util::getMessage(Message::success(), $sqlQuery), + 'values' => $values, + 'has_previous' => $position > 0, + 'has_next' => $numRows >= $cfg['MaxRows'], + 'previous_params' => $previousParams, + 'full_queries_params' => $fullQueriesParams, + 'next_params' => $nextParams, + 'has_icons' => Util::showIcons('TableNavigationLinksMode'), + 'is_full_query' => $isFullQuery, + 'image_path' => $pmaThemeImage, + ]); + } + + /** + * @param string $log Binary log file name + * @param int $position Position to display + * @param int $maxRows Maximum number of rows + * + * @return string + */ + private function getSqlQuery( + string $log, + int $position, + int $maxRows + ): string { + $sqlQuery = 'SHOW BINLOG EVENTS'; + if (! empty($log)) { + $sqlQuery .= ' IN \'' . $log . '\''; + } + $sqlQuery .= ' LIMIT ' . $position . ', ' . $maxRows; + + return $sqlQuery; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/CollationsController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/CollationsController.php new file mode 100644 index 0000000..2d806e8 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/CollationsController.php @@ -0,0 +1,100 @@ +charsets = $charsets ?? Charsets::getCharsets( + $this->dbi, + $cfg['Server']['DisableIS'] + ); + $this->collations = $collations ?? Charsets::getCollations( + $this->dbi, + $cfg['Server']['DisableIS'] + ); + } + + /** + * Index action + * + * @return string HTML + */ + public function indexAction(): string + { + include_once ROOT_PATH . 'libraries/server_common.inc.php'; + + $charsets = []; + /** @var Charset $charset */ + foreach ($this->charsets as $charset) { + $charsetCollations = []; + /** @var Collation $collation */ + foreach ($this->collations[$charset->getName()] as $collation) { + $charsetCollations[] = [ + 'name' => $collation->getName(), + 'description' => $collation->getDescription(), + 'is_default' => $collation->isDefault(), + ]; + } + + $charsets[] = [ + 'name' => $charset->getName(), + 'description' => $charset->getDescription(), + 'collations' => $charsetCollations, + ]; + } + + return $this->template->render('server/collations/index', [ + 'charsets' => $charsets, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/DatabasesController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/DatabasesController.php new file mode 100644 index 0000000..a601137 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/DatabasesController.php @@ -0,0 +1,424 @@ +setSortDetails($params['sort_by'], $params['sort_order']); + $this->hasStatistics = ! empty($params['statistics']); + $this->position = ! empty($params['pos']) ? (int) $params['pos'] : 0; + + /** + * Gets the databases list + */ + if ($server > 0) { + $this->databases = $this->dbi->getDatabasesFull( + null, + $this->hasStatistics, + DatabaseInterface::CONNECT_USER, + $this->sortBy, + $this->sortOrder, + $this->position, + true + ); + $this->databaseCount = count($dblist->databases); + } + + $urlParams = [ + 'statistics' => $this->hasStatistics, + 'pos' => $this->position, + 'sort_by' => $this->sortBy, + 'sort_order' => $this->sortOrder, + ]; + + $databases = $this->getDatabases($replication_types ?? []); + + $charsetsList = []; + if ($cfg['ShowCreateDb'] && $is_create_db_priv) { + $charsets = Charsets::getCharsets($this->dbi, $cfg['Server']['DisableIS']); + $collations = Charsets::getCollations($this->dbi, $cfg['Server']['DisableIS']); + $serverCollation = $this->dbi->getServerCollation(); + /** @var Charset $charset */ + foreach ($charsets as $charset) { + $collationsList = []; + /** @var Collation $collation */ + foreach ($collations[$charset->getName()] as $collation) { + $collationsList[] = [ + 'name' => $collation->getName(), + 'description' => $collation->getDescription(), + 'is_selected' => $serverCollation === $collation->getName(), + ]; + } + $charsetsList[] = [ + 'name' => $charset->getName(), + 'description' => $charset->getDescription(), + 'collations' => $collationsList, + ]; + } + } + + $headerStatistics = $this->getStatisticsColumns(); + + return $this->template->render('server/databases/index', [ + 'is_create_database_shown' => $cfg['ShowCreateDb'], + 'has_create_database_privileges' => $is_create_db_priv, + 'has_statistics' => $this->hasStatistics, + 'database_to_create' => $db_to_create, + 'databases' => $databases['databases'], + 'total_statistics' => $databases['total_statistics'], + 'header_statistics' => $headerStatistics, + 'charsets' => $charsetsList, + 'database_count' => $this->databaseCount, + 'pos' => $this->position, + 'url_params' => $urlParams, + 'max_db_list' => $cfg['MaxDbList'], + 'has_master_replication' => $replication_info['master']['status'], + 'has_slave_replication' => $replication_info['slave']['status'], + 'is_drop_allowed' => $this->dbi->isSuperuser() || $cfg['AllowUserDropDatabase'], + 'default_tab_database' => $cfg['DefaultTabDatabase'], + 'pma_theme_image' => $pmaThemeImage, + 'text_dir' => $text_dir, + ]); + } + + /** + * Handles creating a new database + * + * @param array $params Request parameters + * + * @return array JSON + */ + public function createDatabaseAction(array $params): array + { + global $cfg, $db; + + // lower_case_table_names=1 `DB` becomes `db` + if ($this->dbi->getLowerCaseNames() === '1') { + $params['new_db'] = mb_strtolower( + $params['new_db'] + ); + } + + /** + * Builds and executes the db creation sql query + */ + $sqlQuery = 'CREATE DATABASE ' . Util::backquote($params['new_db']); + if (! empty($params['db_collation'])) { + list($databaseCharset) = explode('_', $params['db_collation']); + $charsets = Charsets::getCharsets( + $this->dbi, + $cfg['Server']['DisableIS'] + ); + $collations = Charsets::getCollations( + $this->dbi, + $cfg['Server']['DisableIS'] + ); + if (in_array($databaseCharset, array_keys($charsets)) + && in_array($params['db_collation'], array_keys($collations[$databaseCharset])) + ) { + $sqlQuery .= ' DEFAULT' + . Util::getCharsetQueryPart($params['db_collation']); + } + } + $sqlQuery .= ';'; + + $result = $this->dbi->tryQuery($sqlQuery); + + if (! $result) { + // avoid displaying the not-created db name in header or navi panel + $db = ''; + + $message = Message::rawError($this->dbi->getError()); + $json = ['message' => $message]; + + $this->response->setRequestStatus(false); + } else { + $db = $params['new_db']; + + $message = Message::success(__('Database %1$s has been created.')); + $message->addParam($params['new_db']); + + $json = [ + 'message' => $message, + 'sql_query' => Util::getMessage(null, $sqlQuery, 'success'), + 'url_query' => Util::getScriptNameForOption( + $cfg['DefaultTabDatabase'], + 'database' + ) . Url::getCommon(['db' => $params['new_db']]), + ]; + } + + return $json; + } + + /** + * Handles dropping multiple databases + * + * @param array $params Request parameters + * + * @return array JSON + */ + public function dropDatabasesAction(array $params): array + { + global $submit_mult, $mult_btn, $selected; + + if (! isset($params['selected_dbs'])) { + $message = Message::error(__('No databases selected.')); + } else { + $action = 'server_databases.php'; + $err_url = $action . Url::getCommon(); + + $submit_mult = 'drop_db'; + $mult_btn = __('Yes'); + + include ROOT_PATH . 'libraries/mult_submits.inc.php'; + + if (empty($message)) { // no error message + $numberOfDatabases = count($selected); + $message = Message::success( + _ngettext( + '%1$d database has been dropped successfully.', + '%1$d databases have been dropped successfully.', + $numberOfDatabases + ) + ); + $message->addParam($numberOfDatabases); + } + } + + $json = []; + if ($message instanceof Message) { + $json = ['message' => $message]; + $this->response->setRequestStatus($message->isSuccess()); + } + + return $json; + } + + /** + * Extracts parameters sort order and sort by + * + * @param string|null $sortBy sort by + * @param string|null $sortOrder sort order + * + * @return void + */ + private function setSortDetails(?string $sortBy, ?string $sortOrder): void + { + if (empty($sortBy)) { + $this->sortBy = 'SCHEMA_NAME'; + } else { + $sortByWhitelist = [ + 'SCHEMA_NAME', + 'DEFAULT_COLLATION_NAME', + 'SCHEMA_TABLES', + 'SCHEMA_TABLE_ROWS', + 'SCHEMA_DATA_LENGTH', + 'SCHEMA_INDEX_LENGTH', + 'SCHEMA_LENGTH', + 'SCHEMA_DATA_FREE', + ]; + $this->sortBy = 'SCHEMA_NAME'; + if (in_array($sortBy, $sortByWhitelist)) { + $this->sortBy = $sortBy; + } + } + + $this->sortOrder = 'asc'; + if (isset($sortOrder) + && mb_strtolower($sortOrder) === 'desc' + ) { + $this->sortOrder = 'desc'; + } + } + + /** + * Returns database list + * + * @param array $replicationTypes replication types + * + * @return array + */ + private function getDatabases(array $replicationTypes): array + { + global $cfg, $replication_info; + + $databases = []; + $totalStatistics = $this->getStatisticsColumns(); + foreach ($this->databases as $database) { + $replication = [ + 'master' => [ + 'status' => $replication_info['master']['status'], + ], + 'slave' => [ + 'status' => $replication_info['slave']['status'], + ], + ]; + foreach ($replicationTypes as $type) { + if ($replication_info[$type]['status']) { + $key = array_search( + $database["SCHEMA_NAME"], + $replication_info[$type]['Ignore_DB'] + ); + if (strlen((string) $key) > 0) { + $replication[$type]['is_replicated'] = false; + } else { + $key = array_search( + $database["SCHEMA_NAME"], + $replication_info[$type]['Do_DB'] + ); + + if (strlen((string) $key) > 0 + || count($replication_info[$type]['Do_DB']) === 0 + ) { + // if ($key != null) did not work for index "0" + $replication[$type]['is_replicated'] = true; + } + } + } + } + + $statistics = $this->getStatisticsColumns(); + if ($this->hasStatistics) { + foreach (array_keys($statistics) as $key) { + $statistics[$key]['raw'] = $database[$key] ?? null; + $totalStatistics[$key]['raw'] += (int) $database[$key] ?? 0; + } + } + + $databases[$database['SCHEMA_NAME']] = [ + 'name' => $database['SCHEMA_NAME'], + 'collation' => [], + 'statistics' => $statistics, + 'replication' => $replication, + 'is_system_schema' => $this->dbi->isSystemSchema( + $database['SCHEMA_NAME'], + true + ), + ]; + $collation = Charsets::findCollationByName( + $this->dbi, + $cfg['Server']['DisableIS'], + $database['DEFAULT_COLLATION_NAME'] + ); + if ($collation !== null) { + $databases[$database['SCHEMA_NAME']]['collation'] = [ + 'name' => $collation->getName(), + 'description' => $collation->getDescription(), + ]; + } + } + + return [ + 'databases' => $databases, + 'total_statistics' => $totalStatistics, + ]; + } + + /** + * Prepares the statistics columns + * + * @return array + */ + private function getStatisticsColumns(): array + { + return [ + 'SCHEMA_TABLES' => [ + 'title' => __('Tables'), + 'format' => 'number', + 'raw' => 0, + ], + 'SCHEMA_TABLE_ROWS' => [ + 'title' => __('Rows'), + 'format' => 'number', + 'raw' => 0, + ], + 'SCHEMA_DATA_LENGTH' => [ + 'title' => __('Data'), + 'format' => 'byte', + 'raw' => 0, + ], + 'SCHEMA_INDEX_LENGTH' => [ + 'title' => __('Indexes'), + 'format' => 'byte', + 'raw' => 0, + ], + 'SCHEMA_LENGTH' => [ + 'title' => __('Total'), + 'format' => 'byte', + 'raw' => 0, + ], + 'SCHEMA_DATA_FREE' => [ + 'title' => __('Overhead'), + 'format' => 'byte', + 'raw' => 0, + ], + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/EnginesController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/EnginesController.php new file mode 100644 index 0000000..1170551 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/EnginesController.php @@ -0,0 +1,69 @@ +template->render('server/engines/index', [ + 'engines' => StorageEngine::getStorageEngines(), + ]); + } + + /** + * Displays details about a given Storage Engine + * + * @param array $params Request params + * + * @return string + */ + public function show(array $params): string + { + require ROOT_PATH . 'libraries/server_common.inc.php'; + + $page = $params['page'] ?? ''; + + $engine = []; + if (StorageEngine::isValid($params['engine'])) { + $storageEngine = StorageEngine::getEngine($params['engine']); + $engine = [ + 'engine' => $params['engine'], + 'title' => $storageEngine->getTitle(), + 'help_page' => $storageEngine->getMysqlHelpPage(), + 'comment' => $storageEngine->getComment(), + 'info_pages' => $storageEngine->getInfoPages(), + 'support' => $storageEngine->getSupportInformationMessage(), + 'variables' => $storageEngine->getHtmlVariables(), + 'page' => ! empty($page) ? $storageEngine->getPage($page) : '', + ]; + } + + return $this->template->render('server/engines/show', [ + 'engine' => $engine, + 'page' => $page, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/PluginsController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/PluginsController.php new file mode 100644 index 0000000..dce48a6 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/PluginsController.php @@ -0,0 +1,77 @@ +plugins = $plugins; + } + + /** + * Index action + * + * @return string + */ + public function index(): string + { + include ROOT_PATH . 'libraries/server_common.inc.php'; + + $header = $this->response->getHeader(); + $scripts = $header->getScripts(); + $scripts->addFile('vendor/jquery/jquery.tablesorter.js'); + $scripts->addFile('server/plugins.js'); + + $plugins = []; + $serverPlugins = $this->plugins->getAll(); + foreach ($serverPlugins as $plugin) { + $plugins[$plugin->getType()][] = $plugin->toArray(); + } + ksort($plugins); + + $cleanTypes = []; + foreach (array_keys($plugins) as $type) { + $cleanTypes[$type] = preg_replace( + '/[^a-z]/', + '', + mb_strtolower($type) + ); + } + return $this->template->render('server/plugins/index', [ + 'plugins' => $plugins, + 'clean_types' => $cleanTypes, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/ReplicationController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/ReplicationController.php new file mode 100644 index 0000000..dd48411 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/ReplicationController.php @@ -0,0 +1,72 @@ +getHtmlForErrorMessage(); + + if ($replication_info['master']['status']) { + $masterReplicationHtml = $replicationGui->getHtmlForMasterReplication(); + } + + if (isset($params['mr_configure'])) { + $masterConfigurationHtml = $replicationGui->getHtmlForMasterConfiguration(); + } else { + if (! isset($params['repl_clear_scr'])) { + $slaveConfigurationHtml = $replicationGui->getHtmlForSlaveConfiguration( + $replication_info['slave']['status'], + $server_slave_replication + ); + } + if (isset($params['sl_configure'])) { + $changeMasterHtml = $replicationGui->getHtmlForReplicationChangeMaster('slave_changemaster'); + } + } + + return $this->template->render('server/replication/index', [ + 'url_params' => $url_params, + 'is_super_user' => $this->dbi->isSuperuser(), + 'error_messages' => $errorMessages, + 'is_master' => $replication_info['master']['status'], + 'master_configure' => $params['mr_configure'], + 'slave_configure' => $params['sl_configure'], + 'clear_screen' => $params['repl_clear_scr'], + 'master_replication_html' => $masterReplicationHtml ?? '', + 'master_configuration_html' => $masterConfigurationHtml ?? '', + 'slave_configuration_html' => $slaveConfigurationHtml ?? '', + 'change_master_html' => $changeMasterHtml ?? '', + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/SqlController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/SqlController.php new file mode 100644 index 0000000..fd9ead6 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/SqlController.php @@ -0,0 +1,34 @@ +getHtml(); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/AbstractController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/AbstractController.php new file mode 100644 index 0000000..8d4b51d --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/AbstractController.php @@ -0,0 +1,42 @@ +data = $data; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/AdvisorController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/AdvisorController.php new file mode 100644 index 0000000..5b93b0e --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/AdvisorController.php @@ -0,0 +1,60 @@ +advisor = $advisor; + } + + /** + * @return string + */ + public function index(): string + { + $data = ''; + if ($this->data->dataLoaded) { + $data = json_encode($this->advisor->run()); + } + + return $this->template->render('server/status/advisor/index', [ + 'data' => $data, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/MonitorController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/MonitorController.php new file mode 100644 index 0000000..1d2cadc --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/MonitorController.php @@ -0,0 +1,146 @@ +monitor = $monitor; + } + + /** + * @return string HTML + */ + public function index(): string + { + $form = [ + 'server_time' => microtime(true) * 1000, + 'server_os' => SysInfo::getOs(), + 'is_superuser' => $this->dbi->isSuperuser(), + 'server_db_isLocal' => $this->data->db_isLocal, + ]; + + $javascriptVariableNames = []; + foreach ($this->data->status as $name => $value) { + if (is_numeric($value)) { + $javascriptVariableNames[] = $name; + } + } + + return $this->template->render('server/status/monitor/index', [ + 'image_path' => $GLOBALS['pmaThemeImage'], + 'javascript_variable_names' => $javascriptVariableNames, + 'form' => $form, + ]); + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function chartingData(array $params): array + { + $json = []; + $json['message'] = $this->monitor->getJsonForChartingData( + $params['requiredData'] ?? '' + ); + + return $json; + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function logDataTypeSlow(array $params): array + { + $json = []; + $json['message'] = $this->monitor->getJsonForLogDataTypeSlow( + (int) $params['time_start'], + (int) $params['time_end'] + ); + + return $json; + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function logDataTypeGeneral(array $params): array + { + $json = []; + $json['message'] = $this->monitor->getJsonForLogDataTypeGeneral( + (int) $params['time_start'], + (int) $params['time_end'], + (bool) $params['limitTypes'], + (bool) $params['removeVariables'] + ); + + return $json; + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function loggingVars(array $params): array + { + $json = []; + $json['message'] = $this->monitor->getJsonForLoggingVars( + $params['varName'], + $params['varValue'] + ); + + return $json; + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function queryAnalyzer(array $params): array + { + $json = []; + $json['message'] = $this->monitor->getJsonForQueryAnalyzer( + $params['database'] ?? '', + $params['query'] ?? '' + ); + + return $json; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/ProcessesController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/ProcessesController.php new file mode 100644 index 0000000..9817e1b --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/ProcessesController.php @@ -0,0 +1,240 @@ + true, + 'full' => $params['full'] ?? '', + 'column_name' => $params['column_name'] ?? '', + 'order_by_field' => $params['order_by_field'] ?? '', + 'sort_order' => $params['sort_order'] ?? '', + ]; + + $serverProcessList = $this->getList($params); + + return $this->template->render('server/status/processes/index', [ + 'url_params' => $urlParams, + 'is_checked' => $isChecked, + 'server_process_list' => $serverProcessList, + ]); + } + + /** + * Only sends the process list table + * + * @param array $params Request parameters + * @return string + */ + public function refresh(array $params): string + { + return $this->getList($params); + } + + /** + * @param array $params Request parameters + * @return array + */ + public function kill(array $params): array + { + $kill = (int) $params['kill']; + $query = $this->dbi->getKillQuery($kill); + + if ($this->dbi->tryQuery($query)) { + $message = Message::success( + __('Thread %s was successfully killed.') + ); + $this->response->setRequestStatus(true); + } else { + $message = Message::error( + __( + 'phpMyAdmin was unable to kill thread %s.' + . ' It probably has already been closed.' + ) + ); + $this->response->setRequestStatus(false); + } + $message->addParam($kill); + + $json = []; + $json['message'] = $message; + + return $json; + } + + /** + * @param array $params Request parameters + * @return string + */ + private function getList(array $params): string + { + $urlParams = []; + + $showFullSql = ! empty($params['full']); + if ($showFullSql) { + $urlParams['full'] = ''; + } else { + $urlParams['full'] = 1; + } + + // This array contains display name and real column name of each + // sortable column in the table + $sortableColumns = [ + [ + 'column_name' => __('ID'), + 'order_by_field' => 'Id', + ], + [ + 'column_name' => __('User'), + 'order_by_field' => 'User', + ], + [ + 'column_name' => __('Host'), + 'order_by_field' => 'Host', + ], + [ + 'column_name' => __('Database'), + 'order_by_field' => 'db', + ], + [ + 'column_name' => __('Command'), + 'order_by_field' => 'Command', + ], + [ + 'column_name' => __('Time'), + 'order_by_field' => 'Time', + ], + [ + 'column_name' => __('Status'), + 'order_by_field' => 'State', + ], + [ + 'column_name' => __('Progress'), + 'order_by_field' => 'Progress', + ], + [ + 'column_name' => __('SQL query'), + 'order_by_field' => 'Info', + ], + ]; + $sortableColCount = count($sortableColumns); + + $sqlQuery = $showFullSql + ? 'SHOW FULL PROCESSLIST' + : 'SHOW PROCESSLIST'; + if ((! empty($params['order_by_field']) + && ! empty($params['sort_order'])) + || ! empty($params['showExecuting']) + ) { + $urlParams['order_by_field'] = $params['order_by_field']; + $urlParams['sort_order'] = $params['sort_order']; + $urlParams['showExecuting'] = $params['showExecuting']; + $sqlQuery = 'SELECT * FROM `INFORMATION_SCHEMA`.`PROCESSLIST` '; + } + if (! empty($params['showExecuting'])) { + $sqlQuery .= ' WHERE state != "" '; + } + if (! empty($params['order_by_field']) && ! empty($params['sort_order'])) { + $sqlQuery .= ' ORDER BY ' + . Util::backquote($params['order_by_field']) + . ' ' . $params['sort_order']; + } + + $result = $this->dbi->query($sqlQuery); + + $columns = []; + foreach ($sortableColumns as $columnKey => $column) { + $is_sorted = ! empty($params['order_by_field']) + && ! empty($params['sort_order']) + && ($params['order_by_field'] == $column['order_by_field']); + + $column['sort_order'] = 'ASC'; + if ($is_sorted && $params['sort_order'] === 'ASC') { + $column['sort_order'] = 'DESC'; + } + if (isset($params['showExecuting'])) { + $column['showExecuting'] = 'on'; + } + + $columns[$columnKey] = [ + 'name' => $column['column_name'], + 'params' => $column, + 'is_sorted' => $is_sorted, + 'sort_order' => $column['sort_order'], + 'has_full_query' => false, + 'is_full' => false, + ]; + + if (0 === --$sortableColCount) { + $columns[$columnKey]['has_full_query'] = true; + if ($showFullSql) { + $columns[$columnKey]['is_full'] = true; + } + } + } + + $rows = []; + while ($process = $this->dbi->fetchAssoc($result)) { + // Array keys need to modify due to the way it has used + // to display column values + if ((! empty($params['order_by_field']) && ! empty($params['sort_order'])) + || ! empty($params['showExecuting']) + ) { + foreach (array_keys($process) as $key) { + $newKey = ucfirst(mb_strtolower($key)); + if ($newKey !== $key) { + $process[$newKey] = $process[$key]; + unset($process[$key]); + } + } + } + + $rows[] = [ + 'id' => $process['Id'], + 'user' => $process['User'], + 'host' => $process['Host'], + 'db' => ! isset($process['db']) || strlen($process['db']) === 0 ? '' : $process['db'], + 'command' => $process['Command'], + 'time' => $process['Time'], + 'state' => ! empty($process['State']) ? $process['State'] : '---', + 'progress' => ! empty($process['Progress']) ? $process['Progress'] : '---', + 'info' => ! empty($process['Info']) ? Util::formatSql( + $process['Info'], + ! $showFullSql + ) : '---', + ]; + } + + return $this->template->render('server/status/processes/list', [ + 'columns' => $columns, + 'rows' => $rows, + 'refresh_params' => $urlParams, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/QueriesController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/QueriesController.php new file mode 100644 index 0000000..76430d4 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/QueriesController.php @@ -0,0 +1,75 @@ +data->dataLoaded) { + $hourFactor = 3600 / $this->data->status['Uptime']; + $usedQueries = $this->data->used_queries; + $totalQueries = array_sum($usedQueries); + + $stats = [ + 'total' => $totalQueries, + 'per_hour' => $totalQueries * $hourFactor, + 'per_minute' => $totalQueries * 60 / $this->data->status['Uptime'], + 'per_second' => $totalQueries / $this->data->status['Uptime'], + ]; + + // reverse sort by value to show most used statements first + arsort($usedQueries); + + $chart = []; + $querySum = array_sum($usedQueries); + $otherSum = 0; + $queries = []; + foreach ($usedQueries as $key => $value) { + // For the percentage column, use Questions - Connections, because + // the number of connections is not an item of the Query types + // but is included in Questions. Then the total of the percentages is 100. + $name = str_replace(['Com_', '_'], ['', ' '], $key); + // Group together values that make out less than 2% into "Other", but only + // if we have more than 6 fractions already + if ($value < $querySum * 0.02 && count($chart) > 6) { + $otherSum += $value; + } else { + $chart[$name] = $value; + } + + $queries[$key] = [ + 'name' => $name, + 'value' => $value, + 'per_hour' => $value * $hourFactor, + 'percentage' => $value * 100 / $totalQueries, + ]; + } + + if ($otherSum > 0) { + $chart[__('Other')] = $otherSum; + } + } + + return $this->template->render('server/status/queries/index', [ + 'is_data_loaded' => $this->data->dataLoaded, + 'stats' => $stats ?? null, + 'queries' => $queries ?? [], + 'chart' => $chart ?? [], + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/StatusController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/StatusController.php new file mode 100644 index 0000000..53d78a3 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/StatusController.php @@ -0,0 +1,260 @@ +data->dataLoaded) { + $networkTraffic = implode( + ' ', + Util::formatByteDown( + $this->data->status['Bytes_received'] + $this->data->status['Bytes_sent'], + 3, + 1 + ) + ); + $uptime = Util::timespanFormat($this->data->status['Uptime']); + $startTime = Util::localisedDate($this->getStartTime()); + + $traffic = $this->getTrafficInfo(); + + $connections = $this->getConnectionsInfo(); + + // display replication information + if ($replication_info['master']['status'] + || $replication_info['slave']['status'] + ) { + $replication = $this->getReplicationInfo($replicationGui); + } + } + + return $this->template->render('server/status/status/index', [ + 'is_data_loaded' => $this->data->dataLoaded, + 'network_traffic' => $networkTraffic ?? null, + 'uptime' => $uptime ?? null, + 'start_time' => $startTime ?? null, + 'traffic' => $traffic, + 'connections' => $connections, + 'is_master' => $replication_info['master']['status'], + 'is_slave' => $replication_info['slave']['status'], + 'replication' => $replication, + ]); + } + + /** + * @return int + */ + private function getStartTime(): int + { + return (int) $this->dbi->fetchValue( + 'SELECT UNIX_TIMESTAMP() - ' . $this->data->status['Uptime'] + ); + } + + /** + * @return array + */ + private function getTrafficInfo(): array + { + $hourFactor = 3600 / $this->data->status['Uptime']; + + return [ + [ + 'name' => __('Received'), + 'number' => implode( + ' ', + Util::formatByteDown( + $this->data->status['Bytes_received'], + 3, + 1 + ) + ), + 'per_hour' => implode( + ' ', + Util::formatByteDown( + $this->data->status['Bytes_received'] * $hourFactor, + 3, + 1 + ) + ), + ], + [ + 'name' => __('Sent'), + 'number' => implode( + ' ', + Util::formatByteDown( + $this->data->status['Bytes_sent'], + 3, + 1 + ) + ), + 'per_hour' => implode( + ' ', + Util::formatByteDown( + $this->data->status['Bytes_sent'] * $hourFactor, + 3, + 1 + ) + ), + ], + [ + 'name' => __('Total'), + 'number' => implode( + ' ', + Util::formatByteDown( + $this->data->status['Bytes_received'] + $this->data->status['Bytes_sent'], + 3, + 1 + ) + ), + 'per_hour' => implode( + ' ', + Util::formatByteDown( + ($this->data->status['Bytes_received'] + $this->data->status['Bytes_sent']) * $hourFactor, + 3, + 1 + ) + ), + ], + ]; + } + + /** + * @return array + */ + private function getConnectionsInfo(): array + { + $hourFactor = 3600 / $this->data->status['Uptime']; + + $failedAttemptsPercentage = '---'; + $abortedPercentage = '---'; + if ($this->data->status['Connections'] > 0) { + $failedAttemptsPercentage = Util::formatNumber( + $this->data->status['Aborted_connects'] * 100 / $this->data->status['Connections'], + 0, + 2, + true + ) . '%'; + + $abortedPercentage = Util::formatNumber( + $this->data->status['Aborted_clients'] * 100 / $this->data->status['Connections'], + 0, + 2, + true + ) . '%'; + } + + return [ + [ + 'name' => __('Max. concurrent connections'), + 'number' => Util::formatNumber( + $this->data->status['Max_used_connections'], + 0 + ), + 'per_hour' => '---', + 'percentage' => '---', + ], + [ + 'name' => __('Failed attempts'), + 'number' => Util::formatNumber( + $this->data->status['Aborted_connects'], + 4, + 1, + true + ), + 'per_hour' => Util::formatNumber( + $this->data->status['Aborted_connects'] * $hourFactor, + 4, + 2, + true + ), + 'percentage' => $failedAttemptsPercentage, + ], + [ + 'name' => __('Aborted'), + 'number' => Util::formatNumber( + $this->data->status['Aborted_clients'], + 4, + 1, + true + ), + 'per_hour' => Util::formatNumber( + $this->data->status['Aborted_clients'] * $hourFactor, + 4, + 2, + true + ), + 'percentage' => $abortedPercentage, + ], + [ + 'name' => __('Total'), + 'number' => Util::formatNumber( + $this->data->status['Connections'], + 4, + 0 + ), + 'per_hour' => Util::formatNumber( + $this->data->status['Connections'] * $hourFactor, + 4, + 2 + ), + 'percentage' => Util::formatNumber(100, 0, 2) . '%', + ], + ]; + } + + /** + * @param ReplicationGui $replicationGui ReplicationGui instance + * + * @return string + */ + private function getReplicationInfo(ReplicationGui $replicationGui): string + { + global $replication_info, $replication_types; + + $output = ''; + foreach ($replication_types as $type) { + if (isset($replication_info[$type]['status']) + && $replication_info[$type]['status'] + ) { + $output .= $replicationGui->getHtmlForReplicationStatusTable($type); + } + } + + return $output; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/VariablesController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/VariablesController.php new file mode 100644 index 0000000..a17f15f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/VariablesController.php @@ -0,0 +1,639 @@ +flush($params['flush']); + } + + if ($this->data->dataLoaded) { + $categories = []; + foreach ($this->data->sections as $sectionId => $sectionName) { + if (isset($this->data->sectionUsed[$sectionId])) { + $categories[$sectionId] = [ + 'id' => $sectionId, + 'name' => $sectionName, + 'is_selected' => false, + ]; + if (! empty($params['filterCategory']) + && $params['filterCategory'] === $sectionId + ) { + $categories[$sectionId]['is_selected'] = true; + } + } + } + + $links = []; + foreach ($this->data->links as $sectionName => $sectionLinks) { + $links[$sectionName] = [ + 'name' => 'status_' . $sectionName, + 'links' => $sectionLinks, + ]; + } + + $descriptions = $this->getDescriptions(); + $alerts = $this->getAlerts(); + + $variables = []; + foreach ($this->data->status as $name => $value) { + $variables[$name] = [ + 'name' => $name, + 'value' => $value, + 'is_numeric' => is_numeric($value), + 'class' => $this->data->allocationMap[$name] ?? null, + 'doc' => '', + 'has_alert' => false, + 'is_alert' => false, + 'description' => $descriptions[$name] ?? '', + 'description_doc' => [], + ]; + + // Fields containing % are calculated, + // they can not be described in MySQL documentation + if (mb_strpos($name, '%') === false) { + $variables[$name]['doc'] = Util::linkToVarDocumentation( + $name, + $this->dbi->isMariaDB() + ); + } + + if (isset($alerts[$name])) { + $variables[$name]['has_alert'] = true; + if ($value > $alerts[$name]) { + $variables[$name]['is_alert'] = true; + } + } + + if (isset($this->data->links[$name])) { + foreach ($this->data->links[$name] as $linkName => $linkUrl) { + $variables[$name]['description_doc'][] = [ + 'name' => $linkName, + 'url' => $linkUrl, + ]; + } + } + } + } + + return $this->template->render('server/status/variables/index', [ + 'is_data_loaded' => $this->data->dataLoaded, + 'filter_text' => ! empty($params['filterText']) ? $params['filterText'] : '', + 'is_only_alerts' => ! empty($params['filterAlert']), + 'is_not_formatted' => ! empty($params['dontFormat']), + 'categories' => $categories ?? [], + 'links' => $links ?? [], + 'variables' => $variables ?? [], + ]); + } + + /** + * Flush status variables if requested + * + * @param string $flush Variable name + * @return void + */ + private function flush(string $flush): void + { + $flushCommands = [ + 'STATUS', + 'TABLES', + 'QUERY CACHE', + ]; + + if (in_array($flush, $flushCommands)) { + $this->dbi->query('FLUSH ' . $flush . ';'); + } + } + + /** + * @return array + */ + private function getAlerts(): array + { + // name => max value before alert + return [ + // lower is better + // variable => max value + 'Aborted_clients' => 0, + 'Aborted_connects' => 0, + + 'Binlog_cache_disk_use' => 0, + + 'Created_tmp_disk_tables' => 0, + + 'Handler_read_rnd' => 0, + 'Handler_read_rnd_next' => 0, + + 'Innodb_buffer_pool_pages_dirty' => 0, + 'Innodb_buffer_pool_reads' => 0, + 'Innodb_buffer_pool_wait_free' => 0, + 'Innodb_log_waits' => 0, + 'Innodb_row_lock_time_avg' => 10, // ms + 'Innodb_row_lock_time_max' => 50, // ms + 'Innodb_row_lock_waits' => 0, + + 'Slow_queries' => 0, + 'Delayed_errors' => 0, + 'Select_full_join' => 0, + 'Select_range_check' => 0, + 'Sort_merge_passes' => 0, + 'Opened_tables' => 0, + 'Table_locks_waited' => 0, + 'Qcache_lowmem_prunes' => 0, + + 'Qcache_free_blocks' => + isset($this->data->status['Qcache_total_blocks']) + ? $this->data->status['Qcache_total_blocks'] / 5 + : 0, + 'Slow_launch_threads' => 0, + + // depends on Key_read_requests + // normally lower then 1:0.01 + 'Key_reads' => isset($this->data->status['Key_read_requests']) + ? (0.01 * $this->data->status['Key_read_requests']) : 0, + // depends on Key_write_requests + // normally nearly 1:1 + 'Key_writes' => isset($this->data->status['Key_write_requests']) + ? (0.9 * $this->data->status['Key_write_requests']) : 0, + + 'Key_buffer_fraction' => 0.5, + + // alert if more than 95% of thread cache is in use + 'Threads_cached' => isset($this->data->variables['thread_cache_size']) + ? 0.95 * $this->data->variables['thread_cache_size'] : 0, + + // higher is better + // variable => min value + //'Handler read key' => '> ', + ]; + } + + /** + * Returns a list of variable descriptions + * + * @return array + */ + private function getDescriptions(): array + { + /** + * Messages are built using the message name + */ + return [ + 'Aborted_clients' => __( + 'The number of connections that were aborted because the client died' + . ' without closing the connection properly.' + ), + 'Aborted_connects' => __( + 'The number of failed attempts to connect to the MySQL server.' + ), + 'Binlog_cache_disk_use' => __( + 'The number of transactions that used the temporary binary log cache' + . ' but that exceeded the value of binlog_cache_size and used a' + . ' temporary file to store statements from the transaction.' + ), + 'Binlog_cache_use' => __( + 'The number of transactions that used the temporary binary log cache.' + ), + 'Connections' => __( + 'The number of connection attempts (successful or not)' + . ' to the MySQL server.' + ), + 'Created_tmp_disk_tables' => __( + 'The number of temporary tables on disk created automatically by' + . ' the server while executing statements. If' + . ' Created_tmp_disk_tables is big, you may want to increase the' + . ' tmp_table_size value to cause temporary tables to be' + . ' memory-based instead of disk-based.' + ), + 'Created_tmp_files' => __( + 'How many temporary files mysqld has created.' + ), + 'Created_tmp_tables' => __( + 'The number of in-memory temporary tables created automatically' + . ' by the server while executing statements.' + ), + 'Delayed_errors' => __( + 'The number of rows written with INSERT DELAYED for which some' + . ' error occurred (probably duplicate key).' + ), + 'Delayed_insert_threads' => __( + 'The number of INSERT DELAYED handler threads in use. Every' + . ' different table on which one uses INSERT DELAYED gets' + . ' its own thread.' + ), + 'Delayed_writes' => __( + 'The number of INSERT DELAYED rows written.' + ), + 'Flush_commands' => __( + 'The number of executed FLUSH statements.' + ), + 'Handler_commit' => __( + 'The number of internal COMMIT statements.' + ), + 'Handler_delete' => __( + 'The number of times a row was deleted from a table.' + ), + 'Handler_discover' => __( + 'The MySQL server can ask the NDB Cluster storage engine if it' + . ' knows about a table with a given name. This is called discovery.' + . ' Handler_discover indicates the number of time tables have been' + . ' discovered.' + ), + 'Handler_read_first' => __( + 'The number of times the first entry was read from an index. If this' + . ' is high, it suggests that the server is doing a lot of full' + . ' index scans; for example, SELECT col1 FROM foo, assuming that' + . ' col1 is indexed.' + ), + 'Handler_read_key' => __( + 'The number of requests to read a row based on a key. If this is' + . ' high, it is a good indication that your queries and tables' + . ' are properly indexed.' + ), + 'Handler_read_next' => __( + 'The number of requests to read the next row in key order. This is' + . ' incremented if you are querying an index column with a range' + . ' constraint or if you are doing an index scan.' + ), + 'Handler_read_prev' => __( + 'The number of requests to read the previous row in key order.' + . ' This read method is mainly used to optimize ORDER BY … DESC.' + ), + 'Handler_read_rnd' => __( + 'The number of requests to read a row based on a fixed position.' + . ' This is high if you are doing a lot of queries that require' + . ' sorting of the result. You probably have a lot of queries that' + . ' require MySQL to scan whole tables or you have joins that' + . ' don\'t use keys properly.' + ), + 'Handler_read_rnd_next' => __( + 'The number of requests to read the next row in the data file.' + . ' This is high if you are doing a lot of table scans. Generally' + . ' this suggests that your tables are not properly indexed or that' + . ' your queries are not written to take advantage of the indexes' + . ' you have.' + ), + 'Handler_rollback' => __( + 'The number of internal ROLLBACK statements.' + ), + 'Handler_update' => __( + 'The number of requests to update a row in a table.' + ), + 'Handler_write' => __( + 'The number of requests to insert a row in a table.' + ), + 'Innodb_buffer_pool_pages_data' => __( + 'The number of pages containing data (dirty or clean).' + ), + 'Innodb_buffer_pool_pages_dirty' => __( + 'The number of pages currently dirty.' + ), + 'Innodb_buffer_pool_pages_flushed' => __( + 'The number of buffer pool pages that have been requested' + . ' to be flushed.' + ), + 'Innodb_buffer_pool_pages_free' => __( + 'The number of free pages.' + ), + 'Innodb_buffer_pool_pages_latched' => __( + 'The number of latched pages in InnoDB buffer pool. These are pages' + . ' currently being read or written or that can\'t be flushed or' + . ' removed for some other reason.' + ), + 'Innodb_buffer_pool_pages_misc' => __( + 'The number of pages busy because they have been allocated for' + . ' administrative overhead such as row locks or the adaptive' + . ' hash index. This value can also be calculated as' + . ' Innodb_buffer_pool_pages_total - Innodb_buffer_pool_pages_free' + . ' - Innodb_buffer_pool_pages_data.' + ), + 'Innodb_buffer_pool_pages_total' => __( + 'Total size of buffer pool, in pages.' + ), + 'Innodb_buffer_pool_read_ahead_rnd' => __( + 'The number of "random" read-aheads InnoDB initiated. This happens' + . ' when a query is to scan a large portion of a table but in' + . ' random order.' + ), + 'Innodb_buffer_pool_read_ahead_seq' => __( + 'The number of sequential read-aheads InnoDB initiated. This' + . ' happens when InnoDB does a sequential full table scan.' + ), + 'Innodb_buffer_pool_read_requests' => __( + 'The number of logical read requests InnoDB has done.' + ), + 'Innodb_buffer_pool_reads' => __( + 'The number of logical reads that InnoDB could not satisfy' + . ' from buffer pool and had to do a single-page read.' + ), + 'Innodb_buffer_pool_wait_free' => __( + 'Normally, writes to the InnoDB buffer pool happen in the' + . ' background. However, if it\'s necessary to read or create a page' + . ' and no clean pages are available, it\'s necessary to wait for' + . ' pages to be flushed first. This counter counts instances of' + . ' these waits. If the buffer pool size was set properly, this' + . ' value should be small.' + ), + 'Innodb_buffer_pool_write_requests' => __( + 'The number writes done to the InnoDB buffer pool.' + ), + 'Innodb_data_fsyncs' => __( + 'The number of fsync() operations so far.' + ), + 'Innodb_data_pending_fsyncs' => __( + 'The current number of pending fsync() operations.' + ), + 'Innodb_data_pending_reads' => __( + 'The current number of pending reads.' + ), + 'Innodb_data_pending_writes' => __( + 'The current number of pending writes.' + ), + 'Innodb_data_read' => __( + 'The amount of data read so far, in bytes.' + ), + 'Innodb_data_reads' => __( + 'The total number of data reads.' + ), + 'Innodb_data_writes' => __( + 'The total number of data writes.' + ), + 'Innodb_data_written' => __( + 'The amount of data written so far, in bytes.' + ), + 'Innodb_dblwr_pages_written' => __( + 'The number of pages that have been written for' + . ' doublewrite operations.' + ), + 'Innodb_dblwr_writes' => __( + 'The number of doublewrite operations that have been performed.' + ), + 'Innodb_log_waits' => __( + 'The number of waits we had because log buffer was too small and' + . ' we had to wait for it to be flushed before continuing.' + ), + 'Innodb_log_write_requests' => __( + 'The number of log write requests.' + ), + 'Innodb_log_writes' => __( + 'The number of physical writes to the log file.' + ), + 'Innodb_os_log_fsyncs' => __( + 'The number of fsync() writes done to the log file.' + ), + 'Innodb_os_log_pending_fsyncs' => __( + 'The number of pending log file fsyncs.' + ), + 'Innodb_os_log_pending_writes' => __( + 'Pending log file writes.' + ), + 'Innodb_os_log_written' => __( + 'The number of bytes written to the log file.' + ), + 'Innodb_pages_created' => __( + 'The number of pages created.' + ), + 'Innodb_page_size' => __( + 'The compiled-in InnoDB page size (default 16KB). Many values are' + . ' counted in pages; the page size allows them to be easily' + . ' converted to bytes.' + ), + 'Innodb_pages_read' => __( + 'The number of pages read.' + ), + 'Innodb_pages_written' => __( + 'The number of pages written.' + ), + 'Innodb_row_lock_current_waits' => __( + 'The number of row locks currently being waited for.' + ), + 'Innodb_row_lock_time_avg' => __( + 'The average time to acquire a row lock, in milliseconds.' + ), + 'Innodb_row_lock_time' => __( + 'The total time spent in acquiring row locks, in milliseconds.' + ), + 'Innodb_row_lock_time_max' => __( + 'The maximum time to acquire a row lock, in milliseconds.' + ), + 'Innodb_row_lock_waits' => __( + 'The number of times a row lock had to be waited for.' + ), + 'Innodb_rows_deleted' => __( + 'The number of rows deleted from InnoDB tables.' + ), + 'Innodb_rows_inserted' => __( + 'The number of rows inserted in InnoDB tables.' + ), + 'Innodb_rows_read' => __( + 'The number of rows read from InnoDB tables.' + ), + 'Innodb_rows_updated' => __( + 'The number of rows updated in InnoDB tables.' + ), + 'Key_blocks_not_flushed' => __( + 'The number of key blocks in the key cache that have changed but' + . ' haven\'t yet been flushed to disk. It used to be known as' + . ' Not_flushed_key_blocks.' + ), + 'Key_blocks_unused' => __( + 'The number of unused blocks in the key cache. You can use this' + . ' value to determine how much of the key cache is in use.' + ), + 'Key_blocks_used' => __( + 'The number of used blocks in the key cache. This value is a' + . ' high-water mark that indicates the maximum number of blocks' + . ' that have ever been in use at one time.' + ), + 'Key_buffer_fraction_%' => __( + 'Percentage of used key cache (calculated value)' + ), + 'Key_read_requests' => __( + 'The number of requests to read a key block from the cache.' + ), + 'Key_reads' => __( + 'The number of physical reads of a key block from disk. If Key_reads' + . ' is big, then your key_buffer_size value is probably too small.' + . ' The cache miss rate can be calculated as' + . ' Key_reads/Key_read_requests.' + ), + 'Key_read_ratio_%' => __( + 'Key cache miss calculated as rate of physical reads compared' + . ' to read requests (calculated value)' + ), + 'Key_write_requests' => __( + 'The number of requests to write a key block to the cache.' + ), + 'Key_writes' => __( + 'The number of physical writes of a key block to disk.' + ), + 'Key_write_ratio_%' => __( + 'Percentage of physical writes compared' + . ' to write requests (calculated value)' + ), + 'Last_query_cost' => __( + 'The total cost of the last compiled query as computed by the query' + . ' optimizer. Useful for comparing the cost of different query' + . ' plans for the same query. The default value of 0 means that' + . ' no query has been compiled yet.' + ), + 'Max_used_connections' => __( + 'The maximum number of connections that have been in use' + . ' simultaneously since the server started.' + ), + 'Not_flushed_delayed_rows' => __( + 'The number of rows waiting to be written in INSERT DELAYED queues.' + ), + 'Opened_tables' => __( + 'The number of tables that have been opened. If opened tables is' + . ' big, your table cache value is probably too small.' + ), + 'Open_files' => __( + 'The number of files that are open.' + ), + 'Open_streams' => __( + 'The number of streams that are open (used mainly for logging).' + ), + 'Open_tables' => __( + 'The number of tables that are open.' + ), + 'Qcache_free_blocks' => __( + 'The number of free memory blocks in query cache. High numbers can' + . ' indicate fragmentation issues, which may be solved by issuing' + . ' a FLUSH QUERY CACHE statement.' + ), + 'Qcache_free_memory' => __( + 'The amount of free memory for query cache.' + ), + 'Qcache_hits' => __( + 'The number of cache hits.' + ), + 'Qcache_inserts' => __( + 'The number of queries added to the cache.' + ), + 'Qcache_lowmem_prunes' => __( + 'The number of queries that have been removed from the cache to' + . ' free up memory for caching new queries. This information can' + . ' help you tune the query cache size. The query cache uses a' + . ' least recently used (LRU) strategy to decide which queries' + . ' to remove from the cache.' + ), + 'Qcache_not_cached' => __( + 'The number of non-cached queries (not cachable, or not cached' + . ' due to the query_cache_type setting).' + ), + 'Qcache_queries_in_cache' => __( + 'The number of queries registered in the cache.' + ), + 'Qcache_total_blocks' => __( + 'The total number of blocks in the query cache.' + ), + 'Rpl_status' => __( + 'The status of failsafe replication (not yet implemented).' + ), + 'Select_full_join' => __( + 'The number of joins that do not use indexes. If this value is' + . ' not 0, you should carefully check the indexes of your tables.' + ), + 'Select_full_range_join' => __( + 'The number of joins that used a range search on a reference table.' + ), + 'Select_range_check' => __( + 'The number of joins without keys that check for key usage after' + . ' each row. (If this is not 0, you should carefully check the' + . ' indexes of your tables.)' + ), + 'Select_range' => __( + 'The number of joins that used ranges on the first table. (It\'s' + . ' normally not critical even if this is big.)' + ), + 'Select_scan' => __( + 'The number of joins that did a full scan of the first table.' + ), + 'Slave_open_temp_tables' => __( + 'The number of temporary tables currently' + . ' open by the slave SQL thread.' + ), + 'Slave_retried_transactions' => __( + 'Total (since startup) number of times the replication slave SQL' + . ' thread has retried transactions.' + ), + 'Slave_running' => __( + 'This is ON if this server is a slave that is connected to a master.' + ), + 'Slow_launch_threads' => __( + 'The number of threads that have taken more than slow_launch_time' + . ' seconds to create.' + ), + 'Slow_queries' => __( + 'The number of queries that have taken more than long_query_time' + . ' seconds.' + ), + 'Sort_merge_passes' => __( + 'The number of merge passes the sort algorithm has had to do.' + . ' If this value is large, you should consider increasing the' + . ' value of the sort_buffer_size system variable.' + ), + 'Sort_range' => __( + 'The number of sorts that were done with ranges.' + ), + 'Sort_rows' => __( + 'The number of sorted rows.' + ), + 'Sort_scan' => __( + 'The number of sorts that were done by scanning the table.' + ), + 'Table_locks_immediate' => __( + 'The number of times that a table lock was acquired immediately.' + ), + 'Table_locks_waited' => __( + 'The number of times that a table lock could not be acquired' + . ' immediately and a wait was needed. If this is high, and you have' + . ' performance problems, you should first optimize your queries,' + . ' and then either split your table or tables or use replication.' + ), + 'Threads_cached' => __( + 'The number of threads in the thread cache. The cache hit rate can' + . ' be calculated as Threads_created/Connections. If this value is' + . ' red you should raise your thread_cache_size.' + ), + 'Threads_connected' => __( + 'The number of currently open connections.' + ), + 'Threads_created' => __( + 'The number of threads created to handle connections. If' + . ' Threads_created is big, you may want to increase the' + . ' thread_cache_size value. (Normally this doesn\'t give a notable' + . ' performance improvement if you have a good thread' + . ' implementation.)' + ), + 'Threads_cache_hitrate_%' => __( + 'Thread cache hit rate (calculated value)' + ), + 'Threads_running' => __( + 'The number of threads that are not sleeping.' + ), + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/VariablesController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/VariablesController.php new file mode 100644 index 0000000..ad10ec8 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/VariablesController.php @@ -0,0 +1,238 @@ +response->getHeader(); + $scripts = $header->getScripts(); + $scripts->addFile('server/variables.js'); + + $variables = []; + $serverVarsResult = $this->dbi->tryQuery('SHOW SESSION VARIABLES;'); + if ($serverVarsResult !== false) { + $serverVarsSession = []; + while ($arr = $this->dbi->fetchRow($serverVarsResult)) { + $serverVarsSession[$arr[0]] = $arr[1]; + } + $this->dbi->freeResult($serverVarsResult); + + $serverVars = $this->dbi->fetchResult('SHOW GLOBAL VARIABLES;', 0, 1); + + // list of static (i.e. non-editable) system variables + $staticVariables = KBSearch::getStaticVariables(); + + foreach ($serverVars as $name => $value) { + $hasSessionValue = isset($serverVarsSession[$name]) + && $serverVarsSession[$name] !== $value; + $docLink = Util::linkToVarDocumentation( + $name, + $this->dbi->isMariaDB(), + str_replace('_', ' ', $name) + ); + + list($formattedValue, $isEscaped) = $this->formatVariable($name, $value); + if ($hasSessionValue) { + list($sessionFormattedValue, ) = $this->formatVariable( + $name, + $serverVarsSession[$name] + ); + } + + $variables[] = [ + 'name' => $name, + 'is_editable' => ! in_array(strtolower($name), $staticVariables), + 'doc_link' => $docLink, + 'value' => $formattedValue, + 'is_escaped' => $isEscaped, + 'has_session_value' => $hasSessionValue, + 'session_value' => $sessionFormattedValue ?? null, + ]; + } + } + + return $this->template->render('server/variables/index', [ + 'variables' => $variables, + 'filter_value' => $filterValue, + 'is_superuser' => $this->dbi->isSuperuser(), + 'is_mariadb' => $this->dbi->isMariaDB(), + ]); + } + + /** + * Handle the AJAX request for a single variable value + * + * @param array $params Request parameters + * + * @return array + */ + public function getValue(array $params): array + { + // Send with correct charset + header('Content-Type: text/html; charset=UTF-8'); + // Do not use double quotes inside the query to avoid a problem + // when server is running in ANSI_QUOTES sql_mode + $varValue = $this->dbi->fetchSingleRow( + 'SHOW GLOBAL VARIABLES WHERE Variable_name=\'' + . $this->dbi->escapeString($params['varName']) . '\';', + 'NUM' + ); + + $json = []; + try { + $type = KBSearch::getVariableType($params['varName']); + if ($type === 'byte') { + $json['message'] = implode( + ' ', + Util::formatByteDown($varValue[1], 3, 3) + ); + } else { + throw new KBException("Not a type=byte"); + } + } catch (KBException $e) { + $json['message'] = $varValue[1]; + } + + return $json; + } + + /** + * Handle the AJAX request for setting value for a single variable + * + * @param array $params Request parameters + * + * @return array + */ + public function setValue(array $params): array + { + $value = $params['varValue']; + $matches = []; + try { + $type = KBSearch::getVariableType($params['varName']); + if ($type === 'byte' && preg_match( + '/^\s*(\d+(\.\d+)?)\s*(mb|kb|mib|kib|gb|gib)\s*$/i', + $value, + $matches + )) { + $exp = [ + 'kb' => 1, + 'kib' => 1, + 'mb' => 2, + 'mib' => 2, + 'gb' => 3, + 'gib' => 3, + ]; + $value = floatval($matches[1]) * pow( + 1024, + $exp[mb_strtolower($matches[3])] + ); + } else { + throw new KBException("Not a type=byte or regex not matching"); + } + } catch (KBException $e) { + $value = $this->dbi->escapeString($value); + } + + if (! is_numeric($value)) { + $value = "'" . $value . "'"; + } + + $json = []; + if (! preg_match("/[^a-zA-Z0-9_]+/", $params['varName']) + && $this->dbi->query( + 'SET GLOBAL ' . $params['varName'] . ' = ' . $value + ) + ) { + // Some values are rounded down etc. + $varValue = $this->dbi->fetchSingleRow( + 'SHOW GLOBAL VARIABLES WHERE Variable_name="' + . $this->dbi->escapeString($params['varName']) + . '";', + 'NUM' + ); + list($formattedValue, $isHtmlFormatted) = $this->formatVariable( + $params['varName'], + $varValue[1] + ); + + if ($isHtmlFormatted === false) { + $json['variable'] = htmlspecialchars($formattedValue); + } else { + $json['variable'] = $formattedValue; + } + } else { + $this->response->setRequestStatus(false); + $json['error'] = __('Setting variable failed'); + } + + return $json; + } + + /** + * Format Variable + * + * @param string $name variable name + * @param integer $value variable value + * + * @return array formatted string and bool if string is HTML formatted + */ + private function formatVariable($name, $value) + { + $isHtmlFormatted = false; + $formattedValue = $value; + + if (is_numeric($value)) { + try { + $type = KBSearch::getVariableType($name); + if ($type === 'byte') { + $isHtmlFormatted = true; + $formattedValue = '' + . htmlspecialchars( + implode(' ', Util::formatByteDown($value, 3, 3)) + ) + . ''; + } else { + throw new KBException("Not a type=byte or regex not matching"); + } + } catch (KBException $e) { + $formattedValue = Util::formatNumber($value, 0); + } + } + + return [ + $formattedValue, + $isHtmlFormatted, + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Setup/AbstractController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/AbstractController.php new file mode 100644 index 0000000..8dae377 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/AbstractController.php @@ -0,0 +1,70 @@ +config = $config; + $this->template = $template; + } + + /** + * @return array + */ + protected function getPages(): array + { + $ignored = [ + 'Config', + 'Servers', + ]; + $pages = []; + foreach (SetupFormList::getAll() as $formset) { + if (in_array($formset, $ignored)) { + continue; + } + /** @var BaseForm $formClass */ + $formClass = SetupFormList::get($formset); + + $pages[$formset] = [ + 'name' => $formClass::getName(), + 'formset' => $formset, + ]; + } + + return $pages; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Setup/ConfigController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/ConfigController.php new file mode 100644 index 0000000..f6e37a2 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/ConfigController.php @@ -0,0 +1,55 @@ +getPages(); + + $formDisplayTemplate = new FormDisplayTemplate($GLOBALS['PMA_Config']); + + $formTop = $formDisplayTemplate->displayFormTop('config.php'); + $fieldsetTop = $formDisplayTemplate->displayFieldsetTop( + 'config.inc.php', + '', + null, + ['class' => 'simple'] + ); + $formBottom = $formDisplayTemplate->displayFieldsetBottom(false); + $fieldsetBottom = $formDisplayTemplate->displayFormBottom(); + + $config = ConfigGenerator::getConfigFile($this->config); + + return $this->template->render('setup/config/index', [ + 'formset' => $params['formset'] ?? '', + 'pages' => $pages, + 'form_top_html' => $formTop, + 'fieldset_top_html' => $fieldsetTop, + 'form_bottom_html' => $formBottom, + 'fieldset_bottom_html' => $fieldsetBottom, + 'eol' => Core::ifSetOr($params['eol'], 'unix'), + 'config' => $config, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Setup/FormController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/FormController.php new file mode 100644 index 0000000..c2caf01 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/FormController.php @@ -0,0 +1,50 @@ +getPages(); + + $formset = Core::isValid($params['formset'], 'scalar') ? $params['formset'] : null; + + /** @var BaseForm $formClass */ + $formClass = SetupFormList::get($formset); + if ($formClass === null) { + Core::fatalError(__('Incorrect form specified!')); + } + + ob_start(); + FormProcessing::process(new $formClass($this->config)); + $page = ob_get_clean(); + + return $this->template->render('setup/form/index', [ + 'formset' => $params['formset'] ?? '', + 'pages' => $pages, + 'name' => $formClass::getName(), + 'page' => $page, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Setup/HomeController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/HomeController.php new file mode 100644 index 0000000..37e3ea2 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/HomeController.php @@ -0,0 +1,228 @@ +getPages(); + + // Handle done action info + $actionDone = Core::isValid($params['action_done'], 'scalar') ? $params['action_done'] : null; + $actionDone = preg_replace('/[^a-z_]/', '', $actionDone); + + // message handling + Index::messagesBegin(); + + // Check phpMyAdmin version + if (isset($params['version_check'])) { + Index::versionCheck(); + } + + // Perform various security, compatibility and consistency checks + $configChecker = new ServerConfigChecks($this->config); + $configChecker->performConfigChecks(); + + $text = __( + 'You are not using a secure connection; all data (including potentially ' + . 'sensitive information, like passwords) is transferred unencrypted!' + ); + $text .= ' '; + $text .= __( + 'If your server is also configured to accept HTTPS requests ' + . 'follow this link to use a secure connection.' + ); + $text .= ''; + Index::messagesSet('notice', 'no_https', __('Insecure connection'), $text); + + // Check for done action info and set notice message if present + switch ($actionDone) { + case 'config_saved': + /* Use uniqid to display this message every time configuration is saved */ + Index::messagesSet( + 'notice', + uniqid('config_saved'), + __('Configuration saved.'), + Sanitize::sanitizeMessage( + __( + 'Configuration saved to file config/config.inc.php in phpMyAdmin ' + . 'top level directory, copy it to top level one and delete ' + . 'directory config to use it.' + ) + ) + ); + break; + case 'config_not_saved': + /* Use uniqid to display this message every time configuration is saved */ + Index::messagesSet( + 'notice', + uniqid('config_not_saved'), + __('Configuration not saved!'), + Sanitize::sanitizeMessage( + __( + 'Please create web server writable folder [em]config[/em] in ' + . 'phpMyAdmin top level directory as described in ' + . '[doc@setup_script]documentation[/doc]. Otherwise you will be ' + . 'only able to download or display it.' + ) + ) + ); + break; + default: + break; + } + + Index::messagesEnd(); + $messages = Index::messagesShowHtml(); + + $formDisplay = new FormDisplay($this->config); + + $defaultLanguageOptions = [ + 'doc' => $formDisplay->getDocLink('DefaultLang'), + 'values' => [], + 'values_escaped' => true, + ]; + + // prepare unfiltered language list + $sortedLanguages = LanguageManager::getInstance()->sortedLanguages(); + $languages = []; + foreach ($sortedLanguages as $language) { + $languages[] = [ + 'code' => $language->getCode(), + 'name' => $language->getName(), + 'is_active' => $language->isActive(), + ]; + $defaultLanguageOptions['values'][$language->getCode()] = $language->getName(); + } + + $serverDefaultOptions = [ + 'doc' => $formDisplay->getDocLink('ServerDefault'), + 'values' => [], + 'values_disabled' => [], + ]; + + $servers = []; + if ($this->config->getServerCount() > 0) { + $serverDefaultOptions['values']['0'] = __('let the user choose'); + $serverDefaultOptions['values']['-'] = '------------------------------'; + if ($this->config->getServerCount() === 1) { + $serverDefaultOptions['values_disabled'][] = '0'; + } + $serverDefaultOptions['values_disabled'][] = '-'; + + foreach ($this->config->getServers() as $id => $server) { + $servers[$id] = [ + 'id' => $id, + 'name' => $this->config->getServerName($id), + 'auth_type' => $this->config->getValue("Servers/$id/auth_type"), + 'dsn' => $this->config->getServerDSN($id), + 'params' => [ + 'token' => $_SESSION[' PMA_token '], + 'edit' => [ + 'page' => 'servers', + 'mode' => 'edit', + 'id' => $id, + ], + 'remove' => [ + 'page' => 'servers', + 'mode' => 'remove', + 'id' => $id, + ], + ], + ]; + $serverDefaultOptions['values'][(string) $id] = $this->config->getServerName($id) . " [$id]"; + } + } else { + $serverDefaultOptions['values']['1'] = __('- none -'); + $serverDefaultOptions['values_escaped'] = true; + } + + $formDisplayTemplate = new FormDisplayTemplate($GLOBALS['PMA_Config']); + $serversFormTopHtml = $formDisplayTemplate->displayFormTop( + 'index.php', + 'get', + [ + 'page' => 'servers', + 'mode' => 'add', + ] + ); + $configFormTopHtml = $formDisplayTemplate->displayFormTop('config.php'); + $formBottomHtml = $formDisplayTemplate->displayFormBottom(); + + $defaultLanguageInput = $formDisplayTemplate->displayInput( + 'DefaultLang', + __('Default language'), + 'select', + $this->config->getValue('DefaultLang'), + '', + true, + $defaultLanguageOptions + ); + $serverDefaultInput = $formDisplayTemplate->displayInput( + 'ServerDefault', + __('Default server'), + 'select', + $this->config->getValue('ServerDefault'), + '', + true, + $serverDefaultOptions + ); + + $eolOptions = [ + 'values' => [ + 'unix' => 'UNIX / Linux (\n)', + 'win' => 'Windows (\r\n)', + ], + 'values_escaped' => true, + ]; + $eol = Core::ifSetOr($_SESSION['eol'], (PMA_IS_WINDOWS ? 'win' : 'unix')); + $eolInput = $formDisplayTemplate->displayInput( + 'eol', + __('End of line'), + 'select', + $eol, + '', + true, + $eolOptions + ); + + return $this->template->render('setup/home/index', [ + 'formset' => $params['formset'] ?? '', + 'languages' => $languages, + 'messages' => $messages, + 'servers_form_top_html' => $serversFormTopHtml, + 'config_form_top_html' => $configFormTopHtml, + 'form_bottom_html' => $formBottomHtml, + 'server_count' => $this->config->getServerCount(), + 'servers' => $servers, + 'default_language_input' => $defaultLanguageInput, + 'server_default_input' => $serverDefaultInput, + 'eol_input' => $eolInput, + 'pages' => $pages, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Setup/ServersController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/ServersController.php new file mode 100644 index 0000000..eedb94e --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/ServersController.php @@ -0,0 +1,66 @@ +getPages(); + + $id = Core::isValid($params['id'], 'numeric') ? (int) $params['id'] : null; + $hasServer = ! empty($id) && $this->config->get("Servers/$id") !== null; + + if (! $hasServer && ($params['mode'] !== 'revert' && $params['mode'] !== 'edit')) { + $id = 0; + } + + ob_start(); + FormProcessing::process(new ServersForm($this->config, $id)); + $page = ob_get_clean(); + + return $this->template->render('setup/servers/index', [ + 'formset' => $params['formset'] ?? '', + 'pages' => $pages, + 'has_server' => $hasServer, + 'mode' => $params['mode'], + 'server_id' => $id, + 'server_dsn' => $this->config->getServerDSN($id), + 'page' => $page, + ]); + } + + /** + * @param array $params Request parameters + * @return void + */ + public function destroy(array $params): void + { + $id = Core::isValid($params['id'], 'numeric') ? (int) $params['id'] : null; + + $hasServer = ! empty($id) && $this->config->get("Servers/$id") !== null; + + if ($hasServer) { + $this->config->removeServer($id); + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Table/AbstractController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Table/AbstractController.php new file mode 100644 index 0000000..35f01ac --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Table/AbstractController.php @@ -0,0 +1,54 @@ +db = $db; + $this->table = $table; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Table/ChartController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Table/ChartController.php new file mode 100644 index 0000000..b2c4176 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Table/ChartController.php @@ -0,0 +1,261 @@ +sql_query = $sql_query; + $this->url_query = $url_query; + $this->cfg = $cfg; + } + + /** + * Execute the query and return the result + * + * @return void + */ + public function indexAction() + { + $response = Response::getInstance(); + if ($response->isAjax() + && isset($_REQUEST['pos']) + && isset($_REQUEST['session_max_rows']) + ) { + $this->ajaxAction(); + return; + } + + // Throw error if no sql query is set + if (! isset($this->sql_query) || $this->sql_query == '') { + $this->response->setRequestStatus(false); + $this->response->addHTML( + Message::error(__('No SQL query was set to fetch data.')) + ); + return; + } + + $this->response->getHeader()->getScripts()->addFiles( + [ + 'chart.js', + 'table/chart.js', + 'vendor/jqplot/jquery.jqplot.js', + 'vendor/jqplot/plugins/jqplot.barRenderer.js', + 'vendor/jqplot/plugins/jqplot.canvasAxisLabelRenderer.js', + 'vendor/jqplot/plugins/jqplot.canvasTextRenderer.js', + 'vendor/jqplot/plugins/jqplot.categoryAxisRenderer.js', + 'vendor/jqplot/plugins/jqplot.dateAxisRenderer.js', + 'vendor/jqplot/plugins/jqplot.pointLabels.js', + 'vendor/jqplot/plugins/jqplot.pieRenderer.js', + 'vendor/jqplot/plugins/jqplot.enhancedPieLegendRenderer.js', + 'vendor/jqplot/plugins/jqplot.highlighter.js', + ] + ); + + /** + * Extract values for common work + * @todo Extract common files + */ + $db = &$this->db; + $table = &$this->table; + $url_params = []; + + /** + * Runs common work + */ + if (strlen($this->table) > 0) { + $url_params['goto'] = Util::getScriptNameForOption( + $this->cfg['DefaultTabTable'], + 'table' + ); + $url_params['back'] = 'tbl_sql.php'; + include ROOT_PATH . 'libraries/tbl_common.inc.php'; + $this->dbi->selectDb($GLOBALS['db']); + } elseif (strlen($this->db) > 0) { + $url_params['goto'] = Util::getScriptNameForOption( + $this->cfg['DefaultTabDatabase'], + 'database' + ); + $url_params['back'] = 'sql.php'; + include ROOT_PATH . 'libraries/db_common.inc.php'; + } else { + $url_params['goto'] = Util::getScriptNameForOption( + $this->cfg['DefaultTabServer'], + 'server' + ); + $url_params['back'] = 'sql.php'; + include ROOT_PATH . 'libraries/server_common.inc.php'; + } + + $data = []; + + $result = $this->dbi->tryQuery($this->sql_query); + $fields_meta = $this->dbi->getFieldsMeta($result); + while ($row = $this->dbi->fetchAssoc($result)) { + $data[] = $row; + } + + $keys = array_keys($data[0]); + + $numeric_types = [ + 'int', + 'real', + ]; + $numeric_column_count = 0; + foreach ($keys as $idx => $key) { + if (in_array($fields_meta[$idx]->type, $numeric_types)) { + $numeric_column_count++; + } + } + + if ($numeric_column_count == 0) { + $this->response->setRequestStatus(false); + $this->response->addJSON( + 'message', + __('No numeric columns present in the table to plot.') + ); + return; + } + + $url_params['db'] = $this->db; + $url_params['reload'] = 1; + + /** + * Displays the page + */ + $this->response->addHTML( + $this->template->render('table/chart/tbl_chart', [ + 'url_query' => $this->url_query, + 'url_params' => $url_params, + 'keys' => $keys, + 'fields_meta' => $fields_meta, + 'numeric_types' => $numeric_types, + 'numeric_column_count' => $numeric_column_count, + 'sql_query' => $this->sql_query, + ]) + ); + } + + /** + * Handle ajax request + * + * @return void + */ + public function ajaxAction() + { + /** + * Extract values for common work + * @todo Extract common files + */ + $db = &$this->db; + $table = &$this->table; + + if (strlen($this->table) > 0 && strlen($this->db) > 0) { + include ROOT_PATH . 'libraries/tbl_common.inc.php'; + } + + $parser = new Parser($this->sql_query); + /** + * @var SelectStatement $statement + */ + $statement = $parser->statements[0]; + if (empty($statement->limit)) { + $statement->limit = new Limit( + $_REQUEST['session_max_rows'], + $_REQUEST['pos'] + ); + } else { + $start = $statement->limit->offset + $_REQUEST['pos']; + $rows = min( + $_REQUEST['session_max_rows'], + $statement->limit->rowCount - $_REQUEST['pos'] + ); + $statement->limit = new Limit($rows, $start); + } + $sql_with_limit = $statement->build(); + + $data = []; + $result = $this->dbi->tryQuery($sql_with_limit); + while ($row = $this->dbi->fetchAssoc($result)) { + $data[] = $row; + } + + if (empty($data)) { + $this->response->setRequestStatus(false); + $this->response->addJSON('message', __('No data to display')); + return; + } + $sanitized_data = []; + + foreach ($data as $data_row_number => $data_row) { + $tmp_row = []; + foreach ($data_row as $data_column => $data_value) { + $escaped_value = $data_value === null ? null : htmlspecialchars($data_value); + $tmp_row[htmlspecialchars($data_column)] = $escaped_value; + } + $sanitized_data[] = $tmp_row; + } + $this->response->setRequestStatus(true); + $this->response->addJSON('message', null); + $this->response->addJSON('chartData', json_encode($sanitized_data)); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Table/GisVisualizationController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Table/GisVisualizationController.php new file mode 100644 index 0000000..18e844f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Table/GisVisualizationController.php @@ -0,0 +1,227 @@ +sql_query = $sql_query; + $this->url_params = $url_params; + $this->url_params['goto'] = $goto; + $this->url_params['back'] = $back; + $this->visualizationSettings = $visualizationSettings; + } + + /** + * Save to file + * + * @return void + */ + public function saveToFileAction() + { + $this->response->disable(); + $file_name = $this->visualizationSettings['spatialColumn']; + $save_format = $_GET['fileFormat']; + $this->visualization->toFile($file_name, $save_format); + } + + /** + * Index + * + * @return void + */ + public function indexAction() + { + // Throw error if no sql query is set + if (! isset($this->sql_query) || $this->sql_query == '') { + $this->response->setRequestStatus(false); + $this->response->addHTML( + Message::error(__('No SQL query was set to fetch data.')) + ); + return; + } + + // Execute the query and return the result + $result = $this->dbi->tryQuery($this->sql_query); + // Get the meta data of results + $meta = $this->dbi->getFieldsMeta($result); + + // Find the candidate fields for label column and spatial column + $labelCandidates = []; + $spatialCandidates = []; + foreach ($meta as $column_meta) { + if ($column_meta->type == 'geometry') { + $spatialCandidates[] = $column_meta->name; + } else { + $labelCandidates[] = $column_meta->name; + } + } + + // Get settings if any posted + if (Core::isValid($_POST['visualizationSettings'], 'array')) { + $this->visualizationSettings = $_POST['visualizationSettings']; + } + + // Check mysql version + $this->visualizationSettings['mysqlVersion'] = $this->dbi->getVersion(); + + if (! isset($this->visualizationSettings['labelColumn']) + && isset($labelCandidates[0]) + ) { + $this->visualizationSettings['labelColumn'] = ''; + } + + // If spatial column is not set, use first geometric column as spatial column + if (! isset($this->visualizationSettings['spatialColumn'])) { + $this->visualizationSettings['spatialColumn'] = $spatialCandidates[0]; + } + + // Convert geometric columns from bytes to text. + $pos = isset($_GET['pos']) ? $_GET['pos'] + : $_SESSION['tmpval']['pos']; + if (isset($_GET['session_max_rows'])) { + $rows = $_GET['session_max_rows']; + } else { + if ($_SESSION['tmpval']['max_rows'] != 'all') { + $rows = $_SESSION['tmpval']['max_rows']; + } else { + $rows = $GLOBALS['cfg']['MaxRows']; + } + } + $this->visualization = GisVisualization::get( + $this->sql_query, + $this->visualizationSettings, + $rows, + $pos + ); + + if (isset($_GET['saveToFile'])) { + $this->saveToFileAction(); + return; + } + + $this->response->getHeader()->getScripts()->addFiles( + [ + 'vendor/openlayers/OpenLayers.js', + 'vendor/jquery/jquery.svg.js', + 'table/gis_visualization.js', + ] + ); + + // If all the rows contain SRID, use OpenStreetMaps on the initial loading. + if (! isset($_POST['displayVisualization'])) { + if ($this->visualization->hasSrid()) { + $this->visualizationSettings['choice'] = 'useBaseLayer'; + } else { + unset($this->visualizationSettings['choice']); + } + } + + $this->visualization->setUserSpecifiedSettings($this->visualizationSettings); + if ($this->visualizationSettings != null) { + foreach ($this->visualization->getSettings() as $setting => $val) { + if (! isset($this->visualizationSettings[$setting])) { + $this->visualizationSettings[$setting] = $val; + } + } + } + + /** + * Displays the page + */ + $this->url_params['sql_query'] = $this->sql_query; + $downloadUrl = 'tbl_gis_visualization.php' . Url::getCommon( + array_merge( + $this->url_params, + [ + 'saveToFile' => true, + 'session_max_rows' => $rows, + 'pos' => $pos, + ] + ) + ); + $html = $this->template->render('table/gis_visualization/gis_visualization', [ + 'url_params' => $this->url_params, + 'download_url' => $downloadUrl, + 'label_candidates' => $labelCandidates, + 'spatial_candidates' => $spatialCandidates, + 'visualization_settings' => $this->visualizationSettings, + 'sql_query' => $this->sql_query, + 'visualization' => $this->visualization->toImage('svg'), + 'draw_ol' => $this->visualization->asOl(), + 'pma_theme_image' => $GLOBALS['pmaThemeImage'], + ]); + + $this->response->addHTML($html); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Table/IndexesController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Table/IndexesController.php new file mode 100644 index 0000000..cdbfbb9 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Table/IndexesController.php @@ -0,0 +1,179 @@ +index = $index; + } + + /** + * Index + * + * @return void + */ + public function indexAction() + { + if (isset($_POST['do_save_data'])) { + $this->doSaveDataAction(); + return; + } // end builds the new index + + $this->displayFormAction(); + } + + /** + * Display the form to edit/create an index + * + * @return void + */ + public function displayFormAction() + { + $this->dbi->selectDb($GLOBALS['db']); + $add_fields = 0; + if (isset($_POST['index']) && is_array($_POST['index'])) { + // coming already from form + if (isset($_POST['index']['columns']['names'])) { + $add_fields = count($_POST['index']['columns']['names']) + - $this->index->getColumnCount(); + } + if (isset($_POST['add_fields'])) { + $add_fields += $_POST['added_fields']; + } + } elseif (isset($_POST['create_index'])) { + $add_fields = $_POST['added_fields']; + } // end preparing form values + + // Get fields and stores their name/type + if (isset($_POST['create_edit_table'])) { + $fields = json_decode($_POST['columns'], true); + $index_params = [ + 'Non_unique' => $_POST['index']['Index_choice'] == 'UNIQUE' + ? '0' : '1', + ]; + $this->index->set($index_params); + $add_fields = count($fields); + } else { + $fields = $this->dbi->getTable($this->db, $this->table) + ->getNameAndTypeOfTheColumns(); + } + + $form_params = [ + 'db' => $this->db, + 'table' => $this->table, + ]; + + if (isset($_POST['create_index'])) { + $form_params['create_index'] = 1; + } elseif (isset($_POST['old_index'])) { + $form_params['old_index'] = $_POST['old_index']; + } elseif (isset($_POST['index'])) { + $form_params['old_index'] = $_POST['index']; + } + + $this->response->getHeader()->getScripts()->addFile('indexes.js'); + + $this->response->addHTML( + $this->template->render('table/index_form', [ + 'fields' => $fields, + 'index' => $this->index, + 'form_params' => $form_params, + 'add_fields' => $add_fields, + 'create_edit_table' => isset($_POST['create_edit_table']), + 'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'], + ]) + ); + } + + /** + * Process the data from the edit/create index form, + * run the query to build the new index + * and moves back to "tbl_sql.php" + * + * @return void + */ + public function doSaveDataAction() + { + $error = false; + + $sql_query = $this->dbi->getTable($this->db, $this->table) + ->getSqlQueryForIndexCreateOrEdit($this->index, $error); + + // If there is a request for SQL previewing. + if (isset($_POST['preview_sql'])) { + $this->response->addJSON( + 'sql_data', + $this->template->render('preview_sql', ['query_data' => $sql_query]) + ); + } elseif (! $error) { + $this->dbi->query($sql_query); + $response = Response::getInstance(); + if ($response->isAjax()) { + $message = Message::success( + __('Table %1$s has been altered successfully.') + ); + $message->addParam($this->table); + $this->response->addJSON( + 'message', + Util::getMessage($message, $sql_query, 'success') + ); + $this->response->addJSON( + 'index_table', + Index::getHtmlForIndexes( + $this->table, + $this->db + ) + ); + } else { + include ROOT_PATH . 'tbl_structure.php'; + } + } else { + $this->response->setRequestStatus(false); + $this->response->addJSON('message', $error); + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Table/RelationController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Table/RelationController.php new file mode 100644 index 0000000..558842c --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Table/RelationController.php @@ -0,0 +1,398 @@ +options_array = $options_array; + $this->cfgRelation = $cfgRelation; + $this->tbl_storage_engine = $tbl_storage_engine; + $this->existrel = $existrel; + $this->existrel_foreign = $existrel_foreign; + $this->upd_query = $upd_query; + $this->relation = $relation; + } + + /** + * Index + * + * @return void + */ + public function indexAction() + { + // Send table of column names to populate corresponding dropdowns depending + // on the current selection + if (isset($_POST['getDropdownValues']) + && $_POST['getDropdownValues'] === 'true' + ) { + // if both db and table are selected + if (isset($_POST['foreignTable'])) { + $this->getDropdownValueForTableAction(); + } else { // if only the db is selected + $this->getDropdownValueForDbAction(); + } + return; + } + + $this->response->getHeader()->getScripts()->addFiles( + [ + 'table/relation.js', + 'indexes.js', + ] + ); + + // Set the database + $this->dbi->selectDb($this->db); + + // updates for Internal relations + if (isset($_POST['destination_db']) && $this->cfgRelation['relwork']) { + $this->updateForInternalRelationAction(); + } + + // updates for foreign keys + $this->updateForForeignKeysAction(); + + // Updates for display field + if ($this->cfgRelation['displaywork'] && isset($_POST['display_field'])) { + $this->updateForDisplayField(); + } + + // If we did an update, refresh our data + if (isset($_POST['destination_db']) && $this->cfgRelation['relwork']) { + $this->existrel = $this->relation->getForeigners( + $this->db, + $this->table, + '', + 'internal' + ); + } + if (isset($_POST['destination_foreign_db']) + && Util::isForeignKeySupported($this->tbl_storage_engine) + ) { + $this->existrel_foreign = $this->relation->getForeigners( + $this->db, + $this->table, + '', + 'foreign' + ); + } + + /** + * Dialog + */ + // Now find out the columns of our $table + // need to use DatabaseInterface::QUERY_STORE with $this->dbi->numRows() + // in mysqli + $columns = $this->dbi->getColumns($this->db, $this->table); + + $column_array = []; + $column_hash_array = []; + $column_array[''] = ''; + foreach ($columns as $column) { + if (strtoupper($this->tbl_storage_engine) == 'INNODB' + || ! empty($column['Key']) + ) { + $column_array[$column['Field']] = $column['Field']; + $column_hash_array[$column['Field']] = md5($column['Field']); + } + } + if ($GLOBALS['cfg']['NaturalOrder']) { + uksort($column_array, 'strnatcasecmp'); + } + + // common form + $engine = $this->dbi->getTable($this->db, $this->table)->getStorageEngine(); + $foreignKeySupported = Util::isForeignKeySupported($this->tbl_storage_engine); + $this->response->addHTML( + $this->template->render('table/relation/common_form', [ + 'is_foreign_key_supported' => Util::isForeignKeySupported($engine), + 'db' => $this->db, + 'table' => $this->table, + 'cfg_relation' => $this->cfgRelation, + 'tbl_storage_engine' => $this->tbl_storage_engine, + 'existrel' => isset($this->existrel) ? $this->existrel : [], + 'existrel_foreign' => is_array($this->existrel_foreign) && array_key_exists('foreign_keys_data', $this->existrel_foreign) + ? $this->existrel_foreign['foreign_keys_data'] : [], + 'options_array' => $this->options_array, + 'column_array' => $column_array, + 'column_hash_array' => $column_hash_array, + 'save_row' => array_values($columns), + 'url_params' => $GLOBALS['url_params'], + 'databases' => $GLOBALS['dblist']->databases, + 'dbi' => $this->dbi, + 'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'], + 'foreignKeySupported' => $foreignKeySupported, + 'displayIndexesHtml' => $foreignKeySupported ? Index::getHtmlForDisplayIndexes() : null, + ]) + ); + } + + /** + * Update for display field + * + * @return void + */ + public function updateForDisplayField() + { + if ($this->upd_query->updateDisplayField( + $_POST['display_field'], + $this->cfgRelation + ) + ) { + $this->response->addHTML( + Util::getMessage( + __('Display column was successfully updated.'), + '', + 'success' + ) + ); + } + } + + /** + * Update for FK + * + * @return void + */ + public function updateForForeignKeysAction() + { + $multi_edit_columns_name = isset($_POST['foreign_key_fields_name']) + ? $_POST['foreign_key_fields_name'] + : null; + $preview_sql_data = ''; + $seen_error = false; + + // (for now, one index name only; we keep the definitions if the + // foreign db is not the same) + if (isset($_POST['destination_foreign_db']) + && isset($_POST['destination_foreign_table']) + && isset($_POST['destination_foreign_column'])) { + list($html, $preview_sql_data, $display_query, $seen_error) + = $this->upd_query->updateForeignKeys( + $_POST['destination_foreign_db'], + $multi_edit_columns_name, + $_POST['destination_foreign_table'], + $_POST['destination_foreign_column'], + $this->options_array, + $this->table, + is_array($this->existrel_foreign) && array_key_exists('foreign_keys_data', $this->existrel_foreign) + ? $this->existrel_foreign['foreign_keys_data'] : [] + ); + $this->response->addHTML($html); + } + + // If there is a request for SQL previewing. + if (isset($_POST['preview_sql'])) { + Core::previewSQL($preview_sql_data); + } + + if (! empty($display_query) && ! $seen_error) { + $GLOBALS['display_query'] = $display_query; + $this->response->addHTML( + Util::getMessage( + __('Your SQL query has been executed successfully.'), + null, + 'success' + ) + ); + } + } + + /** + * Update for internal relation + * + * @return void + */ + public function updateForInternalRelationAction() + { + $multi_edit_columns_name = isset($_POST['fields_name']) + ? $_POST['fields_name'] + : null; + + if ($this->upd_query->updateInternalRelations( + $multi_edit_columns_name, + $_POST['destination_db'], + $_POST['destination_table'], + $_POST['destination_column'], + $this->cfgRelation, + isset($this->existrel) ? $this->existrel : null + ) + ) { + $this->response->addHTML( + Util::getMessage( + __('Internal relationships were successfully updated.'), + '', + 'success' + ) + ); + } + } + + /** + * Send table columns for foreign table dropdown + * + * @return void + * + */ + public function getDropdownValueForTableAction() + { + $foreignTable = $_POST['foreignTable']; + $table_obj = $this->dbi->getTable($_POST['foreignDb'], $foreignTable); + // Since views do not have keys defined on them provide the full list of + // columns + if ($table_obj->isView()) { + $columnList = $table_obj->getColumns(false, false); + } else { + $columnList = $table_obj->getIndexedColumns(false, false); + } + $columns = []; + foreach ($columnList as $column) { + $columns[] = htmlspecialchars($column); + } + if ($GLOBALS['cfg']['NaturalOrder']) { + usort($columns, 'strnatcasecmp'); + } + $this->response->addJSON('columns', $columns); + + // @todo should be: $server->db($db)->table($table)->primary() + $primary = Index::getPrimary($foreignTable, $_POST['foreignDb']); + if (false === $primary) { + return; + } + + $this->response->addJSON('primary', array_keys($primary->getColumns())); + } + + /** + * Send database selection values for dropdown + * + * @return void + * + */ + public function getDropdownValueForDbAction() + { + $tables = []; + $foreign = isset($_POST['foreign']) && $_POST['foreign'] === 'true'; + + if ($foreign) { + $query = 'SHOW TABLE STATUS FROM ' + . Util::backquote($_POST['foreignDb']); + $tables_rs = $this->dbi->query( + $query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + + while ($row = $this->dbi->fetchArray($tables_rs)) { + if (isset($row['Engine']) + && mb_strtoupper($row['Engine']) == $this->tbl_storage_engine + ) { + $tables[] = htmlspecialchars($row['Name']); + } + } + } else { + $query = 'SHOW TABLES FROM ' + . Util::backquote($_POST['foreignDb']); + $tables_rs = $this->dbi->query( + $query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + while ($row = $this->dbi->fetchArray($tables_rs)) { + $tables[] = htmlspecialchars($row[0]); + } + } + if ($GLOBALS['cfg']['NaturalOrder']) { + usort($tables, 'strnatcasecmp'); + } + $this->response->addJSON('tables', $tables); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Table/SearchController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Table/SearchController.php new file mode 100644 index 0000000..3f2ceae --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Table/SearchController.php @@ -0,0 +1,1244 @@ +url_query = $url_query; + $this->_searchType = $searchType; + $this->_columnNames = []; + $this->_columnNullFlags = []; + $this->_columnTypes = []; + $this->_columnCollations = []; + $this->_geomColumnFlag = false; + $this->_foreigners = []; + $this->relation = $relation; + // Loads table's information + $this->_loadTableInfo(); + $this->_connectionCharSet = $this->dbi->fetchValue( + "SELECT @@character_set_connection" + ); + } + + /** + * Gets all the columns of a table along with their types, collations + * and whether null or not. + * + * @return void + */ + private function _loadTableInfo() + { + // Gets the list and number of columns + $columns = $this->dbi->getColumns( + $this->db, + $this->table, + null, + true + ); + // Get details about the geometry functions + $geom_types = Util::getGISDatatypes(); + + foreach ($columns as $row) { + // set column name + $this->_columnNames[] = $row['Field']; + + $type = $row['Type']; + // check whether table contains geometric columns + if (in_array($type, $geom_types)) { + $this->_geomColumnFlag = true; + } + // reformat mysql query output + if (strncasecmp($type, 'set', 3) == 0 + || strncasecmp($type, 'enum', 4) == 0 + ) { + $type = str_replace(',', ', ', $type); + } else { + // strip the "BINARY" attribute, except if we find "BINARY(" because + // this would be a BINARY or VARBINARY column type + if (! preg_match('@BINARY[\(]@i', $type)) { + $type = str_ireplace("BINARY", '', $type); + } + $type = str_ireplace("ZEROFILL", '', $type); + $type = str_ireplace("UNSIGNED", '', $type); + $type = mb_strtolower($type); + } + if (empty($type)) { + $type = ' '; + } + $this->_columnTypes[] = $type; + $this->_columnNullFlags[] = $row['Null']; + $this->_columnCollations[] + = ! empty($row['Collation']) && $row['Collation'] != 'NULL' + ? $row['Collation'] + : ''; + } // end for + + // Retrieve foreign keys + $this->_foreigners = $this->relation->getForeigners($this->db, $this->table); + } + + /** + * Index action + * + * @return void + */ + public function indexAction() + { + global $goto; + switch ($this->_searchType) { + case 'replace': + if (isset($_POST['find'])) { + $this->findAction(); + + return; + } + $this->response + ->getHeader() + ->getScripts() + ->addFile('table/find_replace.js'); + + if (isset($_POST['replace'])) { + $this->replaceAction(); + } + + // Displays the find and replace form + $this->displaySelectionFormAction(); + break; + + case 'normal': + $this->response->getHeader() + ->getScripts() + ->addFiles( + [ + 'makegrid.js', + 'sql.js', + 'table/select.js', + 'table/change.js', + 'vendor/jquery/jquery.uitablefilter.js', + 'gis_data_editor.js', + ] + ); + + if (isset($_POST['range_search'])) { + $this->rangeSearchAction(); + + return; + } + + /** + * No selection criteria received -> display the selection form + */ + if (! isset($_POST['columnsToDisplay']) + && ! isset($_POST['displayAllColumns']) + ) { + $this->displaySelectionFormAction(); + } else { + $this->doSelectionAction(); + } + break; + + case 'zoom': + $this->response->getHeader() + ->getScripts() + ->addFiles( + [ + 'makegrid.js', + 'sql.js', + 'vendor/jqplot/jquery.jqplot.js', + 'vendor/jqplot/plugins/jqplot.canvasTextRenderer.js', + 'vendor/jqplot/plugins/jqplot.canvasAxisLabelRenderer.js', + 'vendor/jqplot/plugins/jqplot.dateAxisRenderer.js', + 'vendor/jqplot/plugins/jqplot.highlighter.js', + 'vendor/jqplot/plugins/jqplot.cursor.js', + 'table/zoom_plot_jqplot.js', + 'table/change.js', + ] + ); + + /** + * Handle AJAX request for data row on point select + * + * @var boolean Object containing parameters for the POST request + */ + if (isset($_POST['get_data_row']) + && $_POST['get_data_row'] == true + ) { + $this->getDataRowAction(); + + return; + } + /** + * Handle AJAX request for changing field information + * (value,collation,operators,field values) in input form + * + * @var boolean Object containing parameters for the POST request + */ + if (isset($_POST['change_tbl_info']) + && $_POST['change_tbl_info'] == true + ) { + $this->changeTableInfoAction(); + + return; + } + + //Set default datalabel if not selected + if (! isset($_POST['zoom_submit']) || $_POST['dataLabel'] == '') { + $dataLabel = $this->relation->getDisplayField($this->db, $this->table); + } else { + $dataLabel = $_POST['dataLabel']; + } + + // Displays the zoom search form + $this->displaySelectionFormAction($dataLabel); + + /* + * Handle the input criteria and generate the query result + * Form for displaying query results + */ + if (isset($_POST['zoom_submit']) + && $_POST['criteriaColumnNames'][0] != 'pma_null' + && $_POST['criteriaColumnNames'][1] != 'pma_null' + && $_POST['criteriaColumnNames'][0] != $_POST['criteriaColumnNames'][1] + ) { + if (! isset($goto)) { + $goto = Util::getScriptNameForOption( + $GLOBALS['cfg']['DefaultTabTable'], + 'table' + ); + } + $this->zoomSubmitAction($dataLabel, $goto); + } + break; + } + } + + /** + * Zoom submit action + * + * @param string $dataLabel Data label + * @param string $goto Goto + * + * @return void + */ + public function zoomSubmitAction($dataLabel, $goto) + { + //Query generation part + $sql_query = $this->_buildSqlQuery(); + $sql_query .= ' LIMIT ' . $_POST['maxPlotLimit']; + + //Query execution part + $result = $this->dbi->query( + $sql_query . ";", + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + $fields_meta = $this->dbi->getFieldsMeta($result); + $data = []; + while ($row = $this->dbi->fetchAssoc($result)) { + //Need a row with indexes as 0,1,2 for the getUniqueCondition + // hence using a temporary array + $tmpRow = []; + foreach ($row as $val) { + $tmpRow[] = $val; + } + //Get unique condition on each row (will be needed for row update) + $uniqueCondition = Util::getUniqueCondition( + $result, // handle + count($this->_columnNames), // fields_cnt + $fields_meta, // fields_meta + $tmpRow, // row + true, // force_unique + false, // restrict_to_table + null // analyzed_sql_results + ); + //Append it to row array as where_clause + $row['where_clause'] = $uniqueCondition[0]; + + $tmpData = [ + $_POST['criteriaColumnNames'][0] => + $row[$_POST['criteriaColumnNames'][0]], + $_POST['criteriaColumnNames'][1] => + $row[$_POST['criteriaColumnNames'][1]], + 'where_clause' => $uniqueCondition[0], + ]; + $tmpData[$dataLabel] = $dataLabel ? $row[$dataLabel] : ''; + $data[] = $tmpData; + } + unset($tmpData); + + //Displays form for point data and scatter plot + $titles = [ + 'Browse' => Util::getIcon( + 'b_browse', + __('Browse foreign values') + ), + ]; + $column_names_hashes = []; + + foreach ($this->_columnNames as $columnName) { + $column_names_hashes[$columnName] = md5($columnName); + } + + $this->response->addHTML( + $this->template->render('table/search/zoom_result_form', [ + 'db' => $this->db, + 'table' => $this->table, + 'column_names' => $this->_columnNames, + 'column_names_hashes' => $column_names_hashes, + 'foreigners' => $this->_foreigners, + 'column_null_flags' => $this->_columnNullFlags, + 'column_types' => $this->_columnTypes, + 'titles' => $titles, + 'goto' => $goto, + 'data' => $data, + 'data_json' => json_encode($data), + 'zoom_submit' => isset($_POST['zoom_submit']), + 'foreign_max_limit' => $GLOBALS['cfg']['ForeignKeyMaxLimit'], + ]) + ); + } + + /** + * Change table info action + * + * @return void + */ + public function changeTableInfoAction() + { + $field = $_POST['field']; + if ($field == 'pma_null') { + $this->response->addJSON('field_type', ''); + $this->response->addJSON('field_collation', ''); + $this->response->addJSON('field_operators', ''); + $this->response->addJSON('field_value', ''); + return; + } + $key = array_search($field, $this->_columnNames); + $search_index + = (isset($_POST['it']) && is_numeric($_POST['it']) + ? intval($_POST['it']) : 0); + + $properties = $this->getColumnProperties($search_index, $key); + $this->response->addJSON( + 'field_type', + htmlspecialchars($properties['type']) + ); + $this->response->addJSON('field_collation', $properties['collation']); + $this->response->addJSON('field_operators', $properties['func']); + $this->response->addJSON('field_value', $properties['value']); + } + + /** + * Get data row action + * + * @return void + */ + public function getDataRowAction() + { + $extra_data = []; + $row_info_query = 'SELECT * FROM `' . $_POST['db'] . '`.`' + . $_POST['table'] . '` WHERE ' . $_POST['where_clause']; + $result = $this->dbi->query( + $row_info_query . ";", + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + $fields_meta = $this->dbi->getFieldsMeta($result); + while ($row = $this->dbi->fetchAssoc($result)) { + // for bit fields we need to convert them to printable form + $i = 0; + foreach ($row as $col => $val) { + if ($fields_meta[$i]->type == 'bit') { + $row[$col] = Util::printableBitValue( + (int) $val, + (int) $fields_meta[$i]->length + ); + } + $i++; + } + $extra_data['row_info'] = $row; + } + $this->response->addJSON($extra_data); + } + + /** + * Do selection action + * + * @return void + */ + public function doSelectionAction() + { + /** + * Selection criteria have been submitted -> do the work + */ + $sql_query = $this->_buildSqlQuery(); + + /** + * Add this to ensure following procedures included running correctly. + */ + $sql = new Sql(); + $sql->executeQueryAndSendQueryResponse( + null, // analyzed_sql_results + false, // is_gotofile + $this->db, // db + $this->table, // table + null, // find_real_end + null, // sql_query_for_bookmark + null, // extra_data + null, // message_to_show + null, // message + null, // sql_data + $GLOBALS['goto'], // goto + $GLOBALS['pmaThemeImage'], // pmaThemeImage + null, // disp_query + null, // disp_message + null, // query_type + $sql_query, // sql_query + null, // selectedTables + null // complete_query + ); + } + + /** + * Display selection form action + * + * @param string $dataLabel Data label + * + * @return void + */ + public function displaySelectionFormAction($dataLabel = null) + { + global $goto; + $this->url_query .= '&goto=tbl_select.php&back=tbl_select.php'; + if (! isset($goto)) { + $goto = Util::getScriptNameForOption( + $GLOBALS['cfg']['DefaultTabTable'], + 'table' + ); + } + // Displays the table search form + $this->response->addHTML( + $this->template->render('secondary_tabs', [ + 'url_params' => [ + 'db' => $this->db, + 'table' => $this->table, + ], + 'sub_tabs' => $this->_getSubTabs(), + ]) + ); + + $column_names = $this->_columnNames; + $column_types = $this->_columnTypes; + $types = []; + if ($this->_searchType == 'replace') { + $num_cols = count($column_names); + for ($i = 0; $i < $num_cols; $i++) { + $types[$column_names[$i]] = preg_replace('@\\(.*@s', '', $column_types[$i]); + } + } + + $criteria_column_names = isset($_POST['criteriaColumnNames']) ? $_POST['criteriaColumnNames'] : null; + $keys = []; + for ($i = 0; $i < 4; $i++) { + if (isset($criteria_column_names[$i])) { + if ($criteria_column_names[$i] != 'pma_null') { + $keys[$criteria_column_names[$i]] = array_search($criteria_column_names[$i], $column_names); + } + } + } + + $this->response->addHTML( + $this->template->render('table/search/selection_form', [ + 'search_type' => $this->_searchType, + 'db' => $this->db, + 'table' => $this->table, + 'goto' => $goto, + 'self' => $this, + 'geom_column_flag' => $this->_geomColumnFlag, + 'column_names' => $column_names, + 'column_types' => $column_types, + 'types' => $types, + 'column_collations' => $this->_columnCollations, + 'data_label' => $dataLabel, + 'keys' => $keys, + 'criteria_column_names' => $criteria_column_names, + 'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'], + 'criteria_column_types' => isset($_POST['criteriaColumnTypes']) ? $_POST['criteriaColumnTypes'] : null, + 'sql_types' => $this->dbi->types, + 'max_rows' => intval($GLOBALS['cfg']['MaxRows']), + 'max_plot_limit' => ! empty($_POST['maxPlotLimit']) + ? intval($_POST['maxPlotLimit']) + : intval($GLOBALS['cfg']['maxRowPlotLimit']), + ]) + ); + } + + /** + * Range search action + * + * @return void + */ + public function rangeSearchAction() + { + $min_max = $this->getColumnMinMax($_POST['column']); + $this->response->addJSON('column_data', $min_max); + } + + /** + * Find action + * + * @return void + */ + public function findAction() + { + $useRegex = array_key_exists('useRegex', $_POST) + && $_POST['useRegex'] == 'on'; + + $preview = $this->getReplacePreview( + $_POST['columnIndex'], + $_POST['find'], + $_POST['replaceWith'], + $useRegex, + $this->_connectionCharSet + ); + $this->response->addJSON('preview', $preview); + } + + /** + * Replace action + * + * @return void + */ + public function replaceAction() + { + $this->replace( + $_POST['columnIndex'], + $_POST['findString'], + $_POST['replaceWith'], + $_POST['useRegex'], + $this->_connectionCharSet + ); + $this->response->addHTML( + Util::getMessage( + __('Your SQL query has been executed successfully.'), + null, + 'success' + ) + ); + } + + /** + * Returns HTML for previewing strings found and their replacements + * + * @param int $columnIndex index of the column + * @param string $find string to find in the column + * @param string $replaceWith string to replace with + * @param boolean $useRegex to use Regex replace or not + * @param string $charSet character set of the connection + * + * @return string HTML for previewing strings found and their replacements + */ + public function getReplacePreview( + $columnIndex, + $find, + $replaceWith, + $useRegex, + $charSet + ) { + $column = $this->_columnNames[$columnIndex]; + if ($useRegex) { + $result = $this->_getRegexReplaceRows( + $columnIndex, + $find, + $replaceWith, + $charSet + ); + } else { + $sql_query = "SELECT " + . Util::backquote($column) . "," + . " REPLACE(" + . Util::backquote($column) . ", '" . $find . "', '" + . $replaceWith + . "')," + . " COUNT(*)" + . " FROM " . Util::backquote($this->db) + . "." . Util::backquote($this->table) + . " WHERE " . Util::backquote($column) + . " LIKE '%" . $find . "%' COLLATE " . $charSet . "_bin"; // here we + // change the collation of the 2nd operand to a case sensitive + // binary collation to make sure that the comparison + // is case sensitive + $sql_query .= " GROUP BY " . Util::backquote($column) + . " ORDER BY " . Util::backquote($column) . " ASC"; + + $result = $this->dbi->fetchResult($sql_query, 0); + } + + return $this->template->render('table/search/replace_preview', [ + 'db' => $this->db, + 'table' => $this->table, + 'column_index' => $columnIndex, + 'find' => $find, + 'replace_with' => $replaceWith, + 'use_regex' => $useRegex, + 'result' => $result, + ]); + } + + /** + * Finds and returns Regex pattern and their replacements + * + * @param int $columnIndex index of the column + * @param string $find string to find in the column + * @param string $replaceWith string to replace with + * @param string $charSet character set of the connection + * + * @return array|bool Array containing original values, replaced values and count + */ + private function _getRegexReplaceRows( + $columnIndex, + $find, + $replaceWith, + $charSet + ) { + $column = $this->_columnNames[$columnIndex]; + $sql_query = "SELECT " + . Util::backquote($column) . "," + . " 1," // to add an extra column that will have replaced value + . " COUNT(*)" + . " FROM " . Util::backquote($this->db) + . "." . Util::backquote($this->table) + . " WHERE " . Util::backquote($column) + . " RLIKE '" . $this->dbi->escapeString($find) . "' COLLATE " + . $charSet . "_bin"; // here we + // change the collation of the 2nd operand to a case sensitive + // binary collation to make sure that the comparison is case sensitive + $sql_query .= " GROUP BY " . Util::backquote($column) + . " ORDER BY " . Util::backquote($column) . " ASC"; + + $result = $this->dbi->fetchResult($sql_query, 0); + + if (is_array($result)) { + /* Iterate over possible delimiters to get one */ + $delimiters = [ + '/', + '@', + '#', + '~', + '!', + '$', + '%', + '^', + '&', + '_', + ]; + $found = false; + for ($i = 0, $l = count($delimiters); $i < $l; $i++) { + if (strpos($find, $delimiters[$i]) === false) { + $found = true; + break; + } + } + if (! $found) { + return false; + } + $find = $delimiters[$i] . $find . $delimiters[$i]; + foreach ($result as $index => $row) { + $result[$index][1] = preg_replace( + $find, + $replaceWith, + $row[0] + ); + } + } + return $result; + } + + /** + * Replaces a given string in a column with a give replacement + * + * @param int $columnIndex index of the column + * @param string $find string to find in the column + * @param string $replaceWith string to replace with + * @param boolean $useRegex to use Regex replace or not + * @param string $charSet character set of the connection + * + * @return void + */ + public function replace( + $columnIndex, + $find, + $replaceWith, + $useRegex, + $charSet + ) { + $column = $this->_columnNames[$columnIndex]; + if ($useRegex) { + $toReplace = $this->_getRegexReplaceRows( + $columnIndex, + $find, + $replaceWith, + $charSet + ); + $sql_query = "UPDATE " . Util::backquote($this->table) + . " SET " . Util::backquote($column) . " = CASE"; + if (is_array($toReplace)) { + foreach ($toReplace as $row) { + $sql_query .= "\n WHEN " . Util::backquote($column) + . " = '" . $this->dbi->escapeString($row[0]) + . "' THEN '" . $this->dbi->escapeString($row[1]) . "'"; + } + } + $sql_query .= " END" + . " WHERE " . Util::backquote($column) + . " RLIKE '" . $this->dbi->escapeString($find) . "' COLLATE " + . $charSet . "_bin"; // here we + // change the collation of the 2nd operand to a case sensitive + // binary collation to make sure that the comparison + // is case sensitive + } else { + $sql_query = "UPDATE " . Util::backquote($this->table) + . " SET " . Util::backquote($column) . " =" + . " REPLACE(" + . Util::backquote($column) . ", '" . $find . "', '" + . $replaceWith + . "')" + . " WHERE " . Util::backquote($column) + . " LIKE '%" . $find . "%' COLLATE " . $charSet . "_bin"; // here we + // change the collation of the 2nd operand to a case sensitive + // binary collation to make sure that the comparison + // is case sensitive + } + $this->dbi->query( + $sql_query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + $GLOBALS['sql_query'] = $sql_query; + } + + /** + * Finds minimum and maximum value of a given column. + * + * @param string $column Column name + * + * @return array + */ + public function getColumnMinMax($column) + { + $sql_query = 'SELECT MIN(' . Util::backquote($column) . ') AS `min`, ' + . 'MAX(' . Util::backquote($column) . ') AS `max` ' + . 'FROM ' . Util::backquote($this->db) . '.' + . Util::backquote($this->table); + + return $this->dbi->fetchSingleRow($sql_query); + } + + /** + * Returns an array with necessary configurations to create + * sub-tabs in the table_select page. + * + * @return array Array containing configuration (icon, text, link, id, args) + * of sub-tabs + */ + private function _getSubTabs() + { + $subtabs = []; + $subtabs['search']['icon'] = 'b_search'; + $subtabs['search']['text'] = __('Table search'); + $subtabs['search']['link'] = 'tbl_select.php'; + $subtabs['search']['id'] = 'tbl_search_id'; + $subtabs['search']['args']['pos'] = 0; + + $subtabs['zoom']['icon'] = 'b_select'; + $subtabs['zoom']['link'] = 'tbl_zoom_select.php'; + $subtabs['zoom']['text'] = __('Zoom search'); + $subtabs['zoom']['id'] = 'zoom_search_id'; + + $subtabs['replace']['icon'] = 'b_find_replace'; + $subtabs['replace']['link'] = 'tbl_find_replace.php'; + $subtabs['replace']['text'] = __('Find and replace'); + $subtabs['replace']['id'] = 'find_replace_id'; + + return $subtabs; + } + + /** + * Builds the sql search query from the post parameters + * + * @return string the generated SQL query + */ + private function _buildSqlQuery() + { + $sql_query = 'SELECT '; + + // If only distinct values are needed + $is_distinct = isset($_POST['distinct']) ? 'true' : 'false'; + if ($is_distinct == 'true') { + $sql_query .= 'DISTINCT '; + } + + // if all column names were selected to display, we do a 'SELECT *' + // (more efficient and this helps prevent a problem in IE + // if one of the rows is edited and we come back to the Select results) + if (isset($_POST['zoom_submit']) || ! empty($_POST['displayAllColumns'])) { + $sql_query .= '* '; + } else { + $sql_query .= implode( + ', ', + Util::backquote($_POST['columnsToDisplay']) + ); + } // end if + + $sql_query .= ' FROM ' + . Util::backquote($_POST['table']); + $whereClause = $this->_generateWhereClause(); + $sql_query .= $whereClause; + + // if the search results are to be ordered + if (isset($_POST['orderByColumn']) && $_POST['orderByColumn'] != '--nil--') { + $sql_query .= ' ORDER BY ' + . Util::backquote($_POST['orderByColumn']) + . ' ' . $_POST['order']; + } // end if + return $sql_query; + } + + /** + * Provides a column's type, collation, operators list, and criteria value + * to display in table search form + * + * @param integer $search_index Row number in table search form + * @param integer $column_index Column index in ColumnNames array + * + * @return array Array containing column's properties + */ + public function getColumnProperties($search_index, $column_index) + { + $selected_operator = (isset($_POST['criteriaColumnOperators'][$search_index]) + ? $_POST['criteriaColumnOperators'][$search_index] : ''); + $entered_value = (isset($_POST['criteriaValues']) + ? $_POST['criteriaValues'] : ''); + $titles = [ + 'Browse' => Util::getIcon( + 'b_browse', + __('Browse foreign values') + ), + ]; + //Gets column's type and collation + $type = $this->_columnTypes[$column_index]; + $collation = $this->_columnCollations[$column_index]; + //Gets column's comparison operators depending on column type + $typeOperators = $this->dbi->types->getTypeOperatorsHtml( + preg_replace('@\(.*@s', '', $this->_columnTypes[$column_index]), + $this->_columnNullFlags[$column_index], + $selected_operator + ); + $func = $this->template->render('table/search/column_comparison_operators', [ + 'search_index' => $search_index, + 'type_operators' => $typeOperators, + ]); + //Gets link to browse foreign data(if any) and criteria inputbox + $foreignData = $this->relation->getForeignData( + $this->_foreigners, + $this->_columnNames[$column_index], + false, + '', + '' + ); + $value = $this->template->render('table/search/input_box', [ + 'str' => '', + 'column_type' => (string) $type, + 'column_id' => 'fieldID_', + 'in_zoom_search_edit' => false, + 'foreigners' => $this->_foreigners, + 'column_name' => $this->_columnNames[$column_index], + 'column_name_hash' => md5($this->_columnNames[$column_index]), + 'foreign_data' => $foreignData, + 'table' => $this->table, + 'column_index' => $search_index, + 'foreign_max_limit' => $GLOBALS['cfg']['ForeignKeyMaxLimit'], + 'criteria_values' => $entered_value, + 'db' => $this->db, + 'titles' => $titles, + 'in_fbs' => true, + ]); + return [ + 'type' => $type, + 'collation' => $collation, + 'func' => $func, + 'value' => $value, + ]; + } + + /** + * Generates the where clause for the SQL search query to be executed + * + * @return string the generated where clause + */ + private function _generateWhereClause() + { + if (isset($_POST['customWhereClause']) + && trim($_POST['customWhereClause']) != '' + ) { + return ' WHERE ' . $_POST['customWhereClause']; + } + + // If there are no search criteria set or no unary criteria operators, + // return + if (! isset($_POST['criteriaValues']) + && ! isset($_POST['criteriaColumnOperators']) + && ! isset($_POST['geom_func']) + ) { + return ''; + } + + // else continue to form the where clause from column criteria values + $fullWhereClause = []; + foreach ($_POST['criteriaColumnOperators'] as $column_index => $operator) { + $unaryFlag = $this->dbi->types->isUnaryOperator($operator); + $tmp_geom_func = isset($_POST['geom_func'][$column_index]) + ? $_POST['geom_func'][$column_index] : null; + + $whereClause = $this->_getWhereClause( + $_POST['criteriaValues'][$column_index], + $_POST['criteriaColumnNames'][$column_index], + $_POST['criteriaColumnTypes'][$column_index], + $operator, + $unaryFlag, + $tmp_geom_func + ); + + if ($whereClause) { + $fullWhereClause[] = $whereClause; + } + } // end foreach + + if (! empty($fullWhereClause)) { + return ' WHERE ' . implode(' AND ', $fullWhereClause); + } + return ''; + } + + /** + * Return the where clause in case column's type is ENUM. + * + * @param mixed $criteriaValues Search criteria input + * @param string $func_type Search function/operator + * + * @return string part of where clause. + */ + private function _getEnumWhereClause($criteriaValues, $func_type) + { + if (! is_array($criteriaValues)) { + $criteriaValues = explode(',', $criteriaValues); + } + $enum_selected_count = count($criteriaValues); + if ($func_type == '=' && $enum_selected_count > 1) { + $func_type = 'IN'; + $parens_open = '('; + $parens_close = ')'; + } elseif ($func_type == '!=' && $enum_selected_count > 1) { + $func_type = 'NOT IN'; + $parens_open = '('; + $parens_close = ')'; + } else { + $parens_open = ''; + $parens_close = ''; + } + $enum_where = '\'' + . $this->dbi->escapeString($criteriaValues[0]) . '\''; + for ($e = 1; $e < $enum_selected_count; $e++) { + $enum_where .= ', \'' + . $this->dbi->escapeString($criteriaValues[$e]) . '\''; + } + + return ' ' . $func_type . ' ' . $parens_open + . $enum_where . $parens_close; + } + + /** + * Return the where clause for a geometrical column. + * + * @param mixed $criteriaValues Search criteria input + * @param string $names Name of the column on which search is submitted + * @param string $func_type Search function/operator + * @param string $types Type of the field + * @param bool $geom_func Whether geometry functions should be applied + * + * @return string part of where clause. + */ + private function _getGeomWhereClause( + $criteriaValues, + $names, + $func_type, + $types, + $geom_func = null + ) { + $geom_unary_functions = [ + 'IsEmpty' => 1, + 'IsSimple' => 1, + 'IsRing' => 1, + 'IsClosed' => 1, + ]; + $where = ''; + + // Get details about the geometry functions + $geom_funcs = Util::getGISFunctions($types, true, false); + + // If the function takes multiple parameters + if (strpos($func_type, "IS NULL") !== false || strpos($func_type, "IS NOT NULL") !== false) { + return Util::backquote($names) . " " . $func_type; + } elseif ($geom_funcs[$geom_func]['params'] > 1) { + // create gis data from the criteria input + $gis_data = Util::createGISData($criteriaValues, $this->dbi->getVersion()); + return $geom_func . '(' . Util::backquote($names) + . ', ' . $gis_data . ')'; + } + + // New output type is the output type of the function being applied + $type = $geom_funcs[$geom_func]['type']; + $geom_function_applied = $geom_func + . '(' . Util::backquote($names) . ')'; + + // If the where clause is something like 'IsEmpty(`spatial_col_name`)' + if (isset($geom_unary_functions[$geom_func]) + && trim($criteriaValues) == '' + ) { + $where = $geom_function_applied; + } elseif (in_array($type, Util::getGISDatatypes()) + && ! empty($criteriaValues) + ) { + // create gis data from the criteria input + $gis_data = Util::createGISData($criteriaValues, $this->dbi->getVersion()); + $where = $geom_function_applied . " " . $func_type . " " . $gis_data; + } elseif (strlen($criteriaValues) > 0) { + $where = $geom_function_applied . " " + . $func_type . " '" . $criteriaValues . "'"; + } + return $where; + } + + /** + * Return the where clause for query generation based on the inputs provided. + * + * @param mixed $criteriaValues Search criteria input + * @param string $names Name of the column on which search is submitted + * @param string $types Type of the field + * @param string $func_type Search function/operator + * @param bool $unaryFlag Whether operator unary or not + * @param bool $geom_func Whether geometry functions should be applied + * + * @return string generated where clause. + */ + private function _getWhereClause( + $criteriaValues, + $names, + $types, + $func_type, + $unaryFlag, + $geom_func = null + ) { + // If geometry function is set + if (! empty($geom_func)) { + return $this->_getGeomWhereClause( + $criteriaValues, + $names, + $func_type, + $types, + $geom_func + ); + } + + $backquoted_name = Util::backquote($names); + $where = ''; + if ($unaryFlag) { + $where = $backquoted_name . ' ' . $func_type; + } elseif (strncasecmp($types, 'enum', 4) == 0 && (! empty($criteriaValues) || $criteriaValues[0] === '0')) { + $where = $backquoted_name; + $where .= $this->_getEnumWhereClause($criteriaValues, $func_type); + } elseif ($criteriaValues != '') { + // For these types we quote the value. Even if it's another type + // (like INT), for a LIKE we always quote the value. MySQL converts + // strings to numbers and numbers to strings as necessary + // during the comparison + if (preg_match('@char|binary|blob|text|set|date|time|year@i', $types) + || mb_strpos(' ' . $func_type, 'LIKE') + ) { + $quot = '\''; + } else { + $quot = ''; + } + + // LIKE %...% + if ($func_type == 'LIKE %...%') { + $func_type = 'LIKE'; + $criteriaValues = '%' . $criteriaValues . '%'; + } + if ($func_type == 'REGEXP ^...$') { + $func_type = 'REGEXP'; + $criteriaValues = '^' . $criteriaValues . '$'; + } + + if ('IN (...)' != $func_type + && 'NOT IN (...)' != $func_type + && 'BETWEEN' != $func_type + && 'NOT BETWEEN' != $func_type + ) { + return $backquoted_name . ' ' . $func_type . ' ' . $quot + . $this->dbi->escapeString($criteriaValues) . $quot; + } + $func_type = str_replace(' (...)', '', $func_type); + + //Don't explode if this is already an array + //(Case for (NOT) IN/BETWEEN.) + if (is_array($criteriaValues)) { + $values = $criteriaValues; + } else { + $values = explode(',', $criteriaValues); + } + // quote values one by one + $emptyKey = false; + foreach ($values as $key => &$value) { + if ('' === $value) { + $emptyKey = $key; + $value = 'NULL'; + continue; + } + $value = $quot . $this->dbi->escapeString(trim($value)) + . $quot; + } + + if ('BETWEEN' == $func_type || 'NOT BETWEEN' == $func_type) { + $where = $backquoted_name . ' ' . $func_type . ' ' + . (isset($values[0]) ? $values[0] : '') + . ' AND ' . (isset($values[1]) ? $values[1] : ''); + } else { //[NOT] IN + if (false !== $emptyKey) { + unset($values[$emptyKey]); + } + $wheres = []; + if (! empty($values)) { + $wheres[] = $backquoted_name . ' ' . $func_type + . ' (' . implode(',', $values) . ')'; + } + if (false !== $emptyKey) { + $wheres[] = $backquoted_name . ' IS NULL'; + } + $where = implode(' OR ', $wheres); + if (1 < count($wheres)) { + $where = '(' . $where . ')'; + } + } + } // end if + + return $where; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Table/SqlController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Table/SqlController.php new file mode 100644 index 0000000..a3975ac --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Table/SqlController.php @@ -0,0 +1,53 @@ +getHtml( + $params['sql_query'] ?? true, + false, + isset($params['delimiter']) + ? htmlspecialchars($params['delimiter']) + : ';' + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Table/StructureController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Table/StructureController.php new file mode 100644 index 0000000..7f59013 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Table/StructureController.php @@ -0,0 +1,1648 @@ +_db_is_system_schema = $db_is_system_schema; + $this->_url_query = Url::getCommonRaw(['db' => $db, 'table' => $table]); + $this->_tbl_is_view = $tbl_is_view; + $this->_tbl_storage_engine = $tbl_storage_engine; + $this->_table_info_num_rows = $table_info_num_rows; + $this->_tbl_collation = $tbl_collation; + $this->_showtable = $showtable; + $this->table_obj = $this->dbi->getTable($this->db, $this->table); + + $this->createAddField = $createAddField; + $this->relation = $relation; + $this->transformations = $transformations; + } + + /** + * Index action + * + * @param ContainerBuilder $containerBuilder ContainerBuilder instance + * + * @return void + */ + public function indexAction(ContainerBuilder $containerBuilder): void + { + global $sql_query; + + PageSettings::showGroup('TableStructure'); + + $checkUserPrivileges = new CheckUserPrivileges($this->dbi); + $checkUserPrivileges->getPrivileges(); + + $this->response->getHeader()->getScripts()->addFiles( + [ + 'table/structure.js', + 'indexes.js', + ] + ); + + /** + * Handle column moving + */ + if (isset($_POST['move_columns']) + && is_array($_POST['move_columns']) + && $this->response->isAjax() + ) { + $this->moveColumns(); + return; + } + + /** + * handle MySQL reserved words columns check + */ + if (isset($_POST['reserved_word_check'])) { + if ($GLOBALS['cfg']['ReservedWordDisableWarning'] === false) { + $columns_names = $_POST['field_name']; + $reserved_keywords_names = []; + foreach ($columns_names as $column) { + if (Context::isKeyword(trim($column), true)) { + $reserved_keywords_names[] = trim($column); + } + } + if (Context::isKeyword(trim($this->table), true)) { + $reserved_keywords_names[] = trim($this->table); + } + if (count($reserved_keywords_names) === 0) { + $this->response->setRequestStatus(false); + } + $this->response->addJSON( + 'message', + sprintf( + _ngettext( + 'The name \'%s\' is a MySQL reserved keyword.', + 'The names \'%s\' are MySQL reserved keywords.', + count($reserved_keywords_names) + ), + implode(',', $reserved_keywords_names) + ) + ); + } else { + $this->response->setRequestStatus(false); + } + return; + } + /** + * A click on Change has been made for one column + */ + if (isset($_GET['change_column'])) { + $this->displayHtmlForColumnChange(null, 'tbl_structure.php', $containerBuilder); + return; + } + + /** + * Adding or editing partitioning of the table + */ + if (isset($_POST['edit_partitioning']) + && ! isset($_POST['save_partitioning']) + ) { + $this->displayHtmlForPartitionChange(); + return; + } + + /** + * handle multiple field commands if required + * + * submit_mult_*_x comes from IE if is used + */ + $submit_mult = $this->getMultipleFieldCommandType(); + + if (! empty($submit_mult)) { + if (isset($_POST['selected_fld'])) { + if ($submit_mult == 'browse') { + // browsing the table displaying only selected columns + $this->displayTableBrowseForSelectedColumns( + $GLOBALS['goto'], + $GLOBALS['pmaThemeImage'] + ); + } else { + // handle multiple field commands + // handle confirmation of deleting multiple columns + $action = 'tbl_structure.php'; + $GLOBALS['selected'] = $_POST['selected_fld']; + list( + $what_ret, $query_type_ret, $is_unset_submit_mult, + $mult_btn_ret, $centralColsError + ) + = $this->getDataForSubmitMult( + $submit_mult, + $_POST['selected_fld'], + $action, + $containerBuilder + ); + //update the existing variables + // todo: refactor mult_submits.inc.php such as + // below globals are not needed anymore + if (isset($what_ret)) { + $GLOBALS['what'] = $what_ret; + global $what; + } + if (isset($query_type_ret)) { + $GLOBALS['query_type'] = $query_type_ret; + global $query_type; + } + if ($is_unset_submit_mult) { + unset($submit_mult); + } + if (isset($mult_btn_ret)) { + $GLOBALS['mult_btn'] = $mult_btn_ret; + global $mult_btn; + } + include ROOT_PATH . 'libraries/mult_submits.inc.php'; + /** + * if $submit_mult == 'change', execution will have stopped + * at this point + */ + if (empty($message)) { + $message = Message::success(); + } + $this->response->addHTML( + Util::getMessage($message, $sql_query) + ); + } + } else { + $this->response->setRequestStatus(false); + $message = Message::error(__('No column selected.')); + $this->response->addJSON('message', $message); + } + } + + /** + * Modifications have been submitted -> updates the table + */ + if (isset($_POST['do_save_data'])) { + $regenerate = $this->updateColumns(); + if (! $regenerate) { + // continue to show the table's structure + unset($_POST['selected']); + } + } + + /** + * Modifications to the partitioning have been submitted -> updates the table + */ + if (isset($_POST['save_partitioning'])) { + $this->updatePartitioning(); + } + + /** + * Adding indexes + */ + if (isset($_POST['add_key']) + || isset($_POST['partition_maintenance']) + ) { + //todo: set some variables for sql.php include, to be eliminated + //after refactoring sql.php + $db = $this->db; + $table = $this->table; + $sql_query = $GLOBALS['sql_query']; + $cfg = $GLOBALS['cfg']; + $pmaThemeImage = $GLOBALS['pmaThemeImage']; + include ROOT_PATH . 'sql.php'; + $GLOBALS['reload'] = true; + } + + /** + * Gets the relation settings + */ + $cfgRelation = $this->relation->getRelationsParam(); + + /** + * Runs common work + */ + // set db, table references, for require_once that follows + // got to be eliminated in long run + $db = &$this->db; + $table = &$this->table; + $url_params = []; + include_once ROOT_PATH . 'libraries/tbl_common.inc.php'; + $this->_db_is_system_schema = $db_is_system_schema; + $this->_url_query = Url::getCommonRaw([ + 'db' => $db, + 'table' => $table, + 'goto' => 'tbl_structure.php', + 'back' => 'tbl_structure.php', + ]); + /* The url_params array is initialized in above include */ + $url_params['goto'] = 'tbl_structure.php'; + $url_params['back'] = 'tbl_structure.php'; + + // 2. Gets table keys and retains them + // @todo should be: $server->db($db)->table($table)->primary() + $primary = Index::getPrimary($this->table, $this->db); + $columns_with_index = $this->dbi + ->getTable($this->db, $this->table) + ->getColumnsWithIndex( + Index::UNIQUE | Index::INDEX | Index::SPATIAL + | Index::FULLTEXT + ); + $columns_with_unique_index = $this->dbi + ->getTable($this->db, $this->table) + ->getColumnsWithIndex(Index::UNIQUE); + + // 3. Get fields + $fields = (array) $this->dbi->getColumns( + $this->db, + $this->table, + null, + true + ); + + //display table structure + $this->response->addHTML( + $this->displayStructure( + $cfgRelation, + $columns_with_unique_index, + $url_params, + $primary, + $fields, + $columns_with_index + ) + ); + } + + /** + * Moves columns in the table's structure based on $_REQUEST + * + * @return void + */ + protected function moveColumns() + { + $this->dbi->selectDb($this->db); + + /* + * load the definitions for all columns + */ + $columns = $this->dbi->getColumnsFull($this->db, $this->table); + $column_names = array_keys($columns); + $changes = []; + + // @see https://mariadb.com/kb/en/library/changes-improvements-in-mariadb-102/#information-schema + $usesLiteralNull = $this->dbi->isMariaDB() && $this->dbi->getVersion() >= 100200; + $defaultNullValue = $usesLiteralNull ? 'NULL' : null; + // move columns from first to last + for ($i = 0, $l = count($_POST['move_columns']); $i < $l; $i++) { + $column = $_POST['move_columns'][$i]; + // is this column already correctly placed? + if ($column_names[$i] == $column) { + continue; + } + + // it is not, let's move it to index $i + $data = $columns[$column]; + $extracted_columnspec = Util::extractColumnSpec($data['Type']); + if (isset($data['Extra']) + && $data['Extra'] == 'on update CURRENT_TIMESTAMP' + ) { + $extracted_columnspec['attribute'] = $data['Extra']; + unset($data['Extra']); + } + $current_timestamp = ($data['Type'] == 'timestamp' + || $data['Type'] == 'datetime') + && ($data['Default'] == 'CURRENT_TIMESTAMP' + || $data['Default'] == 'current_timestamp()'); + + // @see https://mariadb.com/kb/en/library/information-schema-columns-table/#examples + if ($data['Null'] === 'YES' && in_array($data['Default'], [$defaultNullValue, null])) { + $default_type = 'NULL'; + } elseif ($current_timestamp) { + $default_type = 'CURRENT_TIMESTAMP'; + } elseif ($data['Default'] === null) { + $default_type = 'NONE'; + } else { + $default_type = 'USER_DEFINED'; + } + + $virtual = [ + 'VIRTUAL', + 'PERSISTENT', + 'VIRTUAL GENERATED', + 'STORED GENERATED', + ]; + $data['Virtuality'] = ''; + $data['Expression'] = ''; + if (isset($data['Extra']) && in_array($data['Extra'], $virtual)) { + $data['Virtuality'] = str_replace(' GENERATED', '', $data['Extra']); + $expressions = $this->table_obj->getColumnGenerationExpression($column); + $data['Expression'] = $expressions[$column]; + } + + $changes[] = 'CHANGE ' . Table::generateAlter( + $column, + $column, + mb_strtoupper($extracted_columnspec['type']), + $extracted_columnspec['spec_in_brackets'], + $extracted_columnspec['attribute'], + isset($data['Collation']) ? $data['Collation'] : '', + $data['Null'] === 'YES' ? 'YES' : 'NO', + $default_type, + $current_timestamp ? '' : $data['Default'], + isset($data['Extra']) && $data['Extra'] !== '' ? $data['Extra'] + : false, + isset($data['COLUMN_COMMENT']) && $data['COLUMN_COMMENT'] !== '' + ? $data['COLUMN_COMMENT'] : false, + $data['Virtuality'], + $data['Expression'], + $i === 0 ? '-first' : $column_names[$i - 1] + ); + // update current column_names array, first delete old position + for ($j = 0, $ll = count($column_names); $j < $ll; $j++) { + if ($column_names[$j] == $column) { + unset($column_names[$j]); + } + } + // insert moved column + array_splice($column_names, $i, 0, $column); + } + if (empty($changes) && ! isset($_REQUEST['preview_sql'])) { // should never happen + $this->response->setRequestStatus(false); + return; + } + // query for moving the columns + $sql_query = sprintf( + 'ALTER TABLE %s %s', + Util::backquote($this->table), + implode(', ', $changes) + ); + + if (isset($_REQUEST['preview_sql'])) { // preview sql + $this->response->addJSON( + 'sql_data', + $this->template->render('preview_sql', [ + 'query_data' => $sql_query, + ]) + ); + } else { // move column + $this->dbi->tryQuery($sql_query); + $tmp_error = $this->dbi->getError(); + if ($tmp_error) { + $this->response->setRequestStatus(false); + $this->response->addJSON('message', Message::error($tmp_error)); + } else { + $message = Message::success( + __('The columns have been moved successfully.') + ); + $this->response->addJSON('message', $message); + $this->response->addJSON('columns', $column_names); + } + } + } + + /** + * Displays HTML for changing one or more columns + * + * @param array $selected the selected columns + * @param string $action target script to call + * @param ContainerBuilder $containerBuilder Container builder instance (Used in tbl_columns_definition_form.inc.php) + * + * @return void + */ + protected function displayHtmlForColumnChange($selected, $action, ContainerBuilder $containerBuilder) + { + // $selected comes from mult_submits.inc.php + if (empty($selected)) { + $selected[] = $_REQUEST['field']; + $selected_cnt = 1; + } else { // from a multiple submit + $selected_cnt = count($selected); + } + + /** + * @todo optimize in case of multiple fields to modify + */ + $fields_meta = []; + for ($i = 0; $i < $selected_cnt; $i++) { + $value = $this->dbi->getColumns( + $this->db, + $this->table, + $this->dbi->escapeString($selected[$i]), + true + ); + if (count($value) === 0) { + $message = Message::error( + __('Failed to get description of column %s!') + ); + $message->addParam($selected[$i]); + $this->response->addHTML($message); + } else { + $fields_meta[] = $value; + } + } + $num_fields = count($fields_meta); + // set these globals because tbl_columns_definition_form.inc.php + // verifies them + // @todo: refactor tbl_columns_definition_form.inc.php so that it uses + // protected function params + $GLOBALS['action'] = $action; + $GLOBALS['num_fields'] = $num_fields; + + /** + * Form for changing properties. + */ + $checkUserPrivileges = new CheckUserPrivileges($this->dbi); + $checkUserPrivileges->getPrivileges(); + + include ROOT_PATH . 'libraries/tbl_columns_definition_form.inc.php'; + } + + /** + * Displays HTML for partition change + * + * @return void + */ + protected function displayHtmlForPartitionChange() + { + $partitionDetails = null; + if (! isset($_POST['partition_by'])) { + $partitionDetails = $this->_extractPartitionDetails(); + } + + $partitionDetails = TablePartitionDefinition::getDetails($partitionDetails); + $this->response->addHTML( + $this->template->render('table/structure/partition_definition_form', [ + 'db' => $this->db, + 'table' => $this->table, + 'partition_details' => $partitionDetails, + ]) + ); + } + + /** + * Extracts partition details from CREATE TABLE statement + * + * @return array[]|null array of partition details + */ + private function _extractPartitionDetails() + { + $createTable = (new Table($this->table, $this->db))->showCreate(); + if (! $createTable) { + return null; + } + + $parser = new Parser($createTable); + /** + * @var CreateStatement $stmt + */ + $stmt = $parser->statements[0]; + + $partitionDetails = []; + + $partitionDetails['partition_by'] = ''; + $partitionDetails['partition_expr'] = ''; + $partitionDetails['partition_count'] = ''; + + if (! empty($stmt->partitionBy)) { + $openPos = strpos($stmt->partitionBy, "("); + $closePos = strrpos($stmt->partitionBy, ")"); + + $partitionDetails['partition_by'] + = trim(substr($stmt->partitionBy, 0, $openPos)); + $partitionDetails['partition_expr'] + = trim(substr($stmt->partitionBy, $openPos + 1, $closePos - ($openPos + 1))); + if (isset($stmt->partitionsNum)) { + $count = $stmt->partitionsNum; + } else { + $count = count($stmt->partitions); + } + $partitionDetails['partition_count'] = $count; + } + + $partitionDetails['subpartition_by'] = ''; + $partitionDetails['subpartition_expr'] = ''; + $partitionDetails['subpartition_count'] = ''; + + if (! empty($stmt->subpartitionBy)) { + $openPos = strpos($stmt->subpartitionBy, "("); + $closePos = strrpos($stmt->subpartitionBy, ")"); + + $partitionDetails['subpartition_by'] + = trim(substr($stmt->subpartitionBy, 0, $openPos)); + $partitionDetails['subpartition_expr'] + = trim(substr($stmt->subpartitionBy, $openPos + 1, $closePos - ($openPos + 1))); + if (isset($stmt->subpartitionsNum)) { + $count = $stmt->subpartitionsNum; + } else { + $count = count($stmt->partitions[0]->subpartitions); + } + $partitionDetails['subpartition_count'] = $count; + } + + // Only LIST and RANGE type parameters allow subpartitioning + $partitionDetails['can_have_subpartitions'] + = $partitionDetails['partition_count'] > 1 + && ($partitionDetails['partition_by'] == 'RANGE' + || $partitionDetails['partition_by'] == 'RANGE COLUMNS' + || $partitionDetails['partition_by'] == 'LIST' + || $partitionDetails['partition_by'] == 'LIST COLUMNS'); + + // Values are specified only for LIST and RANGE type partitions + $partitionDetails['value_enabled'] = isset($partitionDetails['partition_by']) + && ($partitionDetails['partition_by'] == 'RANGE' + || $partitionDetails['partition_by'] == 'RANGE COLUMNS' + || $partitionDetails['partition_by'] == 'LIST' + || $partitionDetails['partition_by'] == 'LIST COLUMNS'); + + $partitionDetails['partitions'] = []; + + for ($i = 0, $iMax = (int) $partitionDetails['partition_count']; $i < $iMax; $i++) { + if (! isset($stmt->partitions[$i])) { + $partitionDetails['partitions'][$i] = [ + 'name' => 'p' . $i, + 'value_type' => '', + 'value' => '', + 'engine' => '', + 'comment' => '', + 'data_directory' => '', + 'index_directory' => '', + 'max_rows' => '', + 'min_rows' => '', + 'tablespace' => '', + 'node_group' => '', + ]; + } else { + $p = $stmt->partitions[$i]; + $type = $p->type; + $expr = trim((string) $p->expr, '()'); + if ($expr == 'MAXVALUE') { + $type .= ' MAXVALUE'; + $expr = ''; + } + $partitionDetails['partitions'][$i] = [ + 'name' => $p->name, + 'value_type' => $type, + 'value' => $expr, + 'engine' => $p->options->has('ENGINE', true), + 'comment' => trim((string) $p->options->has('COMMENT', true), "'"), + 'data_directory' => trim((string) $p->options->has('DATA DIRECTORY', true), "'"), + 'index_directory' => trim((string) $p->options->has('INDEX_DIRECTORY', true), "'"), + 'max_rows' => $p->options->has('MAX_ROWS', true), + 'min_rows' => $p->options->has('MIN_ROWS', true), + 'tablespace' => $p->options->has('TABLESPACE', true), + 'node_group' => $p->options->has('NODEGROUP', true), + ]; + } + + $partition =& $partitionDetails['partitions'][$i]; + $partition['prefix'] = 'partitions[' . $i . ']'; + + if ($partitionDetails['subpartition_count'] > 1) { + $partition['subpartition_count'] = $partitionDetails['subpartition_count']; + $partition['subpartitions'] = []; + + for ($j = 0, $jMax = (int) $partitionDetails['subpartition_count']; $j < $jMax; $j++) { + if (! isset($stmt->partitions[$i]->subpartitions[$j])) { + $partition['subpartitions'][$j] = [ + 'name' => $partition['name'] . '_s' . $j, + 'engine' => '', + 'comment' => '', + 'data_directory' => '', + 'index_directory' => '', + 'max_rows' => '', + 'min_rows' => '', + 'tablespace' => '', + 'node_group' => '', + ]; + } else { + $sp = $stmt->partitions[$i]->subpartitions[$j]; + $partition['subpartitions'][$j] = [ + 'name' => $sp->name, + 'engine' => $sp->options->has('ENGINE', true), + 'comment' => trim($sp->options->has('COMMENT', true), "'"), + 'data_directory' => trim($sp->options->has('DATA DIRECTORY', true), "'"), + 'index_directory' => trim($sp->options->has('INDEX_DIRECTORY', true), "'"), + 'max_rows' => $sp->options->has('MAX_ROWS', true), + 'min_rows' => $sp->options->has('MIN_ROWS', true), + 'tablespace' => $sp->options->has('TABLESPACE', true), + 'node_group' => $sp->options->has('NODEGROUP', true), + ]; + } + + $subpartition =& $partition['subpartitions'][$j]; + $subpartition['prefix'] = 'partitions[' . $i . ']' + . '[subpartitions][' . $j . ']'; + } + } + } + + return $partitionDetails; + } + + /** + * Update the table's partitioning based on $_REQUEST + * + * @return void + */ + protected function updatePartitioning() + { + $sql_query = "ALTER TABLE " . Util::backquote($this->table) . " " + . $this->createAddField->getPartitionsDefinition(); + + // Execute alter query + $result = $this->dbi->tryQuery($sql_query); + + if ($result !== false) { + $message = Message::success( + __('Table %1$s has been altered successfully.') + ); + $message->addParam($this->table); + $this->response->addHTML( + Util::getMessage($message, $sql_query, 'success') + ); + } else { + $this->response->setRequestStatus(false); + $this->response->addJSON( + 'message', + Message::rawError( + __('Query error') . ':
' . $this->dbi->getError() + ) + ); + } + } + + /** + * Function to get the type of command for multiple field handling + * + * @return string|null + */ + protected function getMultipleFieldCommandType() + { + $types = [ + 'change', + 'drop', + 'primary', + 'index', + 'unique', + 'spatial', + 'fulltext', + 'browse', + ]; + + foreach ($types as $type) { + if (isset($_POST['submit_mult_' . $type . '_x'])) { + return $type; + } + } + + if (isset($_POST['submit_mult'])) { + return $_POST['submit_mult']; + } elseif (isset($_POST['mult_btn']) + && $_POST['mult_btn'] == __('Yes') + ) { + if (isset($_POST['selected'])) { + $_POST['selected_fld'] = $_POST['selected']; + } + return 'row_delete'; + } + + return null; + } + + /** + * Function to display table browse for selected columns + * + * @param string $goto goto page url + * @param string $pmaThemeImage URI of the pma theme image + * + * @return void + */ + protected function displayTableBrowseForSelectedColumns($goto, $pmaThemeImage) + { + $GLOBALS['active_page'] = 'sql.php'; + $fields = []; + foreach ($_POST['selected_fld'] as $sval) { + $fields[] = Util::backquote($sval); + } + $sql_query = sprintf( + 'SELECT %s FROM %s.%s', + implode(', ', $fields), + Util::backquote($this->db), + Util::backquote($this->table) + ); + + // Parse and analyze the query + $db = &$this->db; + list( + $analyzed_sql_results, + $db, + ) = ParseAnalyze::sqlQuery($sql_query, $db); + // @todo: possibly refactor + extract($analyzed_sql_results); + + $sql = new Sql(); + $this->response->addHTML( + $sql->executeQueryAndGetQueryResponse( + isset($analyzed_sql_results) ? $analyzed_sql_results : '', + false, // is_gotofile + $this->db, // db + $this->table, // table + null, // find_real_end + null, // sql_query_for_bookmark + null, // extra_data + null, // message_to_show + null, // message + null, // sql_data + $goto, // goto + $pmaThemeImage, // pmaThemeImage + null, // disp_query + null, // disp_message + null, // query_type + $sql_query, // sql_query + null, // selectedTables + null // complete_query + ) + ); + } + + /** + * Update the table's structure based on $_REQUEST + * + * @return boolean true if error occurred + * + */ + protected function updateColumns() + { + $err_url = 'tbl_structure.php' . Url::getCommon( + [ + 'db' => $this->db, + 'table' => $this->table, + ] + ); + $regenerate = false; + $field_cnt = count($_POST['field_name']); + $changes = []; + $adjust_privileges = []; + $columns_with_index = $this->dbi + ->getTable($this->db, $this->table) + ->getColumnsWithIndex( + Index::PRIMARY | Index::UNIQUE + ); + for ($i = 0; $i < $field_cnt; $i++) { + if (! $this->columnNeedsAlterTable($i)) { + continue; + } + + $changes[] = 'CHANGE ' . Table::generateAlter( + Util::getValueByKey($_POST, "field_orig.${i}", ''), + $_POST['field_name'][$i], + $_POST['field_type'][$i], + $_POST['field_length'][$i], + $_POST['field_attribute'][$i], + Util::getValueByKey($_POST, "field_collation.${i}", ''), + Util::getValueByKey($_POST, "field_null.${i}", 'NO'), + $_POST['field_default_type'][$i], + $_POST['field_default_value'][$i], + Util::getValueByKey($_POST, "field_extra.${i}", false), + Util::getValueByKey($_POST, "field_comments.${i}", ''), + Util::getValueByKey($_POST, "field_virtuality.${i}", ''), + Util::getValueByKey($_POST, "field_expression.${i}", ''), + Util::getValueByKey($_POST, "field_move_to.${i}", ''), + $columns_with_index + ); + + // find the remembered sort expression + $sorted_col = $this->table_obj->getUiProp( + Table::PROP_SORTED_COLUMN + ); + // if the old column name is part of the remembered sort expression + if (mb_strpos( + (string) $sorted_col, + Util::backquote($_POST['field_orig'][$i]) + ) !== false) { + // delete the whole remembered sort expression + $this->table_obj->removeUiProp(Table::PROP_SORTED_COLUMN); + } + + if (isset($_POST['field_adjust_privileges'][$i]) + && ! empty($_POST['field_adjust_privileges'][$i]) + && $_POST['field_orig'][$i] != $_POST['field_name'][$i] + ) { + $adjust_privileges[$_POST['field_orig'][$i]] + = $_POST['field_name'][$i]; + } + } // end for + + if (count($changes) > 0 || isset($_POST['preview_sql'])) { + // Builds the primary keys statements and updates the table + $key_query = ''; + /** + * this is a little bit more complex + * + * @todo if someone selects A_I when altering a column we need to check: + * - no other column with A_I + * - the column has an index, if not create one + * + */ + + // To allow replication, we first select the db to use + // and then run queries on this db. + if (! $this->dbi->selectDb($this->db)) { + Util::mysqlDie( + $this->dbi->getError(), + 'USE ' . Util::backquote($this->db) . ';', + false, + $err_url + ); + } + $sql_query = 'ALTER TABLE ' . Util::backquote($this->table) . ' '; + $sql_query .= implode(', ', $changes) . $key_query; + $sql_query .= ';'; + + // If there is a request for SQL previewing. + if (isset($_POST['preview_sql'])) { + Core::previewSQL(count($changes) > 0 ? $sql_query : ''); + } + + $columns_with_index = $this->dbi + ->getTable($this->db, $this->table) + ->getColumnsWithIndex( + Index::PRIMARY | Index::UNIQUE | Index::INDEX + | Index::SPATIAL | Index::FULLTEXT + ); + + $changedToBlob = []; + // While changing the Column Collation + // First change to BLOB + for ($i = 0; $i < $field_cnt; $i++) { + if (isset($_POST['field_collation'][$i]) + && isset($_POST['field_collation_orig'][$i]) + && $_POST['field_collation'][$i] !== $_POST['field_collation_orig'][$i] + && ! in_array($_POST['field_orig'][$i], $columns_with_index) + ) { + $secondary_query = 'ALTER TABLE ' . Util::backquote( + $this->table + ) + . ' CHANGE ' . Util::backquote( + $_POST['field_orig'][$i] + ) + . ' ' . Util::backquote($_POST['field_orig'][$i]) + . ' BLOB'; + + if (isset($_POST['field_virtuality'][$i]) + && isset($_POST['field_expression'][$i])) { + if ($_POST['field_virtuality'][$i]) { + $secondary_query .= ' AS (' . $_POST['field_expression'][$i] . ') ' + . $_POST['field_virtuality'][$i]; + } + } + + $secondary_query .= ';'; + + $this->dbi->query($secondary_query); + $changedToBlob[$i] = true; + } else { + $changedToBlob[$i] = false; + } + } + + // Then make the requested changes + $result = $this->dbi->tryQuery($sql_query); + + if ($result !== false) { + $changed_privileges = $this->adjustColumnPrivileges( + $adjust_privileges + ); + + if ($changed_privileges) { + $message = Message::success( + __( + 'Table %1$s has been altered successfully. Privileges ' . + 'have been adjusted.' + ) + ); + } else { + $message = Message::success( + __('Table %1$s has been altered successfully.') + ); + } + $message->addParam($this->table); + + $this->response->addHTML( + Util::getMessage($message, $sql_query, 'success') + ); + } else { + // An error happened while inserting/updating a table definition + + // Save the Original Error + $orig_error = $this->dbi->getError(); + $changes_revert = []; + + // Change back to Original Collation and data type + for ($i = 0; $i < $field_cnt; $i++) { + if ($changedToBlob[$i]) { + $changes_revert[] = 'CHANGE ' . Table::generateAlter( + Util::getValueByKey($_POST, "field_orig.${i}", ''), + $_POST['field_name'][$i], + $_POST['field_type_orig'][$i], + $_POST['field_length_orig'][$i], + $_POST['field_attribute_orig'][$i], + Util::getValueByKey($_POST, "field_collation_orig.${i}", ''), + Util::getValueByKey($_POST, "field_null_orig.${i}", 'NO'), + $_POST['field_default_type_orig'][$i], + $_POST['field_default_value_orig'][$i], + Util::getValueByKey($_POST, "field_extra_orig.${i}", false), + Util::getValueByKey($_POST, "field_comments_orig.${i}", ''), + Util::getValueByKey($_POST, "field_virtuality_orig.${i}", ''), + Util::getValueByKey($_POST, "field_expression_orig.${i}", ''), + Util::getValueByKey($_POST, "field_move_to_orig.${i}", '') + ); + } + } + + $revert_query = 'ALTER TABLE ' . Util::backquote($this->table) + . ' '; + $revert_query .= implode(', ', $changes_revert) . ''; + $revert_query .= ';'; + + // Column reverted back to original + $this->dbi->query($revert_query); + + $this->response->setRequestStatus(false); + $this->response->addJSON( + 'message', + Message::rawError( + __('Query error') . ':
' . $orig_error + ) + ); + $regenerate = true; + } + } + + // update field names in relation + if (isset($_POST['field_orig']) && is_array($_POST['field_orig'])) { + foreach ($_POST['field_orig'] as $fieldindex => $fieldcontent) { + if ($_POST['field_name'][$fieldindex] != $fieldcontent) { + $this->relation->renameField( + $this->db, + $this->table, + $fieldcontent, + $_POST['field_name'][$fieldindex] + ); + } + } + } + + // update mime types + if (isset($_POST['field_mimetype']) + && is_array($_POST['field_mimetype']) + && $GLOBALS['cfg']['BrowseMIME'] + ) { + foreach ($_POST['field_mimetype'] as $fieldindex => $mimetype) { + if (isset($_POST['field_name'][$fieldindex]) + && strlen($_POST['field_name'][$fieldindex]) > 0 + ) { + $this->transformations->setMime( + $this->db, + $this->table, + $_POST['field_name'][$fieldindex], + $mimetype, + $_POST['field_transformation'][$fieldindex], + $_POST['field_transformation_options'][$fieldindex], + $_POST['field_input_transformation'][$fieldindex], + $_POST['field_input_transformation_options'][$fieldindex] + ); + } + } + } + return $regenerate; + } + + /** + * Adjusts the Privileges for all the columns whose names have changed + * + * @param array $adjust_privileges assoc array of old col names mapped to new + * cols + * + * @return boolean boolean whether at least one column privileges + * adjusted + */ + protected function adjustColumnPrivileges(array $adjust_privileges) + { + $changed = false; + + if (Util::getValueByKey($GLOBALS, 'col_priv', false) + && Util::getValueByKey($GLOBALS, 'is_reload_priv', false) + ) { + $this->dbi->selectDb('mysql'); + + // For Column specific privileges + foreach ($adjust_privileges as $oldCol => $newCol) { + $this->dbi->query( + sprintf( + 'UPDATE %s SET Column_name = "%s" + WHERE Db = "%s" + AND Table_name = "%s" + AND Column_name = "%s";', + Util::backquote('columns_priv'), + $newCol, + $this->db, + $this->table, + $oldCol + ) + ); + + // i.e. if atleast one column privileges adjusted + $changed = true; + } + + if ($changed) { + // Finally FLUSH the new privileges + $this->dbi->query("FLUSH PRIVILEGES;"); + } + } + + return $changed; + } + + /** + * Verifies if some elements of a column have changed + * + * @param integer $i column index in the request + * + * @return boolean true if we need to generate ALTER TABLE + * + */ + protected function columnNeedsAlterTable($i) + { + // these two fields are checkboxes so might not be part of the + // request; therefore we define them to avoid notices below + if (! isset($_POST['field_null'][$i])) { + $_POST['field_null'][$i] = 'NO'; + } + if (! isset($_POST['field_extra'][$i])) { + $_POST['field_extra'][$i] = ''; + } + + // field_name does not follow the convention (corresponds to field_orig) + if ($_POST['field_name'][$i] != $_POST['field_orig'][$i]) { + return true; + } + + $fields = [ + 'field_attribute', + 'field_collation', + 'field_comments', + 'field_default_value', + 'field_default_type', + 'field_extra', + 'field_length', + 'field_null', + 'field_type', + ]; + foreach ($fields as $field) { + if ($_POST[$field][$i] != $_POST[$field . '_orig'][$i]) { + return true; + } + } + return ! empty($_POST['field_move_to'][$i]); + } + + /** + * Displays the table structure ('show table' works correct since 3.23.03) + * + * @param array $cfgRelation current relation parameters + * @param array $columns_with_unique_index Columns with unique index + * @param mixed $url_params Contains an associative + * array with url params + * @param Index|false $primary_index primary index or false if + * no one exists + * @param array $fields Fields + * @param array $columns_with_index Columns with index + * + * @return string + */ + protected function displayStructure( + array $cfgRelation, + array $columns_with_unique_index, + $url_params, + $primary_index, + array $fields, + array $columns_with_index + ) { + // prepare comments + $comments_map = []; + $mime_map = []; + + if ($GLOBALS['cfg']['ShowPropertyComments']) { + $comments_map = $this->relation->getComments($this->db, $this->table); + if ($cfgRelation['mimework'] && $GLOBALS['cfg']['BrowseMIME']) { + $mime_map = $this->transformations->getMime($this->db, $this->table, true); + } + } + $centralColumns = new CentralColumns($this->dbi); + $central_list = $centralColumns->getFromTable( + $this->db, + $this->table + ); + $columns_list = []; + + $titles = [ + 'Change' => Util::getIcon('b_edit', __('Change')), + 'Drop' => Util::getIcon('b_drop', __('Drop')), + 'NoDrop' => Util::getIcon('b_drop', __('Drop')), + 'Primary' => Util::getIcon('b_primary', __('Primary')), + 'Index' => Util::getIcon('b_index', __('Index')), + 'Unique' => Util::getIcon('b_unique', __('Unique')), + 'Spatial' => Util::getIcon('b_spatial', __('Spatial')), + 'IdxFulltext' => Util::getIcon('b_ftext', __('Fulltext')), + 'NoPrimary' => Util::getIcon('bd_primary', __('Primary')), + 'NoIndex' => Util::getIcon('bd_index', __('Index')), + 'NoUnique' => Util::getIcon('bd_unique', __('Unique')), + 'NoSpatial' => Util::getIcon('bd_spatial', __('Spatial')), + 'NoIdxFulltext' => Util::getIcon('bd_ftext', __('Fulltext')), + 'DistinctValues' => Util::getIcon('b_browse', __('Distinct values')), + ]; + + $edit_view_url = ''; + if ($this->_tbl_is_view && ! $this->_db_is_system_schema) { + $edit_view_url = Url::getCommon([ + 'db' => $this->db, + 'table' => $this->table, + ]); + } + + /** + * Displays Space usage and row statistics + */ + // BEGIN - Calc Table Space + // Get valid statistics whatever is the table type + if ($GLOBALS['cfg']['ShowStats']) { + //get table stats in HTML format + $tablestats = $this->getTableStats(); + //returning the response in JSON format to be used by Ajax + $this->response->addJSON('tableStat', $tablestats); + } + // END - Calc Table Space + + $hideStructureActions = false; + if ($GLOBALS['cfg']['HideStructureActions'] === true) { + $hideStructureActions = true; + } + + // logic removed from Template + $rownum = 0; + $columns_list = []; + $attributes = []; + $displayed_fields = []; + $row_comments = []; + $extracted_columnspecs = []; + $collations = []; + foreach ($fields as &$field) { + $rownum += 1; + $columns_list[] = $field['Field']; + + $extracted_columnspecs[$rownum] = Util::extractColumnSpec($field['Type']); + $attributes[$rownum] = $extracted_columnspecs[$rownum]['attribute']; + if (strpos($field['Extra'], 'on update CURRENT_TIMESTAMP') !== false) { + $attributes[$rownum] = 'on update CURRENT_TIMESTAMP'; + } + + $displayed_fields[$rownum] = new stdClass(); + $displayed_fields[$rownum]->text = $field['Field']; + $displayed_fields[$rownum]->icon = ""; + $row_comments[$rownum] = ''; + + if (isset($comments_map[$field['Field']])) { + $displayed_fields[$rownum]->comment = $comments_map[$field['Field']]; + $row_comments[$rownum] = $comments_map[$field['Field']]; + } + + if ($primary_index && $primary_index->hasColumn($field['Field'])) { + $displayed_fields[$rownum]->icon .= + Util::getImage('b_primary', __('Primary')); + } + + if (in_array($field['Field'], $columns_with_index)) { + $displayed_fields[$rownum]->icon .= + Util::getImage('bd_primary', __('Index')); + } + + $collation = Charsets::findCollationByName( + $this->dbi, + $GLOBALS['cfg']['Server']['DisableIS'], + $field['Collation'] ?? '' + ); + if ($collation !== null) { + $collations[$collation->getName()] = [ + 'name' => $collation->getName(), + 'description' => $collation->getDescription(), + ]; + } + } + + $engine = $this->table_obj->getStorageEngine(); + return $this->template->render('table/structure/display_structure', [ + 'url_params' => [ + 'db' => $this->db, + 'table' => $this->table, + ], + 'collations' => $collations, + 'is_foreign_key_supported' => Util::isForeignKeySupported($engine), + 'displayIndexesHtml' => Index::getHtmlForDisplayIndexes(), + 'cfg_relation' => $this->relation->getRelationsParam(), + 'hide_structure_actions' => $hideStructureActions, + 'db' => $this->db, + 'table' => $this->table, + 'db_is_system_schema' => $this->_db_is_system_schema, + 'tbl_is_view' => $this->_tbl_is_view, + 'mime_map' => $mime_map, + 'url_query' => $this->_url_query, + 'titles' => $titles, + 'tbl_storage_engine' => $this->_tbl_storage_engine, + 'primary' => $primary_index, + 'columns_with_unique_index' => $columns_with_unique_index, + 'edit_view_url' => $edit_view_url, + 'columns_list' => $columns_list, + 'table_stats' => isset($tablestats) ? $tablestats : null, + 'fields' => $fields, + 'extracted_columnspecs' => $extracted_columnspecs, + 'columns_with_index' => $columns_with_index, + 'central_list' => $central_list, + 'comments_map' => $comments_map, + 'browse_mime' => $GLOBALS['cfg']['BrowseMIME'], + 'show_column_comments' => $GLOBALS['cfg']['ShowColumnComments'], + 'show_stats' => $GLOBALS['cfg']['ShowStats'], + 'relation_commwork' => $GLOBALS['cfgRelation']['commwork'], + 'relation_mimework' => $GLOBALS['cfgRelation']['mimework'], + 'central_columns_work' => $GLOBALS['cfgRelation']['centralcolumnswork'], + 'mysql_int_version' => $this->dbi->getVersion(), + 'is_mariadb' => $this->dbi->isMariaDB(), + 'pma_theme_image' => $GLOBALS['pmaThemeImage'], + 'text_dir' => $GLOBALS['text_dir'], + 'is_active' => Tracker::isActive(), + 'have_partitioning' => Partition::havePartitioning(), + 'partitions' => Partition::getPartitions($this->db, $this->table), + 'partition_names' => Partition::getPartitionNames($this->db, $this->table), + 'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'], + 'attributes' => $attributes, + 'displayed_fields' => $displayed_fields, + 'row_comments' => $row_comments, + ]); + } + + /** + * Get HTML snippet for display table statistics + * + * @return string + */ + protected function getTableStats() + { + if (empty($this->_showtable)) { + $this->_showtable = $this->dbi->getTable( + $this->db, + $this->table + )->getStatusInfo(null, true); + } + + if (empty($this->_showtable['Data_length'])) { + $this->_showtable['Data_length'] = 0; + } + if (empty($this->_showtable['Index_length'])) { + $this->_showtable['Index_length'] = 0; + } + + $is_innodb = (isset($this->_showtable['Type']) + && $this->_showtable['Type'] == 'InnoDB'); + + $mergetable = $this->table_obj->isMerge(); + + // this is to display for example 261.2 MiB instead of 268k KiB + $max_digits = 3; + $decimals = 1; + list($data_size, $data_unit) = Util::formatByteDown( + $this->_showtable['Data_length'], + $max_digits, + $decimals + ); + if ($mergetable === false) { + list($index_size, $index_unit) = Util::formatByteDown( + $this->_showtable['Index_length'], + $max_digits, + $decimals + ); + } + if (isset($this->_showtable['Data_free'])) { + list($free_size, $free_unit) = Util::formatByteDown( + $this->_showtable['Data_free'], + $max_digits, + $decimals + ); + list($effect_size, $effect_unit) = Util::formatByteDown( + $this->_showtable['Data_length'] + + $this->_showtable['Index_length'] + - $this->_showtable['Data_free'], + $max_digits, + $decimals + ); + } else { + list($effect_size, $effect_unit) = Util::formatByteDown( + $this->_showtable['Data_length'] + + $this->_showtable['Index_length'], + $max_digits, + $decimals + ); + } + list($tot_size, $tot_unit) = Util::formatByteDown( + $this->_showtable['Data_length'] + $this->_showtable['Index_length'], + $max_digits, + $decimals + ); + if ($this->_table_info_num_rows > 0) { + list($avg_size, $avg_unit) = Util::formatByteDown( + ($this->_showtable['Data_length'] + + $this->_showtable['Index_length']) + / $this->_showtable['Rows'], + 6, + 1 + ); + } else { + $avg_size = $avg_unit = ''; + } + /** @var Innodb $innodbEnginePlugin */ + $innodbEnginePlugin = StorageEngine::getEngine('Innodb'); + $innodb_file_per_table = $innodbEnginePlugin->supportsFilePerTable(); + + $engine = $this->dbi->getTable($this->db, $this->table)->getStorageEngine(); + + $tableCollation = []; + $collation = Charsets::findCollationByName( + $this->dbi, + $GLOBALS['cfg']['Server']['DisableIS'], + $this->_tbl_collation + ); + if ($collation !== null) { + $tableCollation = [ + 'name' => $collation->getName(), + 'description' => $collation->getDescription(), + ]; + } + return $this->template->render('table/structure/display_table_stats', [ + 'url_params' => [ + 'db' => $GLOBALS['db'], + 'table' => $GLOBALS['table'], + ], + 'is_foreign_key_supported' => Util::isForeignKeySupported($engine), + 'cfg_relation' => $this->relation->getRelationsParam(), + 'showtable' => $this->_showtable, + 'table_info_num_rows' => $this->_table_info_num_rows, + 'tbl_is_view' => $this->_tbl_is_view, + 'db_is_system_schema' => $this->_db_is_system_schema, + 'tbl_storage_engine' => $this->_tbl_storage_engine, + 'url_query' => $this->_url_query, + 'table_collation' => $tableCollation, + 'is_innodb' => $is_innodb, + 'mergetable' => $mergetable, + 'avg_size' => isset($avg_size) ? $avg_size : null, + 'avg_unit' => isset($avg_unit) ? $avg_unit : null, + 'data_size' => $data_size, + 'data_unit' => $data_unit, + 'index_size' => isset($index_size) ? $index_size : null, + 'index_unit' => isset($index_unit) ? $index_unit : null, + 'innodb_file_per_table' => $innodb_file_per_table, + 'free_size' => isset($free_size) ? $free_size : null, + 'free_unit' => isset($free_unit) ? $free_unit : null, + 'effect_size' => $effect_size, + 'effect_unit' => $effect_unit, + 'tot_size' => $tot_size, + 'tot_unit' => $tot_unit, + 'table' => $GLOBALS['table'], + ]); + } + + /** + * Gets table primary key + * + * @return string + */ + protected function getKeyForTablePrimary() + { + $this->dbi->selectDb($this->db); + $result = $this->dbi->query( + 'SHOW KEYS FROM ' . Util::backquote($this->table) . ';' + ); + $primary = ''; + while ($row = $this->dbi->fetchAssoc($result)) { + // Backups the list of primary keys + if ($row['Key_name'] == 'PRIMARY') { + $primary .= $row['Column_name'] . ', '; + } + } // end while + $this->dbi->freeResult($result); + + return $primary; + } + + /** + * Get List of information for Submit Mult + * + * @param string $submit_mult mult_submit type + * @param array $selected the selected columns + * @param string $action action type + * @param ContainerBuilder $containerBuilder Container builder instance + * + * @return array + */ + protected function getDataForSubmitMult($submit_mult, $selected, $action, ContainerBuilder $containerBuilder) + { + $centralColumns = new CentralColumns($this->dbi); + $what = null; + $query_type = null; + $is_unset_submit_mult = false; + $mult_btn = null; + $centralColsError = null; + switch ($submit_mult) { + case 'drop': + $what = 'drop_fld'; + break; + case 'primary': + // Gets table primary key + $primary = $this->getKeyForTablePrimary(); + if (empty($primary)) { + // no primary key, so we can safely create new + $is_unset_submit_mult = true; + $query_type = 'primary_fld'; + $mult_btn = __('Yes'); + } else { + // primary key exists, so lets as user + $what = 'primary_fld'; + } + break; + case 'index': + $is_unset_submit_mult = true; + $query_type = 'index_fld'; + $mult_btn = __('Yes'); + break; + case 'unique': + $is_unset_submit_mult = true; + $query_type = 'unique_fld'; + $mult_btn = __('Yes'); + break; + case 'spatial': + $is_unset_submit_mult = true; + $query_type = 'spatial_fld'; + $mult_btn = __('Yes'); + break; + case 'ftext': + $is_unset_submit_mult = true; + $query_type = 'fulltext_fld'; + $mult_btn = __('Yes'); + break; + case 'add_to_central_columns': + $centralColsError = $centralColumns->syncUniqueColumns( + $selected, + false + ); + break; + case 'remove_from_central_columns': + $centralColsError = $centralColumns->deleteColumnsFromList( + $_POST['db'], + $selected, + false + ); + break; + case 'change': + $this->displayHtmlForColumnChange($selected, $action, $containerBuilder); + // execution stops here but PhpMyAdmin\Response correctly finishes + // the rendering + exit; + case 'browse': + // this should already be handled by tbl_structure.php + } + + return [ + $what, + $query_type, + $is_unset_submit_mult, + $mult_btn, + $centralColsError, + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/TransformationOverviewController.php b/srcs/phpmyadmin/libraries/classes/Controllers/TransformationOverviewController.php new file mode 100644 index 0000000..621961f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/TransformationOverviewController.php @@ -0,0 +1,80 @@ +transformations = $transformations; + } + + /** + * @return string HTML + */ + public function indexAction(): string + { + $types = $this->transformations->getAvailableMimeTypes(); + + $mimeTypes = []; + foreach ($types['mimetype'] as $mimeType) { + $mimeTypes[] = [ + 'name' => $mimeType, + 'is_empty' => isset($types['empty_mimetype'][$mimeType]), + ]; + } + + $transformations = [ + 'transformation' => [], + 'input_transformation' => [], + ]; + + foreach (array_keys($transformations) as $type) { + foreach ($types[$type] as $key => $transformation) { + $transformations[$type][] = [ + 'name' => $transformation, + 'description' => $this->transformations->getDescription( + $types[$type . '_file'][$key] + ), + ]; + } + } + + return $this->template->render('transformation_overview', [ + 'mime_types' => $mimeTypes, + 'transformations' => $transformations, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Core.php b/srcs/phpmyadmin/libraries/classes/Core.php new file mode 100644 index 0000000..581afbc --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Core.php @@ -0,0 +1,1302 @@ + + * // $_REQUEST['db'] not set + * echo Core::ifSetOr($_REQUEST['db'], ''); // '' + * // $_POST['sql_query'] not set + * echo Core::ifSetOr($_POST['sql_query']); // null + * // $cfg['EnableFoo'] not set + * echo Core::ifSetOr($cfg['EnableFoo'], false, 'boolean'); // false + * echo Core::ifSetOr($cfg['EnableFoo']); // null + * // $cfg['EnableFoo'] set to 1 + * echo Core::ifSetOr($cfg['EnableFoo'], false, 'boolean'); // false + * echo Core::ifSetOr($cfg['EnableFoo'], false, 'similar'); // 1 + * echo Core::ifSetOr($cfg['EnableFoo'], false); // 1 + * // $cfg['EnableFoo'] set to true + * echo Core::ifSetOr($cfg['EnableFoo'], false, 'boolean'); // true + * + * + * @param mixed $var param to check + * @param mixed $default default value + * @param mixed $type var type or array of values to check against $var + * + * @return mixed $var or $default + * + * @see self::isValid() + */ + public static function ifSetOr(&$var, $default = null, $type = 'similar') + { + if (! self::isValid($var, $type, $default)) { + return $default; + } + + return $var; + } + + /** + * checks given $var against $type or $compare + * + * $type can be: + * - false : no type checking + * - 'scalar' : whether type of $var is integer, float, string or boolean + * - 'numeric' : whether type of $var is any number representation + * - 'length' : whether type of $var is scalar with a string length > 0 + * - 'similar' : whether type of $var is similar to type of $compare + * - 'equal' : whether type of $var is identical to type of $compare + * - 'identical' : whether $var is identical to $compare, not only the type! + * - or any other valid PHP variable type + * + * + * // $_REQUEST['doit'] = true; + * Core::isValid($_REQUEST['doit'], 'identical', 'true'); // false + * // $_REQUEST['doit'] = 'true'; + * Core::isValid($_REQUEST['doit'], 'identical', 'true'); // true + * + * + * NOTE: call-by-reference is used to not get NOTICE on undefined vars, + * but the var is not altered inside this function, also after checking a var + * this var exists nut is not set, example: + * + * // $var is not set + * isset($var); // false + * functionCallByReference($var); // false + * isset($var); // true + * functionCallByReference($var); // true + * + * + * to avoid this we set this var to null if not isset + * + * @param mixed $var variable to check + * @param mixed $type var type or array of valid values to check against $var + * @param mixed $compare var to compare with $var + * + * @return boolean whether valid or not + * + * @todo add some more var types like hex, bin, ...? + * @see https://secure.php.net/gettype + */ + public static function isValid(&$var, $type = 'length', $compare = null): bool + { + if (! isset($var)) { + // var is not even set + return false; + } + + if ($type === false) { + // no vartype requested + return true; + } + + if (is_array($type)) { + return in_array($var, $type); + } + + // allow some aliases of var types + $type = strtolower($type); + switch ($type) { + case 'identic': + $type = 'identical'; + break; + case 'len': + $type = 'length'; + break; + case 'bool': + $type = 'boolean'; + break; + case 'float': + $type = 'double'; + break; + case 'int': + $type = 'integer'; + break; + case 'null': + $type = 'NULL'; + break; + } + + if ($type === 'identical') { + return $var === $compare; + } + + // whether we should check against given $compare + if ($type === 'similar') { + switch (gettype($compare)) { + case 'string': + case 'boolean': + $type = 'scalar'; + break; + case 'integer': + case 'double': + $type = 'numeric'; + break; + default: + $type = gettype($compare); + } + } elseif ($type === 'equal') { + $type = gettype($compare); + } + + // do the check + if ($type === 'length' || $type === 'scalar') { + $is_scalar = is_scalar($var); + if ($is_scalar && $type === 'length') { + return strlen((string) $var) > 0; + } + return $is_scalar; + } + + if ($type === 'numeric') { + return is_numeric($var); + } + + return gettype($var) === $type; + } + + /** + * Removes insecure parts in a path; used before include() or + * require() when a part of the path comes from an insecure source + * like a cookie or form. + * + * @param string $path The path to check + * + * @return string The secured path + * + * @access public + */ + public static function securePath(string $path): string + { + // change .. to . + return preg_replace('@\.\.*@', '.', $path); + } // end function + + /** + * displays the given error message on phpMyAdmin error page in foreign language, + * ends script execution and closes session + * + * loads language file if not loaded already + * + * @param string $error_message the error message or named error message + * @param string|array $message_args arguments applied to $error_message + * + * @return void + */ + public static function fatalError( + string $error_message, + $message_args = null + ): void { + /* Use format string if applicable */ + if (is_string($message_args)) { + $error_message = sprintf($error_message, $message_args); + } elseif (is_array($message_args)) { + $error_message = vsprintf($error_message, $message_args); + } + + /* + * Avoid using Response class as config does not have to be loaded yet + * (this can happen on early fatal error) + */ + if (isset($GLOBALS['dbi']) && $GLOBALS['dbi'] !== null && isset($GLOBALS['PMA_Config']) && $GLOBALS['PMA_Config']->get('is_setup') === false && Response::getInstance()->isAjax()) { + $response = Response::getInstance(); + $response->setRequestStatus(false); + $response->addJSON('message', Message::error($error_message)); + } elseif (! empty($_REQUEST['ajax_request'])) { + // Generate JSON manually + self::headerJSON(); + echo json_encode( + [ + 'success' => false, + 'message' => Message::error($error_message)->getDisplay(), + ] + ); + } else { + $error_message = strtr($error_message, ['
' => '[br]']); + $error_header = __('Error'); + $lang = isset($GLOBALS['lang']) ? $GLOBALS['lang'] : 'en'; + $dir = isset($GLOBALS['text_dir']) ? $GLOBALS['text_dir'] : 'ltr'; + + echo DisplayError::display(new Template(), $lang, $dir, $error_header, $error_message); + } + if (! defined('TESTSUITE')) { + exit; + } + } + + /** + * Returns a link to the PHP documentation + * + * @param string $target anchor in documentation + * + * @return string the URL + * + * @access public + */ + public static function getPHPDocLink(string $target): string + { + /* List of PHP documentation translations */ + $php_doc_languages = [ + 'pt_BR', + 'zh', + 'fr', + 'de', + 'it', + 'ja', + 'pl', + 'ro', + 'ru', + 'fa', + 'es', + 'tr', + ]; + + $lang = 'en'; + if (in_array($GLOBALS['lang'], $php_doc_languages)) { + $lang = $GLOBALS['lang']; + } + + return self::linkURL('https://secure.php.net/manual/' . $lang . '/' . $target); + } + + /** + * Warn or fail on missing extension. + * + * @param string $extension Extension name + * @param bool $fatal Whether the error is fatal. + * @param string $extra Extra string to append to message. + * + * @return void + */ + public static function warnMissingExtension( + string $extension, + bool $fatal = false, + string $extra = '' + ): void { + /* Gettext does not have to be loaded yet here */ + if (function_exists('__')) { + $message = __( + 'The %s extension is missing. Please check your PHP configuration.' + ); + } else { + $message + = 'The %s extension is missing. Please check your PHP configuration.'; + } + $doclink = self::getPHPDocLink('book.' . $extension . '.php'); + $message = sprintf( + $message, + '[a@' . $doclink . '@Documentation][em]' . $extension . '[/em][/a]' + ); + if ($extra != '') { + $message .= ' ' . $extra; + } + if ($fatal) { + self::fatalError($message); + return; + } + + $GLOBALS['error_handler']->addError( + $message, + E_USER_WARNING, + '', + '', + false + ); + } + + /** + * returns count of tables in given db + * + * @param string $db database to count tables for + * + * @return integer count of tables in $db + */ + public static function getTableCount(string $db): int + { + $tables = $GLOBALS['dbi']->tryQuery( + 'SHOW TABLES FROM ' . Util::backquote($db) . ';', + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + if ($tables) { + $num_tables = $GLOBALS['dbi']->numRows($tables); + $GLOBALS['dbi']->freeResult($tables); + } else { + $num_tables = 0; + } + + return $num_tables; + } + + /** + * Converts numbers like 10M into bytes + * Used with permission from Moodle (https://moodle.org) by Martin Dougiamas + * (renamed with PMA prefix to avoid double definition when embedded + * in Moodle) + * + * @param string|int $size size (Default = 0) + * + * @return integer + */ + public static function getRealSize($size = 0): int + { + if (! $size) { + return 0; + } + + $binaryprefixes = [ + 'T' => 1099511627776, + 't' => 1099511627776, + 'G' => 1073741824, + 'g' => 1073741824, + 'M' => 1048576, + 'm' => 1048576, + 'K' => 1024, + 'k' => 1024, + ]; + + if (preg_match('/^([0-9]+)([KMGT])/i', $size, $matches)) { + return $matches[1] * $binaryprefixes[$matches[2]]; + } + + return (int) $size; + } // end getRealSize() + + /** + * Checks given $page against given $whitelist and returns true if valid + * it optionally ignores query parameters in $page (script.php?ignored) + * + * @param string $page page to check + * @param array $whitelist whitelist to check page against + * @param boolean $include whether the page is going to be included + * + * @return boolean whether $page is valid or not (in $whitelist or not) + */ + public static function checkPageValidity(&$page, array $whitelist = [], $include = false): bool + { + if (empty($whitelist)) { + $whitelist = self::$goto_whitelist; + } + if (empty($page)) { + return false; + } + + if (in_array($page, $whitelist)) { + return true; + } + if ($include) { + return false; + } + + $_page = mb_substr( + $page, + 0, + mb_strpos($page . '?', '?') + ); + if (in_array($_page, $whitelist)) { + return true; + } + + $_page = urldecode($page); + $_page = mb_substr( + $_page, + 0, + mb_strpos($_page . '?', '?') + ); + if (in_array($_page, $whitelist)) { + return true; + } + + return false; + } + + /** + * tries to find the value for the given environment variable name + * + * searches in $_SERVER, $_ENV then tries getenv() and apache_getenv() + * in this order + * + * @param string $var_name variable name + * + * @return string value of $var or empty string + */ + public static function getenv(string $var_name): string + { + if (isset($_SERVER[$var_name])) { + return (string) $_SERVER[$var_name]; + } + + if (isset($_ENV[$var_name])) { + return (string) $_ENV[$var_name]; + } + + if (getenv($var_name)) { + return getenv($var_name); + } + + if (function_exists('apache_getenv') + && apache_getenv($var_name, true) + ) { + return apache_getenv($var_name, true); + } + + return ''; + } + + /** + * Send HTTP header, taking IIS limits into account (600 seems ok) + * + * @param string $uri the header to send + * @param bool $use_refresh whether to use Refresh: header when running on IIS + * + * @return void + */ + public static function sendHeaderLocation(string $uri, bool $use_refresh = false): void + { + if ($GLOBALS['PMA_Config']->get('PMA_IS_IIS') && mb_strlen($uri) > 600) { + Response::getInstance()->disable(); + + $template = new Template(); + echo $template->render('header_location', ['uri' => $uri]); + + return; + } + + /* + * Avoid relative path redirect problems in case user entered URL + * like /phpmyadmin/index.php/ which some web servers happily accept. + */ + if ($uri[0] == '.') { + $uri = $GLOBALS['PMA_Config']->getRootPath() . substr($uri, 2); + } + + $response = Response::getInstance(); + + session_write_close(); + if ($response->headersSent()) { + trigger_error( + 'Core::sendHeaderLocation called when headers are already sent!', + E_USER_ERROR + ); + } + // bug #1523784: IE6 does not like 'Refresh: 0', it + // results in a blank page + // but we need it when coming from the cookie login panel) + if ($GLOBALS['PMA_Config']->get('PMA_IS_IIS') && $use_refresh) { + $response->header('Refresh: 0; ' . $uri); + } else { + $response->header('Location: ' . $uri); + } + } + + /** + * Outputs application/json headers. This includes no caching. + * + * @return void + */ + public static function headerJSON(): void + { + if (defined('TESTSUITE')) { + return; + } + // No caching + self::noCacheHeader(); + // MIME type + header('Content-Type: application/json; charset=UTF-8'); + // Disable content sniffing in browser + // This is needed in case we include HTML in JSON, browser might assume it's + // html to display + header('X-Content-Type-Options: nosniff'); + } + + /** + * Outputs headers to prevent caching in browser (and on the way). + * + * @return void + */ + public static function noCacheHeader(): void + { + if (defined('TESTSUITE')) { + return; + } + // rfc2616 - Section 14.21 + header('Expires: ' . gmdate(DATE_RFC1123)); + // HTTP/1.1 + header( + 'Cache-Control: no-store, no-cache, must-revalidate,' + . ' pre-check=0, post-check=0, max-age=0' + ); + + header('Pragma: no-cache'); // HTTP/1.0 + // test case: exporting a database into a .gz file with Safari + // would produce files not having the current time + // (added this header for Safari but should not harm other browsers) + header('Last-Modified: ' . gmdate(DATE_RFC1123)); + } + + + /** + * Sends header indicating file download. + * + * @param string $filename Filename to include in headers if empty, + * none Content-Disposition header will be sent. + * @param string $mimetype MIME type to include in headers. + * @param int $length Length of content (optional) + * @param bool $no_cache Whether to include no-caching headers. + * + * @return void + */ + public static function downloadHeader( + string $filename, + string $mimetype, + int $length = 0, + bool $no_cache = true + ): void { + if ($no_cache) { + self::noCacheHeader(); + } + /* Replace all possibly dangerous chars in filename */ + $filename = Sanitize::sanitizeFilename($filename); + if (! empty($filename)) { + header('Content-Description: File Transfer'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + } + header('Content-Type: ' . $mimetype); + // inform the server that compression has been done, + // to avoid a double compression (for example with Apache + mod_deflate) + $notChromeOrLessThan43 = PMA_USR_BROWSER_AGENT != 'CHROME' // see bug #4942 + || (PMA_USR_BROWSER_AGENT == 'CHROME' && PMA_USR_BROWSER_VER < 43); + if (strpos($mimetype, 'gzip') !== false && $notChromeOrLessThan43) { + header('Content-Encoding: gzip'); + } + header('Content-Transfer-Encoding: binary'); + if ($length > 0) { + header('Content-Length: ' . $length); + } + } + + /** + * Returns value of an element in $array given by $path. + * $path is a string describing position of an element in an associative array, + * eg. Servers/1/host refers to $array[Servers][1][host] + * + * @param string $path path in the array + * @param array $array the array + * @param mixed $default default value + * + * @return mixed array element or $default + */ + public static function arrayRead(string $path, array $array, $default = null) + { + $keys = explode('/', $path); + $value =& $array; + foreach ($keys as $key) { + if (! isset($value[$key])) { + return $default; + } + $value =& $value[$key]; + } + return $value; + } + + /** + * Stores value in an array + * + * @param string $path path in the array + * @param array $array the array + * @param mixed $value value to store + * + * @return void + */ + public static function arrayWrite(string $path, array &$array, $value): void + { + $keys = explode('/', $path); + $last_key = array_pop($keys); + $a =& $array; + foreach ($keys as $key) { + if (! isset($a[$key])) { + $a[$key] = []; + } + $a =& $a[$key]; + } + $a[$last_key] = $value; + } + + /** + * Removes value from an array + * + * @param string $path path in the array + * @param array $array the array + * + * @return void + */ + public static function arrayRemove(string $path, array &$array): void + { + $keys = explode('/', $path); + $keys_last = array_pop($keys); + $path = []; + $depth = 0; + + $path[0] =& $array; + $found = true; + // go as deep as required or possible + foreach ($keys as $key) { + if (! isset($path[$depth][$key])) { + $found = false; + break; + } + $depth++; + $path[$depth] =& $path[$depth - 1][$key]; + } + // if element found, remove it + if ($found) { + unset($path[$depth][$keys_last]); + $depth--; + } + + // remove empty nested arrays + for (; $depth >= 0; $depth--) { + if (! isset($path[$depth + 1]) || count($path[$depth + 1]) === 0) { + unset($path[$depth][$keys[$depth]]); + } else { + break; + } + } + } + + /** + * Returns link to (possibly) external site using defined redirector. + * + * @param string $url URL where to go. + * + * @return string URL for a link. + */ + public static function linkURL(string $url): string + { + if (! preg_match('#^https?://#', $url)) { + return $url; + } + + $params = []; + $params['url'] = $url; + + $url = Url::getCommon($params); + //strip off token and such sensitive information. Just keep url. + $arr = parse_url($url); + parse_str($arr["query"], $vars); + $query = http_build_query(["url" => $vars["url"]]); + + if ($GLOBALS['PMA_Config'] !== null && $GLOBALS['PMA_Config']->get('is_setup')) { + $url = '../url.php?' . $query; + } else { + $url = './url.php?' . $query; + } + + return $url; + } + + /** + * Checks whether domain of URL is whitelisted domain or not. + * Use only for URLs of external sites. + * + * @param string $url URL of external site. + * + * @return boolean True: if domain of $url is allowed domain, + * False: otherwise. + */ + public static function isAllowedDomain(string $url): bool + { + $arr = parse_url($url); + // We need host to be set + if (! isset($arr['host']) || strlen($arr['host']) == 0) { + return false; + } + // We do not want these to be present + $blocked = [ + 'user', + 'pass', + 'port', + ]; + foreach ($blocked as $part) { + if (isset($arr[$part]) && strlen((string) $arr[$part]) != 0) { + return false; + } + } + $domain = $arr["host"]; + $domainWhiteList = [ + /* Include current domain */ + $_SERVER['SERVER_NAME'], + /* phpMyAdmin domains */ + 'wiki.phpmyadmin.net', + 'www.phpmyadmin.net', + 'phpmyadmin.net', + 'demo.phpmyadmin.net', + 'docs.phpmyadmin.net', + /* mysql.com domains */ + 'dev.mysql.com', + 'bugs.mysql.com', + /* mariadb domains */ + 'mariadb.org', + 'mariadb.com', + /* php.net domains */ + 'php.net', + 'secure.php.net', + /* Github domains*/ + 'github.com', + 'www.github.com', + /* Percona domains */ + 'www.percona.com', + /* Following are doubtful ones. */ + 'mysqldatabaseadministration.blogspot.com', + ]; + + return in_array($domain, $domainWhiteList); + } + + /** + * Replace some html-unfriendly stuff + * + * @param string $buffer String to process + * + * @return string Escaped and cleaned up text suitable for html + */ + public static function mimeDefaultFunction(string $buffer): string + { + $buffer = htmlspecialchars($buffer); + $buffer = str_replace(' ', '  ', $buffer); + return preg_replace("@((\015\012)|(\015)|(\012))@", '
' . "\n", $buffer); + } + + /** + * Displays SQL query before executing. + * + * @param array|string $query_data Array containing queries or query itself + * + * @return void + */ + public static function previewSQL($query_data): void + { + $retval = '
'; + if (empty($query_data)) { + $retval .= __('No change'); + } elseif (is_array($query_data)) { + foreach ($query_data as $query) { + $retval .= Util::formatSql($query); + } + } else { + $retval .= Util::formatSql($query_data); + } + $retval .= '
'; + $response = Response::getInstance(); + $response->addJSON('sql_data', $retval); + exit; + } + + /** + * recursively check if variable is empty + * + * @param mixed $value the variable + * + * @return bool true if empty + */ + public static function emptyRecursive($value): bool + { + $empty = true; + if (is_array($value)) { + array_walk_recursive( + $value, + function ($item) use (&$empty) { + $empty = $empty && empty($item); + } + ); + } else { + $empty = empty($value); + } + return $empty; + } + + /** + * Creates some globals from $_POST variables matching a pattern + * + * @param array $post_patterns The patterns to search for + * + * @return void + */ + public static function setPostAsGlobal(array $post_patterns): void + { + foreach (array_keys($_POST) as $post_key) { + foreach ($post_patterns as $one_post_pattern) { + if (preg_match($one_post_pattern, $post_key)) { + Migration::getInstance()->setGlobal($post_key, $_POST[$post_key]); + } + } + } + } + + /** + * Creates some globals from $_REQUEST + * + * @param string $param db|table + * + * @return void + */ + public static function setGlobalDbOrTable(string $param): void + { + $value = ''; + if (self::isValid($_REQUEST[$param])) { + $value = $_REQUEST[$param]; + } + Migration::getInstance()->setGlobal($param, $value); + Migration::getInstance()->setGlobal('url_params', [$param => $value] + $GLOBALS['url_params']); + } + + /** + * PATH_INFO could be compromised if set, so remove it from PHP_SELF + * and provide a clean PHP_SELF here + * + * @return void + */ + public static function cleanupPathInfo(): void + { + global $PMA_PHP_SELF; + + $PMA_PHP_SELF = self::getenv('PHP_SELF'); + if (empty($PMA_PHP_SELF)) { + $PMA_PHP_SELF = urldecode(self::getenv('REQUEST_URI')); + } + $_PATH_INFO = self::getenv('PATH_INFO'); + if (! empty($_PATH_INFO) && ! empty($PMA_PHP_SELF)) { + $question_pos = mb_strpos($PMA_PHP_SELF, '?'); + if ($question_pos != false) { + $PMA_PHP_SELF = mb_substr($PMA_PHP_SELF, 0, $question_pos); + } + $path_info_pos = mb_strrpos($PMA_PHP_SELF, $_PATH_INFO); + if ($path_info_pos !== false) { + $path_info_part = mb_substr($PMA_PHP_SELF, $path_info_pos, mb_strlen($_PATH_INFO)); + if ($path_info_part == $_PATH_INFO) { + $PMA_PHP_SELF = mb_substr($PMA_PHP_SELF, 0, $path_info_pos); + } + } + } + + $path = []; + foreach (explode('/', $PMA_PHP_SELF) as $part) { + // ignore parts that have no value + if (empty($part) || $part === '.') { + continue; + } + + if ($part !== '..') { + // cool, we found a new part + $path[] = $part; + } elseif (count($path) > 0) { + // going back up? sure + array_pop($path); + } + // Here we intentionall ignore case where we go too up + // as there is nothing sane to do + } + + $PMA_PHP_SELF = htmlspecialchars('/' . implode('/', $path)); + } + + /** + * Checks that required PHP extensions are there. + * @return void + */ + public static function checkExtensions(): void + { + /** + * Warning about mbstring. + */ + if (! function_exists('mb_detect_encoding')) { + self::warnMissingExtension('mbstring'); + } + + /** + * We really need this one! + */ + if (! function_exists('preg_replace')) { + self::warnMissingExtension('pcre', true); + } + + /** + * JSON is required in several places. + */ + if (! function_exists('json_encode')) { + self::warnMissingExtension('json', true); + } + + /** + * ctype is required for Twig. + */ + if (! function_exists('ctype_alpha')) { + self::warnMissingExtension('ctype', true); + } + + /** + * hash is required for cookie authentication. + */ + if (! function_exists('hash_hmac')) { + self::warnMissingExtension('hash', true); + } + } + + /** + * Gets the "true" IP address of the current user + * + * @return string|bool the ip of the user + * + * @access private + */ + public static function getIp() + { + /* Get the address of user */ + if (empty($_SERVER['REMOTE_ADDR'])) { + /* We do not know remote IP */ + return false; + } + + $direct_ip = $_SERVER['REMOTE_ADDR']; + + /* Do we trust this IP as a proxy? If yes we will use it's header. */ + if (! isset($GLOBALS['cfg']['TrustedProxies'][$direct_ip])) { + /* Return true IP */ + return $direct_ip; + } + + /** + * Parse header in form: + * X-Forwarded-For: client, proxy1, proxy2 + */ + // Get header content + $value = self::getenv($GLOBALS['cfg']['TrustedProxies'][$direct_ip]); + // Grab first element what is client adddress + $value = explode(',', $value)[0]; + // checks that the header contains only one IP address, + $is_ip = filter_var($value, FILTER_VALIDATE_IP); + + if ($is_ip !== false) { + // True IP behind a proxy + return $value; + } + + // We could not parse header + return false; + } // end of the 'getIp()' function + + /** + * Sanitizes MySQL hostname + * + * * strips p: prefix(es) + * + * @param string $name User given hostname + * + * @return string + */ + public static function sanitizeMySQLHost(string $name): string + { + while (strtolower(substr($name, 0, 2)) == 'p:') { + $name = substr($name, 2); + } + + return $name; + } + + /** + * Sanitizes MySQL username + * + * * strips part behind null byte + * + * @param string $name User given username + * + * @return string + */ + public static function sanitizeMySQLUser(string $name): string + { + $position = strpos($name, chr(0)); + if ($position !== false) { + return substr($name, 0, $position); + } + return $name; + } + + /** + * Safe unserializer wrapper + * + * It does not unserialize data containing objects + * + * @param string $data Data to unserialize + * + * @return mixed + */ + public static function safeUnserialize(string $data) + { + if (! is_string($data)) { + return null; + } + + /* validate serialized data */ + $length = strlen($data); + $depth = 0; + for ($i = 0; $i < $length; $i++) { + $value = $data[$i]; + + switch ($value) { + case '}': + /* end of array */ + if ($depth <= 0) { + return null; + } + $depth--; + break; + case 's': + /* string */ + // parse sting length + $strlen = intval(substr($data, $i + 2)); + // string start + $i = strpos($data, ':', $i + 2); + if ($i === false) { + return null; + } + // skip string, quotes and ; + $i += 2 + $strlen + 1; + if ($data[$i] != ';') { + return null; + } + break; + + case 'b': + case 'i': + case 'd': + /* bool, integer or double */ + // skip value to sepearator + $i = strpos($data, ';', $i); + if ($i === false) { + return null; + } + break; + case 'a': + /* array */ + // find array start + $i = strpos($data, '{', $i); + if ($i === false) { + return null; + } + // remember nesting + $depth++; + break; + case 'N': + /* null */ + // skip to end + $i = strpos($data, ';', $i); + if ($i === false) { + return null; + } + break; + default: + /* any other elements are not wanted */ + return null; + } + } + + // check unterminated arrays + if ($depth > 0) { + return null; + } + + return unserialize($data); + } + + /** + * Applies changes to PHP configuration. + * + * @return void + */ + public static function configure(): void + { + /** + * Set utf-8 encoding for PHP + */ + ini_set('default_charset', 'utf-8'); + mb_internal_encoding('utf-8'); + + /** + * Set precision to sane value, with higher values + * things behave slightly unexpectedly, for example + * round(1.2, 2) returns 1.199999999999999956. + */ + ini_set('precision', '14'); + + /** + * check timezone setting + * this could produce an E_WARNING - but only once, + * if not done here it will produce E_WARNING on every date/time function + */ + date_default_timezone_set(@date_default_timezone_get()); + } + + /** + * Check whether PHP configuration matches our needs. + * + * @return void + */ + public static function checkConfiguration(): void + { + /** + * As we try to handle charsets by ourself, mbstring overloads just + * break it, see bug 1063821. + * + * We specifically use empty here as we are looking for anything else than + * empty value or 0. + */ + if (extension_loaded('mbstring') && ! empty(ini_get('mbstring.func_overload'))) { + self::fatalError( + __( + 'You have enabled mbstring.func_overload in your PHP ' + . 'configuration. This option is incompatible with phpMyAdmin ' + . 'and might cause some data to be corrupted!' + ) + ); + } + + /** + * The ini_set and ini_get functions can be disabled using + * disable_functions but we're relying quite a lot of them. + */ + if (! function_exists('ini_get') || ! function_exists('ini_set')) { + self::fatalError( + __( + 'The ini_get and/or ini_set functions are disabled in php.ini. ' + . 'phpMyAdmin requires these functions!' + ) + ); + } + } + + /** + * Checks request and fails with fatal error if something problematic is found + * + * @return void + */ + public static function checkRequest(): void + { + if (isset($_REQUEST['GLOBALS']) || isset($_FILES['GLOBALS'])) { + self::fatalError(__("GLOBALS overwrite attempt")); + } + + /** + * protect against possible exploits - there is no need to have so much variables + */ + if (count($_REQUEST) > 1000) { + self::fatalError(__('possible exploit')); + } + } + + /** + * Sign the sql query using hmac using the session token + * + * @param string $sqlQuery The sql query + * @return string + */ + public static function signSqlQuery($sqlQuery) + { + /** @var array $cfg */ + global $cfg; + return hash_hmac('sha256', $sqlQuery, $_SESSION[' HMAC_secret '] . $cfg['blowfish_secret']); + } + + /** + * Check that the sql query has a valid hmac signature + * + * @param string $sqlQuery The sql query + * @param string $signature The Signature to check + * @return bool + */ + public static function checkSqlQuerySignature($sqlQuery, $signature) + { + /** @var array $cfg */ + global $cfg; + $hmac = hash_hmac('sha256', $sqlQuery, $_SESSION[' HMAC_secret '] . $cfg['blowfish_secret']); + return hash_equals($hmac, $signature); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/CreateAddField.php b/srcs/phpmyadmin/libraries/classes/CreateAddField.php new file mode 100644 index 0000000..1020f10 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/CreateAddField.php @@ -0,0 +1,555 @@ +dbi = $dbi; + } + + /** + * Transforms the radio button field_key into 4 arrays + * + * @return array An array of arrays which represents column keys for each index type + */ + private function getIndexedColumns(): array + { + $fieldCount = count($_POST['field_name']); + $fieldPrimary = json_decode($_POST['primary_indexes'], true); + $fieldIndex = json_decode($_POST['indexes'], true); + $fieldUnique = json_decode($_POST['unique_indexes'], true); + $fieldFullText = json_decode($_POST['fulltext_indexes'], true); + $fieldSpatial = json_decode($_POST['spatial_indexes'], true); + + return [ + $fieldCount, + $fieldPrimary, + $fieldIndex, + $fieldUnique, + $fieldFullText, + $fieldSpatial, + ]; + } + + /** + * Initiate the column creation statement according to the table creation or + * add columns to a existing table + * + * @param int $fieldCount number of columns + * @param boolean $isCreateTable true if requirement is to get the statement + * for table creation + * + * @return array An array of initial sql statements + * according to the request + */ + private function buildColumnCreationStatement( + int $fieldCount, + bool $isCreateTable = true + ): array { + $definitions = []; + $previousField = -1; + for ($i = 0; $i < $fieldCount; ++$i) { + // '0' is also empty for php :-( + if (strlen($_POST['field_name'][$i]) === 0) { + continue; + } + + $definition = $this->getStatementPrefix($isCreateTable) . + Table::generateFieldSpec( + trim($_POST['field_name'][$i]), + $_POST['field_type'][$i], + $_POST['field_length'][$i], + $_POST['field_attribute'][$i], + isset($_POST['field_collation'][$i]) + ? $_POST['field_collation'][$i] + : '', + isset($_POST['field_null'][$i]) + ? $_POST['field_null'][$i] + : 'NO', + $_POST['field_default_type'][$i], + $_POST['field_default_value'][$i], + isset($_POST['field_extra'][$i]) + ? $_POST['field_extra'][$i] + : false, + isset($_POST['field_comments'][$i]) + ? $_POST['field_comments'][$i] + : '', + isset($_POST['field_virtuality'][$i]) + ? $_POST['field_virtuality'][$i] + : '', + isset($_POST['field_expression'][$i]) + ? $_POST['field_expression'][$i] + : '' + ); + + $definition .= $this->setColumnCreationStatementSuffix( + $previousField, + $isCreateTable + ); + $previousField = $i; + $definitions[] = $definition; + } // end for + + return $definitions; + } + + /** + * Set column creation suffix according to requested position of the new column + * + * @param int $previousField previous field for ALTER statement + * @param bool $isCreateTable true if requirement is to get the statement + * for table creation + * + * @return string suffix + */ + private function setColumnCreationStatementSuffix( + int $previousField, + bool $isCreateTable = true + ): string { + // no suffix is needed if request is a table creation + $sqlSuffix = ' '; + if ($isCreateTable) { + return $sqlSuffix; + } + + if ((string) $_POST['field_where'] === 'last') { + return $sqlSuffix; + } + + // Only the first field can be added somewhere other than at the end + if ($previousField == -1) { + if ((string) $_POST['field_where'] === 'first') { + $sqlSuffix .= ' FIRST'; + } elseif (! empty($_POST['after_field'])) { + $sqlSuffix .= ' AFTER ' + . Util::backquote($_POST['after_field']); + } + } else { + $sqlSuffix .= ' AFTER ' + . Util::backquote( + $_POST['field_name'][$previousField] + ); + } + + return $sqlSuffix; + } + + /** + * Create relevant index statements + * + * @param array $index an array of index columns + * @param string $indexChoice index choice that which represents + * the index type of $indexed_fields + * @param boolean $isCreateTable true if requirement is to get the statement + * for table creation + * + * @return array an array of sql statements for indexes + */ + private function buildIndexStatements( + array $index, + string $indexChoice, + bool $isCreateTable = true + ): array { + $statement = []; + if (! count($index)) { + return $statement; + } + + $sqlQuery = $this->getStatementPrefix($isCreateTable) + . ' ' . $indexChoice; + + if (! empty($index['Key_name']) && $index['Key_name'] != 'PRIMARY') { + $sqlQuery .= ' ' . Util::backquote($index['Key_name']); + } + + $indexFields = []; + foreach ($index['columns'] as $key => $column) { + $indexFields[$key] = Util::backquote( + $_POST['field_name'][$column['col_index']] + ); + if ($column['size']) { + $indexFields[$key] .= '(' . $column['size'] . ')'; + } + } + + $sqlQuery .= ' (' . implode(', ', $indexFields) . ')'; + + $keyBlockSizes = $index['Key_block_size']; + if (! empty($keyBlockSizes)) { + $sqlQuery .= " KEY_BLOCK_SIZE = " + . $this->dbi->escapeString($keyBlockSizes); + } + + // specifying index type is allowed only for primary, unique and index only + $type = $index['Index_type']; + if ($index['Index_choice'] != 'SPATIAL' + && $index['Index_choice'] != 'FULLTEXT' + && in_array($type, Index::getIndexTypes()) + ) { + $sqlQuery .= ' USING ' . $type; + } + + $parser = $index['Parser']; + if ($index['Index_choice'] == 'FULLTEXT' && ! empty($parser)) { + $sqlQuery .= " WITH PARSER " . $this->dbi->escapeString($parser); + } + + $comment = $index['Index_comment']; + if (! empty($comment)) { + $sqlQuery .= " COMMENT '" . $this->dbi->escapeString($comment) + . "'"; + } + + $statement[] = $sqlQuery; + + return $statement; + } + + /** + * Statement prefix for the buildColumnCreationStatement() + * + * @param boolean $isCreateTable true if requirement is to get the statement + * for table creation + * + * @return string prefix + */ + private function getStatementPrefix(bool $isCreateTable = true): string + { + $sqlPrefix = " "; + if (! $isCreateTable) { + $sqlPrefix = ' ADD '; + } + return $sqlPrefix; + } + + /** + * Merge index definitions for one type of index + * + * @param array $definitions the index definitions to merge to + * @param boolean $isCreateTable true if requirement is to get the statement + * for table creation + * @param array $indexedColumns the columns for one type of index + * @param string $indexKeyword the index keyword to use in the definition + * + * @return array + */ + private function mergeIndexStatements( + array $definitions, + bool $isCreateTable, + array $indexedColumns, + string $indexKeyword + ): array { + foreach ($indexedColumns as $index) { + $statements = $this->buildIndexStatements( + $index, + " " . $indexKeyword . " ", + $isCreateTable + ); + $definitions = array_merge($definitions, $statements); + } + return $definitions; + } + + /** + * Returns sql statement according to the column and index specifications as + * requested + * + * @param boolean $isCreateTable true if requirement is to get the statement + * for table creation + * + * @return string sql statement + */ + private function getColumnCreationStatements(bool $isCreateTable = true): string + { + $sqlStatement = ""; + list( + $fieldCount, + $fieldPrimary, + $fieldIndex, + $fieldUnique, + $fieldFullText, + $fieldSpatial + ) = $this->getIndexedColumns(); + $definitions = $this->buildColumnCreationStatement( + $fieldCount, + $isCreateTable + ); + + // Builds the PRIMARY KEY statements + $primaryKeyStatements = $this->buildIndexStatements( + isset($fieldPrimary[0]) ? $fieldPrimary[0] : [], + " PRIMARY KEY ", + $isCreateTable + ); + $definitions = array_merge($definitions, $primaryKeyStatements); + + // Builds the INDEX statements + $definitions = $this->mergeIndexStatements( + $definitions, + $isCreateTable, + $fieldIndex, + "INDEX" + ); + + // Builds the UNIQUE statements + $definitions = $this->mergeIndexStatements( + $definitions, + $isCreateTable, + $fieldUnique, + "UNIQUE" + ); + + // Builds the FULLTEXT statements + $definitions = $this->mergeIndexStatements( + $definitions, + $isCreateTable, + $fieldFullText, + "FULLTEXT" + ); + + // Builds the SPATIAL statements + $definitions = $this->mergeIndexStatements( + $definitions, + $isCreateTable, + $fieldSpatial, + "SPATIAL" + ); + + if (count($definitions)) { + $sqlStatement = implode(', ', $definitions); + } + return preg_replace('@, $@', '', $sqlStatement); + } + + /** + * Returns the partitioning clause + * + * @return string partitioning clause + */ + public function getPartitionsDefinition(): string + { + $sqlQuery = ""; + if (! empty($_POST['partition_by']) + && ! empty($_POST['partition_expr']) + && ! empty($_POST['partition_count']) + && $_POST['partition_count'] > 1 + ) { + $sqlQuery .= " PARTITION BY " . $_POST['partition_by'] + . " (" . $_POST['partition_expr'] . ")" + . " PARTITIONS " . $_POST['partition_count']; + } + + if (! empty($_POST['subpartition_by']) + && ! empty($_POST['subpartition_expr']) + && ! empty($_POST['subpartition_count']) + && $_POST['subpartition_count'] > 1 + ) { + $sqlQuery .= " SUBPARTITION BY " . $_POST['subpartition_by'] + . " (" . $_POST['subpartition_expr'] . ")" + . " SUBPARTITIONS " . $_POST['subpartition_count']; + } + + if (! empty($_POST['partitions'])) { + $partitions = []; + foreach ($_POST['partitions'] as $partition) { + $partitions[] = $this->getPartitionDefinition($partition); + } + $sqlQuery .= " (" . implode(", ", $partitions) . ")"; + } + + return $sqlQuery; + } + + /** + * Returns the definition of a partition/subpartition + * + * @param array $partition array of partition/subpartition detiails + * @param boolean $isSubPartition whether a subpartition + * + * @return string partition/subpartition definition + */ + private function getPartitionDefinition( + array $partition, + bool $isSubPartition = false + ): string { + $sqlQuery = " " . ($isSubPartition ? "SUB" : "") . "PARTITION "; + $sqlQuery .= $partition['name']; + + if (! empty($partition['value_type'])) { + $sqlQuery .= " VALUES " . $partition['value_type']; + + if ($partition['value_type'] != 'LESS THAN MAXVALUE') { + $sqlQuery .= " (" . $partition['value'] . ")"; + } + } + + if (! empty($partition['engine'])) { + $sqlQuery .= " ENGINE = " . $partition['engine']; + } + if (! empty($partition['comment'])) { + $sqlQuery .= " COMMENT = '" . $partition['comment'] . "'"; + } + if (! empty($partition['data_directory'])) { + $sqlQuery .= " DATA DIRECTORY = '" . $partition['data_directory'] . "'"; + } + if (! empty($partition['index_directory'])) { + $sqlQuery .= " INDEX_DIRECTORY = '" . $partition['index_directory'] . "'"; + } + if (! empty($partition['max_rows'])) { + $sqlQuery .= " MAX_ROWS = " . $partition['max_rows']; + } + if (! empty($partition['min_rows'])) { + $sqlQuery .= " MIN_ROWS = " . $partition['min_rows']; + } + if (! empty($partition['tablespace'])) { + $sqlQuery .= " TABLESPACE = " . $partition['tablespace']; + } + if (! empty($partition['node_group'])) { + $sqlQuery .= " NODEGROUP = " . $partition['node_group']; + } + + if (! empty($partition['subpartitions'])) { + $subpartitions = []; + foreach ($partition['subpartitions'] as $subpartition) { + $subpartitions[] = $this->getPartitionDefinition( + $subpartition, + true + ); + } + $sqlQuery .= " (" . implode(", ", $subpartitions) . ")"; + } + + return $sqlQuery; + } + + /** + * Function to get table creation sql query + * + * @param string $db database name + * @param string $table table name + * + * @return string + */ + public function getTableCreationQuery(string $db, string $table): string + { + // get column addition statements + $sqlStatement = $this->getColumnCreationStatements(true); + + // Builds the 'create table' statement + $sqlQuery = 'CREATE TABLE ' . Util::backquote($db) . '.' + . Util::backquote(trim($table)) . ' (' . $sqlStatement . ')'; + + // Adds table type, character set, comments and partition definition + if (! empty($_POST['tbl_storage_engine']) + && ($_POST['tbl_storage_engine'] != 'Default') + ) { + $sqlQuery .= ' ENGINE = ' . $_POST['tbl_storage_engine']; + } + if (! empty($_POST['tbl_collation'])) { + $sqlQuery .= Util::getCharsetQueryPart($_POST['tbl_collation']); + } + if (! empty($_POST['connection']) + && ! empty($_POST['tbl_storage_engine']) + && $_POST['tbl_storage_engine'] == 'FEDERATED' + ) { + $sqlQuery .= " CONNECTION = '" + . $this->dbi->escapeString($_POST['connection']) . "'"; + } + if (! empty($_POST['comment'])) { + $sqlQuery .= ' COMMENT = \'' + . $this->dbi->escapeString($_POST['comment']) . '\''; + } + $sqlQuery .= $this->getPartitionsDefinition(); + $sqlQuery .= ';'; + + return $sqlQuery; + } + + /** + * Function to get the number of fields for the table creation form + * + * @return int + */ + public function getNumberOfFieldsFromRequest(): int + { + // Limit to 4096 fields (MySQL maximal value) + $mysqlLimit = 4096; + + if (isset($_POST['submit_num_fields'])) { // adding new fields + $numberOfFields = intval($_POST['orig_num_fields']) + intval($_POST['added_fields']); + } elseif (isset($_POST['orig_num_fields'])) { // retaining existing fields + $numberOfFields = intval($_POST['orig_num_fields']); + } elseif (isset($_POST['num_fields']) + && intval($_POST['num_fields']) > 0 + ) { // new table with specified number of fields + $numberOfFields = intval($_POST['num_fields']); + } else { // new table with unspecified number of fields + $numberOfFields = 4; + } + + return min($numberOfFields, $mysqlLimit); + } + + /** + * Function to execute the column creation statement + * + * @param string $db current database + * @param string $table current table + * @param string $errorUrl error page url + * + * @return array + */ + public function tryColumnCreationQuery( + string $db, + string $table, + string $errorUrl + ): array { + // get column addition statements + $sqlStatement = $this->getColumnCreationStatements(false); + + // To allow replication, we first select the db to use and then run queries + // on this db. + if (! $this->dbi->selectDb($db)) { + Util::mysqlDie( + $this->dbi->getError(), + 'USE ' . Util::backquote($db), + false, + $errorUrl + ); + } + $sqlQuery = 'ALTER TABLE ' . + Util::backquote($table) . ' ' . $sqlStatement . ';'; + // If there is a request for SQL previewing. + if (isset($_POST['preview_sql'])) { + Core::previewSQL($sqlQuery); + } + return [ + $this->dbi->tryQuery($sqlQuery), + $sqlQuery, + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Database/DatabaseList.php b/srcs/phpmyadmin/libraries/classes/Database/DatabaseList.php new file mode 100644 index 0000000..a9d3889 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Database/DatabaseList.php @@ -0,0 +1,60 @@ +getDatabaseList(); + } + + return null; + } + + /** + * Accessor to PMA::$databases + * + * @return ListDatabase + */ + public function getDatabaseList() + { + if (null === $this->databases) { + $this->databases = new ListDatabase(); + } + + return $this->databases; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Database/Designer.php b/srcs/phpmyadmin/libraries/classes/Database/Designer.php new file mode 100644 index 0000000..f9d6775 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Database/Designer.php @@ -0,0 +1,407 @@ +dbi = $dbi; + $this->relation = $relation; + $this->template = $template; + } + + /** + * Function to get html for displaying the page edit/delete form + * + * @param string $db database name + * @param string $operation 'edit' or 'delete' depending on the operation + * + * @return string html content + */ + public function getHtmlForEditOrDeletePages($db, $operation) + { + $cfgRelation = $this->relation->getRelationsParam(); + return $this->template->render('database/designer/edit_delete_pages', [ + 'db' => $db, + 'operation' => $operation, + 'pdfwork' => $cfgRelation['pdfwork'], + 'pages' => $this->getPageIdsAndNames($db), + ]); + } + + /** + * Function to get html for displaying the page save as form + * + * @param string $db database name + * + * @return string html content + */ + public function getHtmlForPageSaveAs($db) + { + $cfgRelation = $this->relation->getRelationsParam(); + return $this->template->render('database/designer/page_save_as', [ + 'db' => $db, + 'pdfwork' => $cfgRelation['pdfwork'], + 'pages' => $this->getPageIdsAndNames($db), + ]); + } + + /** + * Retrieve IDs and names of schema pages + * + * @param string $db database name + * + * @return array array of schema page id and names + */ + private function getPageIdsAndNames($db) + { + $result = []; + $cfgRelation = $this->relation->getRelationsParam(); + if (! $cfgRelation['pdfwork']) { + return $result; + } + + $page_query = "SELECT `page_nr`, `page_descr` FROM " + . Util::backquote($cfgRelation['db']) . "." + . Util::backquote($cfgRelation['pdf_pages']) + . " WHERE db_name = '" . $this->dbi->escapeString($db) . "'" + . " ORDER BY `page_descr`"; + $page_rs = $this->relation->queryAsControlUser( + $page_query, + false, + DatabaseInterface::QUERY_STORE + ); + + while ($curr_page = $this->dbi->fetchAssoc($page_rs)) { + $result[intval($curr_page['page_nr'])] = $curr_page['page_descr']; + } + return $result; + } + + /** + * Function to get html for displaying the schema export + * + * @param string $db database name + * @param int $page the page to be exported + * + * @return string + */ + public function getHtmlForSchemaExport($db, $page) + { + /* Scan for schema plugins */ + /** @var SchemaPlugin[] $export_list */ + $export_list = Plugins::getPlugins( + "schema", + 'libraries/classes/Plugins/Schema/', + null + ); + + /* Fail if we didn't find any schema plugin */ + if (empty($export_list)) { + return Message::error( + __('Could not load schema plugins, please check your installation!') + )->getDisplay(); + } + + return $this->template->render('database/designer/schema_export', [ + 'db' => $db, + 'page' => $page, + 'export_list' => $export_list, + ]); + } + + /** + * Returns array of stored values of Designer Settings + * + * @return array stored values + */ + private function getSideMenuParamsArray() + { + $params = []; + + $cfgRelation = $this->relation->getRelationsParam(); + + if ($cfgRelation['designersettingswork']) { + $query = 'SELECT `settings_data` FROM ' + . Util::backquote($cfgRelation['db']) . '.' + . Util::backquote($cfgRelation['designer_settings']) + . ' WHERE ' . Util::backquote('username') . ' = "' + . $GLOBALS['dbi']->escapeString($GLOBALS['cfg']['Server']['user']) + . '";'; + + $result = $this->dbi->fetchSingleRow($query); + + $params = json_decode((string) $result['settings_data'], true); + } + + return $params; + } + + /** + * Returns class names for various buttons on Designer Side Menu + * + * @return array class names of various buttons + */ + public function returnClassNamesFromMenuButtons() + { + $classes_array = []; + $params_array = $this->getSideMenuParamsArray(); + + if (isset($params_array['angular_direct']) + && $params_array['angular_direct'] == 'angular' + ) { + $classes_array['angular_direct'] = 'M_butt_Selected_down'; + } else { + $classes_array['angular_direct'] = 'M_butt'; + } + + if (isset($params_array['snap_to_grid']) + && $params_array['snap_to_grid'] == 'on' + ) { + $classes_array['snap_to_grid'] = 'M_butt_Selected_down'; + } else { + $classes_array['snap_to_grid'] = 'M_butt'; + } + + if (isset($params_array['pin_text']) + && $params_array['pin_text'] == 'true' + ) { + $classes_array['pin_text'] = 'M_butt_Selected_down'; + } else { + $classes_array['pin_text'] = 'M_butt'; + } + + if (isset($params_array['relation_lines']) + && $params_array['relation_lines'] == 'false' + ) { + $classes_array['relation_lines'] = 'M_butt_Selected_down'; + } else { + $classes_array['relation_lines'] = 'M_butt'; + } + + if (isset($params_array['small_big_all']) + && $params_array['small_big_all'] == 'v' + ) { + $classes_array['small_big_all'] = 'M_butt_Selected_down'; + } else { + $classes_array['small_big_all'] = 'M_butt'; + } + + if (isset($params_array['side_menu']) + && $params_array['side_menu'] == 'true' + ) { + $classes_array['side_menu'] = 'M_butt_Selected_down'; + } else { + $classes_array['side_menu'] = 'M_butt'; + } + + return $classes_array; + } + + /** + * Get HTML to display tables on designer page + * + * @param string $db The database name from the request + * @param DesignerTable[] $designerTables The designer tables + * @param array $tab_pos tables positions + * @param int $display_page page number of the selected page + * @param array $tab_column table column info + * @param array $tables_all_keys all indices + * @param array $tables_pk_or_unique_keys unique or primary indices + * + * @return string html + */ + public function getDatabaseTables( + string $db, + array $designerTables, + array $tab_pos, + $display_page, + array $tab_column, + array $tables_all_keys, + array $tables_pk_or_unique_keys + ) { + $columns_type = []; + foreach ($designerTables as $designerTable) { + $table_name = $designerTable->getDbTableString(); + $limit = count($tab_column[$table_name]['COLUMN_ID']); + for ($j = 0; $j < $limit; $j++) { + $table_column_name = $table_name . '.' . $tab_column[$table_name]['COLUMN_NAME'][$j]; + if (isset($tables_pk_or_unique_keys[$table_column_name])) { + $columns_type[$table_column_name] = 'designer/FieldKey_small'; + } else { + $columns_type[$table_column_name] = 'designer/Field_small'; + if (false !== strpos($tab_column[$table_name]['TYPE'][$j], 'char') + || false !== strpos($tab_column[$table_name]['TYPE'][$j], 'text')) { + $columns_type[$table_column_name] .= '_char'; + } elseif (false !== strpos($tab_column[$table_name]['TYPE'][$j], 'int') + || false !== strpos($tab_column[$table_name]['TYPE'][$j], 'float') + || false !== strpos($tab_column[$table_name]['TYPE'][$j], 'double') + || false !== strpos($tab_column[$table_name]['TYPE'][$j], 'decimal')) { + $columns_type[$table_column_name] .= '_int'; + } elseif (false !== strpos($tab_column[$table_name]['TYPE'][$j], 'date') + || false !== strpos($tab_column[$table_name]['TYPE'][$j], 'time') + || false !== strpos($tab_column[$table_name]['TYPE'][$j], 'year')) { + $columns_type[$table_column_name] .= '_date'; + } + } + } + } + return $this->template->render('database/designer/database_tables', [ + 'db' => $GLOBALS['db'], + 'get_db' => $db, + 'has_query' => isset($_REQUEST['query']), + 'tab_pos' => $tab_pos, + 'display_page' => $display_page, + 'tab_column' => $tab_column, + 'tables_all_keys' => $tables_all_keys, + 'tables_pk_or_unique_keys' => $tables_pk_or_unique_keys, + 'tables' => $designerTables, + 'columns_type' => $columns_type, + 'theme' => $GLOBALS['PMA_Theme'], + ]); + } + + + /** + * Returns HTML for Designer page + * + * @param string $db database in use + * @param string $getDb database in url + * @param DesignerTable[] $designerTables The designer tables + * @param array $scriptTables array on foreign key support for each table + * @param array $scriptContr initialization data array + * @param DesignerTable[] $scriptDisplayField displayed tables in designer with their display fields + * @param int $displayPage page number of the selected page + * @param boolean $hasQuery whether this is visual query builder + * @param string $selectedPage name of the selected page + * @param array $paramsArray array with class name for various buttons on side menu + * @param array|null $tabPos table positions + * @param array $tabColumn table column info + * @param array $tablesAllKeys all indices + * @param array $tablesPkOrUniqueKeys unique or primary indices + * + * @return string html + */ + public function getHtmlForMain( + string $db, + string $getDb, + array $designerTables, + array $scriptTables, + array $scriptContr, + array $scriptDisplayField, + $displayPage, + $hasQuery, + $selectedPage, + array $paramsArray, + ?array $tabPos, + array $tabColumn, + array $tablesAllKeys, + array $tablesPkOrUniqueKeys + ): string { + $cfgRelation = $this->relation->getRelationsParam(); + $columnsType = []; + foreach ($designerTables as $designerTable) { + $tableName = $designerTable->getDbTableString(); + $limit = count($tabColumn[$tableName]['COLUMN_ID']); + for ($j = 0; $j < $limit; $j++) { + $tableColumnName = $tableName . '.' . $tabColumn[$tableName]['COLUMN_NAME'][$j]; + if (isset($tablesPkOrUniqueKeys[$tableColumnName])) { + $columnsType[$tableColumnName] = 'designer/FieldKey_small'; + } else { + $columnsType[$tableColumnName] = 'designer/Field_small'; + if (false !== strpos($tabColumn[$tableName]['TYPE'][$j], 'char') + || false !== strpos($tabColumn[$tableName]['TYPE'][$j], 'text')) { + $columnsType[$tableColumnName] .= '_char'; + } elseif (false !== strpos($tabColumn[$tableName]['TYPE'][$j], 'int') + || false !== strpos($tabColumn[$tableName]['TYPE'][$j], 'float') + || false !== strpos($tabColumn[$tableName]['TYPE'][$j], 'double') + || false !== strpos($tabColumn[$tableName]['TYPE'][$j], 'decimal')) { + $columnsType[$tableColumnName] .= '_int'; + } elseif (false !== strpos($tabColumn[$tableName]['TYPE'][$j], 'date') + || false !== strpos($tabColumn[$tableName]['TYPE'][$j], 'time') + || false !== strpos($tabColumn[$tableName]['TYPE'][$j], 'year')) { + $columnsType[$tableColumnName] .= '_date'; + } + } + } + } + + $displayedFields = []; + foreach ($scriptDisplayField as $designerTable) { + if ($designerTable->getDisplayField() !== null) { + $displayedFields[$designerTable->getTableName()] = $designerTable->getDisplayField(); + } + } + + $designerConfig = new stdClass(); + $designerConfig->db = $db; + $designerConfig->scriptTables = $scriptTables; + $designerConfig->scriptContr = $scriptContr; + $designerConfig->server = $GLOBALS['server']; + $designerConfig->scriptDisplayField = $displayedFields; + $designerConfig->displayPage = (int) $displayPage; + $designerConfig->tablesEnabled = $cfgRelation['pdfwork']; + + return $this->template->render('database/designer/main', [ + 'db' => $db, + 'get_db' => $getDb, + 'designer_config' => json_encode($designerConfig), + 'display_page' => (int) $displayPage, + 'has_query' => $hasQuery, + 'selected_page' => $selectedPage, + 'params_array' => $paramsArray, + 'theme' => $GLOBALS['PMA_Theme'], + 'tab_pos' => $tabPos, + 'tab_column' => $tabColumn, + 'tables_all_keys' => $tablesAllKeys, + 'tables_pk_or_unique_keys' => $tablesPkOrUniqueKeys, + 'designerTables' => $designerTables, + 'columns_type' => $columnsType, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Database/Designer/Common.php b/srcs/phpmyadmin/libraries/classes/Database/Designer/Common.php new file mode 100644 index 0000000..b80f323 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Database/Designer/Common.php @@ -0,0 +1,830 @@ +dbi = $dbi; + $this->relation = $relation; + } + + /** + * Retrieves table info and returns it + * + * @param string $db (optional) Filter only a DB ($table is required if you use $db) + * @param string $table (optional) Filter only a table ($db is now required) + * @return DesignerTable[] with table info + */ + public function getTablesInfo(string $db = null, string $table = null): array + { + $designerTables = []; + $db = ($db === null) ? $GLOBALS['db'] : $db; + // seems to be needed later + $this->dbi->selectDb($db); + if ($db === null && $table === null) { + $tables = $this->dbi->getTablesFull($db); + } else { + $tables = $this->dbi->getTablesFull($db, $table); + } + + foreach ($tables as $one_table) { + $DF = $this->relation->getDisplayField($db, $one_table['TABLE_NAME']); + $DF = is_string($DF) ? $DF : ''; + $DF = ($DF !== '') ? $DF : null; + $designerTables[] = new DesignerTable( + $db, + $one_table['TABLE_NAME'], + is_string($one_table['ENGINE']) ? $one_table['ENGINE'] : '', + $DF + ); + } + + return $designerTables; + } + + /** + * Retrieves table column info + * + * @param DesignerTable[] $designerTables The designer tables + * @return array table column nfo + */ + public function getColumnsInfo(array $designerTables): array + { + //$this->dbi->selectDb($GLOBALS['db']); + $tabColumn = []; + + foreach ($designerTables as $designerTable) { + $fieldsRs = $this->dbi->query( + $this->dbi->getColumnsSql( + $designerTable->getDatabaseName(), + $designerTable->getTableName(), + null, + true + ), + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + $j = 0; + while ($row = $this->dbi->fetchAssoc($fieldsRs)) { + if (! isset($tabColumn[$designerTable->getDbTableString()])) { + $tabColumn[$designerTable->getDbTableString()] = []; + } + $tabColumn[$designerTable->getDbTableString()]['COLUMN_ID'][$j] = $j; + $tabColumn[$designerTable->getDbTableString()]['COLUMN_NAME'][$j] = $row['Field']; + $tabColumn[$designerTable->getDbTableString()]['TYPE'][$j] = $row['Type']; + $tabColumn[$designerTable->getDbTableString()]['NULLABLE'][$j] = $row['Null']; + $j++; + } + } + + return $tabColumn; + } + + /** + * Returns JavaScript code for initializing vars + * + * @param DesignerTable[] $designerTables The designer tables + * @return array JavaScript code + */ + public function getScriptContr(array $designerTables): array + { + $this->dbi->selectDb($GLOBALS['db']); + $con = []; + $con["C_NAME"] = []; + $i = 0; + $alltab_rs = $this->dbi->query( + 'SHOW TABLES FROM ' . Util::backquote($GLOBALS['db']), + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + while ($val = @$this->dbi->fetchRow($alltab_rs)) { + $row = $this->relation->getForeigners($GLOBALS['db'], $val[0], '', 'internal'); + + if ($row !== false) { + foreach ($row as $field => $value) { + $con['C_NAME'][$i] = ''; + $con['DTN'][$i] = rawurlencode($GLOBALS['db'] . "." . $val[0]); + $con['DCN'][$i] = rawurlencode($field); + $con['STN'][$i] = rawurlencode( + $value['foreign_db'] . "." . $value['foreign_table'] + ); + $con['SCN'][$i] = rawurlencode($value['foreign_field']); + $i++; + } + } + $row = $this->relation->getForeigners($GLOBALS['db'], $val[0], '', 'foreign'); + + // We do not have access to the foreign keys if he user has partial access to the columns + if ($row !== false && isset($row['foreign_keys_data'])) { + foreach ($row['foreign_keys_data'] as $one_key) { + foreach ($one_key['index_list'] as $index => $one_field) { + $con['C_NAME'][$i] = rawurlencode($one_key['constraint']); + $con['DTN'][$i] = rawurlencode($GLOBALS['db'] . "." . $val[0]); + $con['DCN'][$i] = rawurlencode($one_field); + $con['STN'][$i] = rawurlencode( + (isset($one_key['ref_db_name']) ? + $one_key['ref_db_name'] : $GLOBALS['db']) + . "." . $one_key['ref_table_name'] + ); + $con['SCN'][$i] = rawurlencode($one_key['ref_index_list'][$index]); + $i++; + } + } + } + } + + $tableDbNames = []; + foreach ($designerTables as $designerTable) { + $tableDbNames[] = $designerTable->getDbTableString(); + } + + $ti = 0; + $retval = []; + for ($i = 0, $cnt = count($con["C_NAME"]); $i < $cnt; $i++) { + $c_name_i = $con['C_NAME'][$i]; + $dtn_i = $con['DTN'][$i]; + $retval[$ti] = []; + $retval[$ti][$c_name_i] = []; + if (in_array($dtn_i, $tableDbNames) && in_array($con['STN'][$i], $tableDbNames)) { + $retval[$ti][$c_name_i][$dtn_i] = []; + $retval[$ti][$c_name_i][$dtn_i][$con['DCN'][$i]] = [ + 0 => $con['STN'][$i], + 1 => $con['SCN'][$i], + ]; + } + $ti++; + } + return $retval; + } + + /** + * Returns UNIQUE and PRIMARY indices + * + * @param DesignerTable[] $designerTables The designer tables + * @return array unique or primary indices + */ + public function getPkOrUniqueKeys(array $designerTables): array + { + return $this->getAllKeys($designerTables, true); + } + + /** + * Returns all indices + * + * @param DesignerTable[] $designerTables The designer tables + * @param bool $unique_only whether to include only unique ones + * + * @return array indices + */ + public function getAllKeys(array $designerTables, bool $unique_only = false): array + { + $keys = []; + + foreach ($designerTables as $designerTable) { + $schema = $designerTable->getDatabaseName(); + // for now, take into account only the first index segment + foreach (Index::getFromTable($designerTable->getTableName(), $schema) as $index) { + if ($unique_only && ! $index->isUnique()) { + continue; + } + $columns = $index->getColumns(); + foreach ($columns as $column_name => $dummy) { + $keys[$schema . '.' . $designerTable->getTableName() . '.' . $column_name] = 1; + } + } + } + return $keys; + } + + /** + * Return j_tab and h_tab arrays + * + * @param DesignerTable[] $designerTables The designer tables + * @return array + */ + public function getScriptTabs(array $designerTables): array + { + $retval = [ + 'j_tabs' => [], + 'h_tabs' => [], + ]; + + foreach ($designerTables as $designerTable) { + $key = rawurlencode($designerTable->getDbTableString()); + $retval['j_tabs'][$key] = $designerTable->supportsForeignkeys() ? 1 : 0; + $retval['h_tabs'][$key] = 1; + } + + return $retval; + } + + /** + * Returns table positions of a given pdf page + * + * @param int $pg pdf page id + * + * @return array|null of table positions + */ + public function getTablePositions($pg): ?array + { + $cfgRelation = $this->relation->getRelationsParam(); + if (! $cfgRelation['pdfwork']) { + return []; + } + + $query = " + SELECT CONCAT_WS('.', `db_name`, `table_name`) AS `name`, + `db_name` as `dbName`, `table_name` as `tableName`, + `x` AS `X`, + `y` AS `Y`, + 1 AS `V`, + 1 AS `H` + FROM " . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['table_coords']) . " + WHERE pdf_page_number = " . intval($pg); + + return $this->dbi->fetchResult( + $query, + 'name', + null, + DatabaseInterface::CONNECT_CONTROL, + DatabaseInterface::QUERY_STORE + ); + } + + /** + * Returns page name of a given pdf page + * + * @param int $pg pdf page id + * + * @return string|null table name + */ + public function getPageName($pg) + { + $cfgRelation = $this->relation->getRelationsParam(); + if (! $cfgRelation['pdfwork']) { + return null; + } + + $query = "SELECT `page_descr`" + . " FROM " . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['pdf_pages']) + . " WHERE " . Util::backquote('page_nr') . " = " . intval($pg); + $page_name = $this->dbi->fetchResult( + $query, + null, + null, + DatabaseInterface::CONNECT_CONTROL, + DatabaseInterface::QUERY_STORE + ); + return ( is_array($page_name) && isset($page_name[0]) ) ? $page_name[0] : null; + } + + /** + * Deletes a given pdf page and its corresponding coordinates + * + * @param int $pg page id + * + * @return boolean success/failure + */ + public function deletePage($pg) + { + $cfgRelation = $this->relation->getRelationsParam(); + if (! $cfgRelation['pdfwork']) { + return false; + } + + $query = "DELETE FROM " . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['table_coords']) + . " WHERE " . Util::backquote('pdf_page_number') . " = " . intval($pg); + $success = $this->relation->queryAsControlUser( + $query, + true, + DatabaseInterface::QUERY_STORE + ); + + if ($success) { + $query = "DELETE FROM " . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['pdf_pages']) + . " WHERE " . Util::backquote('page_nr') . " = " . intval($pg); + $success = $this->relation->queryAsControlUser( + $query, + true, + DatabaseInterface::QUERY_STORE + ); + } + + return (bool) $success; + } + + /** + * Returns the id of the default pdf page of the database. + * Default page is the one which has the same name as the database. + * + * @param string $db database + * + * @return int|null id of the default pdf page for the database + */ + public function getDefaultPage($db): ?int + { + $cfgRelation = $this->relation->getRelationsParam(); + if (! $cfgRelation['pdfwork']) { + return -1; + } + + $query = "SELECT `page_nr`" + . " FROM " . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['pdf_pages']) + . " WHERE `db_name` = '" . $this->dbi->escapeString($db) . "'" + . " AND `page_descr` = '" . $this->dbi->escapeString($db) . "'"; + + $default_page_no = $this->dbi->fetchResult( + $query, + null, + null, + DatabaseInterface::CONNECT_CONTROL, + DatabaseInterface::QUERY_STORE + ); + + if (is_array($default_page_no) && isset($default_page_no[0])) { + return intval($default_page_no[0]); + } + return -1; + } + + /** + * Get the id of the page to load. If a default page exists it will be returned. + * If no such exists, returns the id of the first page of the database. + * + * @param string $db database + * + * @return int id of the page to load + */ + public function getLoadingPage($db) + { + $cfgRelation = $this->relation->getRelationsParam(); + if (! $cfgRelation['pdfwork']) { + return -1; + } + + $page_no = -1; + + $default_page_no = $this->getDefaultPage($db); + if ($default_page_no != -1) { + $page_no = $default_page_no; + } else { + $query = "SELECT MIN(`page_nr`)" + . " FROM " . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['pdf_pages']) + . " WHERE `db_name` = '" . $this->dbi->escapeString($db) . "'"; + + $min_page_no = $this->dbi->fetchResult( + $query, + null, + null, + DatabaseInterface::CONNECT_CONTROL, + DatabaseInterface::QUERY_STORE + ); + if (is_array($min_page_no) && isset($min_page_no[0])) { + $page_no = $min_page_no[0]; + } + } + return intval($page_no); + } + + /** + * Creates a new page and returns its auto-incrementing id + * + * @param string $pageName name of the page + * @param string $db name of the database + * + * @return int|null + */ + public function createNewPage($pageName, $db) + { + $cfgRelation = $this->relation->getRelationsParam(); + if ($cfgRelation['pdfwork']) { + return $this->relation->createPage( + $pageName, + $cfgRelation, + $db + ); + } + return null; + } + + /** + * Saves positions of table(s) of a given pdf page + * + * @param int $pg pdf page id + * + * @return boolean success/failure + */ + public function saveTablePositions($pg) + { + $pageId = $this->dbi->escapeString($pg); + + $db = $this->dbi->escapeString($_POST['db']); + + $cfgRelation = $this->relation->getRelationsParam(); + if (! $cfgRelation['pdfwork']) { + return false; + } + + $query = "DELETE FROM " + . Util::backquote($cfgRelation['db']) + . "." . Util::backquote( + $cfgRelation['table_coords'] + ) + . " WHERE `pdf_page_number` = '" . $pageId . "'"; + + $res = $this->relation->queryAsControlUser( + $query, + true, + DatabaseInterface::QUERY_STORE + ); + + if (! $res) { + return (bool) $res; + } + + foreach ($_POST['t_h'] as $key => $value) { + $DB = $_POST['t_db'][$key]; + $TAB = $_POST['t_tbl'][$key]; + if (! $value) { + continue; + } + + $query = "INSERT INTO " + . Util::backquote($cfgRelation['db']) . "." + . Util::backquote($cfgRelation['table_coords']) + . " (`db_name`, `table_name`, `pdf_page_number`, `x`, `y`)" + . " VALUES (" + . "'" . $this->dbi->escapeString($DB) . "', " + . "'" . $this->dbi->escapeString($TAB) . "', " + . "'" . $pageId . "', " + . "'" . $this->dbi->escapeString($_POST['t_x'][$key]) . "', " + . "'" . $this->dbi->escapeString($_POST['t_y'][$key]) . "')"; + + $res = $this->relation->queryAsControlUser( + $query, + true, + DatabaseInterface::QUERY_STORE + ); + } + + return (bool) $res; + } + + /** + * Saves the display field for a table. + * + * @param string $db database name + * @param string $table table name + * @param string $field display field name + * + * @return array + */ + public function saveDisplayField($db, $table, $field) + { + $cfgRelation = $this->relation->getRelationsParam(); + if (! $cfgRelation['displaywork']) { + return [ + false, + _pgettext( + 'phpMyAdmin configuration storage is not configured for "Display Features" on designer when user tries to set a display field.', + 'phpMyAdmin configuration storage is not configured for "Display Features".' + ), + ]; + } + + $upd_query = new Table($table, $db, $this->dbi); + $upd_query->updateDisplayField($field, $cfgRelation); + + return [ + true, + null, + ]; + } + + /** + * Adds a new foreign relation + * + * @param string $db database name + * @param string $T1 foreign table + * @param string $F1 foreign field + * @param string $T2 master table + * @param string $F2 master field + * @param string $on_delete on delete action + * @param string $on_update on update action + * @param string $DB1 database + * @param string $DB2 database + * + * @return array array of success/failure and message + */ + public function addNewRelation($db, $T1, $F1, $T2, $F2, $on_delete, $on_update, $DB1, $DB2) + { + $tables = $this->dbi->getTablesFull($DB1, $T1); + $type_T1 = mb_strtoupper($tables[$T1]['ENGINE']); + $tables = $this->dbi->getTablesFull($DB2, $T2); + $type_T2 = mb_strtoupper($tables[$T2]['ENGINE']); + + // native foreign key + if (Util::isForeignKeySupported($type_T1) + && Util::isForeignKeySupported($type_T2) + && $type_T1 == $type_T2 + ) { + // relation exists? + $existrel_foreign = $this->relation->getForeigners($DB2, $T2, '', 'foreign'); + $foreigner = $this->relation->searchColumnInForeigners($existrel_foreign, $F2); + if ($foreigner + && isset($foreigner['constraint']) + ) { + return [ + false, + __('Error: relationship already exists.'), + ]; + } + // note: in InnoDB, the index does not requires to be on a PRIMARY + // or UNIQUE key + // improve: check all other requirements for InnoDB relations + $result = $this->dbi->query( + 'SHOW INDEX FROM ' . Util::backquote($DB1) + . '.' . Util::backquote($T1) . ';' + ); + + // will be use to emphasis prim. keys in the table view + $index_array1 = []; + while ($row = $this->dbi->fetchAssoc($result)) { + $index_array1[$row['Column_name']] = 1; + } + $this->dbi->freeResult($result); + + $result = $this->dbi->query( + 'SHOW INDEX FROM ' . Util::backquote($DB2) + . '.' . Util::backquote($T2) . ';' + ); + // will be used to emphasis prim. keys in the table view + $index_array2 = []; + while ($row = $this->dbi->fetchAssoc($result)) { + $index_array2[$row['Column_name']] = 1; + } + $this->dbi->freeResult($result); + + if (! empty($index_array1[$F1]) && ! empty($index_array2[$F2])) { + $upd_query = 'ALTER TABLE ' . Util::backquote($DB2) + . '.' . Util::backquote($T2) + . ' ADD FOREIGN KEY (' + . Util::backquote($F2) . ')' + . ' REFERENCES ' + . Util::backquote($DB1) . '.' + . Util::backquote($T1) . '(' + . Util::backquote($F1) . ')'; + + if ($on_delete != 'nix') { + $upd_query .= ' ON DELETE ' . $on_delete; + } + if ($on_update != 'nix') { + $upd_query .= ' ON UPDATE ' . $on_update; + } + $upd_query .= ';'; + if ($this->dbi->tryQuery($upd_query)) { + return [ + true, + __('FOREIGN KEY relationship has been added.'), + ]; + } + + $error = $this->dbi->getError(); + return [ + false, + __('Error: FOREIGN KEY relationship could not be added!') + . "
" . $error, + ]; + } + + return [ + false, + __('Error: Missing index on column(s).'), + ]; + } + + // internal (pmadb) relation + if ($GLOBALS['cfgRelation']['relwork'] == false) { + return [ + false, + __('Error: Relational features are disabled!'), + ]; + } + + // no need to recheck if the keys are primary or unique at this point, + // this was checked on the interface part + + $q = "INSERT INTO " + . Util::backquote($GLOBALS['cfgRelation']['db']) + . "." + . Util::backquote($GLOBALS['cfgRelation']['relation']) + . "(master_db, master_table, master_field, " + . "foreign_db, foreign_table, foreign_field)" + . " values(" + . "'" . $this->dbi->escapeString($DB2) . "', " + . "'" . $this->dbi->escapeString($T2) . "', " + . "'" . $this->dbi->escapeString($F2) . "', " + . "'" . $this->dbi->escapeString($DB1) . "', " + . "'" . $this->dbi->escapeString($T1) . "', " + . "'" . $this->dbi->escapeString($F1) . "')"; + + if ($this->relation->queryAsControlUser($q, false, DatabaseInterface::QUERY_STORE) + ) { + return [ + true, + __('Internal relationship has been added.'), + ]; + } + + $error = $this->dbi->getError(DatabaseInterface::CONNECT_CONTROL); + return [ + false, + __('Error: Internal relationship could not be added!') + . "
" . $error, + ]; + } + + /** + * Removes a foreign relation + * + * @param string $T1 foreign db.table + * @param string $F1 foreign field + * @param string $T2 master db.table + * @param string $F2 master field + * + * @return array array of success/failure and message + */ + public function removeRelation($T1, $F1, $T2, $F2) + { + list($DB1, $T1) = explode(".", $T1); + list($DB2, $T2) = explode(".", $T2); + + $tables = $this->dbi->getTablesFull($DB1, $T1); + $type_T1 = mb_strtoupper($tables[$T1]['ENGINE']); + $tables = $this->dbi->getTablesFull($DB2, $T2); + $type_T2 = mb_strtoupper($tables[$T2]['ENGINE']); + + if (Util::isForeignKeySupported($type_T1) + && Util::isForeignKeySupported($type_T2) + && $type_T1 == $type_T2 + ) { + // InnoDB + $existrel_foreign = $this->relation->getForeigners($DB2, $T2, '', 'foreign'); + $foreigner = $this->relation->searchColumnInForeigners($existrel_foreign, $F2); + + if (isset($foreigner['constraint'])) { + $upd_query = 'ALTER TABLE ' . Util::backquote($DB2) + . '.' . Util::backquote($T2) . ' DROP FOREIGN KEY ' + . Util::backquote($foreigner['constraint']) . ';'; + if ($this->dbi->query($upd_query)) { + return [ + true, + __('FOREIGN KEY relationship has been removed.'), + ]; + } + + $error = $this->dbi->getError(); + return [ + false, + __('Error: FOREIGN KEY relationship could not be removed!') + . "
" . $error, + ]; + } + } + + // internal relations + $delete_query = "DELETE FROM " + . Util::backquote($GLOBALS['cfgRelation']['db']) . "." + . $GLOBALS['cfgRelation']['relation'] . " WHERE " + . "master_db = '" . $this->dbi->escapeString($DB2) . "'" + . " AND master_table = '" . $this->dbi->escapeString($T2) . "'" + . " AND master_field = '" . $this->dbi->escapeString($F2) . "'" + . " AND foreign_db = '" . $this->dbi->escapeString($DB1) . "'" + . " AND foreign_table = '" . $this->dbi->escapeString($T1) . "'" + . " AND foreign_field = '" . $this->dbi->escapeString($F1) . "'"; + + $result = $this->relation->queryAsControlUser( + $delete_query, + false, + DatabaseInterface::QUERY_STORE + ); + + if (! $result) { + $error = $this->dbi->getError(DatabaseInterface::CONNECT_CONTROL); + return [ + false, + __('Error: Internal relationship could not be removed!') . "
" . $error, + ]; + } + + return [ + true, + __('Internal relationship has been removed.'), + ]; + } + + /** + * Save value for a designer setting + * + * @param string $index setting + * @param string $value value + * + * @return bool whether the operation succeeded + */ + public function saveSetting($index, $value) + { + $cfgRelation = $this->relation->getRelationsParam(); + $success = true; + if ($cfgRelation['designersettingswork']) { + $cfgDesigner = [ + 'user' => $GLOBALS['cfg']['Server']['user'], + 'db' => $cfgRelation['db'], + 'table' => $cfgRelation['designer_settings'], + ]; + + $orig_data_query = "SELECT settings_data" + . " FROM " . Util::backquote($cfgDesigner['db']) + . "." . Util::backquote($cfgDesigner['table']) + . " WHERE username = '" + . $this->dbi->escapeString($cfgDesigner['user']) . "';"; + + $orig_data = $this->dbi->fetchSingleRow( + $orig_data_query, + 'ASSOC', + DatabaseInterface::CONNECT_CONTROL + ); + + if (! empty($orig_data)) { + $orig_data = json_decode($orig_data['settings_data'], true); + $orig_data[$index] = $value; + $orig_data = json_encode($orig_data); + + $save_query = "UPDATE " + . Util::backquote($cfgDesigner['db']) + . "." . Util::backquote($cfgDesigner['table']) + . " SET settings_data = '" . $orig_data . "'" + . " WHERE username = '" + . $this->dbi->escapeString($cfgDesigner['user']) . "';"; + + $success = $this->relation->queryAsControlUser($save_query); + } else { + $save_data = [$index => $value]; + + $query = "INSERT INTO " + . Util::backquote($cfgDesigner['db']) + . "." . Util::backquote($cfgDesigner['table']) + . " (username, settings_data)" + . " VALUES('" . $this->dbi->escapeString($cfgDesigner['user']) + . "', '" . json_encode($save_data) . "');"; + + $success = $this->relation->queryAsControlUser($query); + } + } + + return (bool) $success; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Database/Designer/DesignerTable.php b/srcs/phpmyadmin/libraries/classes/Database/Designer/DesignerTable.php new file mode 100644 index 0000000..a4c1c6f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Database/Designer/DesignerTable.php @@ -0,0 +1,103 @@ +databaseName = $databaseName; + $this->tableName = $tableName; + $this->tableEngine = $tableEngine; + $this->displayField = $displayField; + } + + /** + * The table engine supports or not foreign keys + * + * @return bool + */ + public function supportsForeignkeys(): bool + { + return Util::isForeignKeySupported($this->tableEngine); + } + + /** + * Get the database name + * + * @return string + */ + public function getDatabaseName(): string + { + return $this->databaseName; + } + + /** + * Get the table name + * + * @return string + */ + public function getTableName(): string + { + return $this->tableName; + } + + /** + * Get the table engine + * + * @return string + */ + public function getTableEngine(): string + { + return $this->tableEngine; + } + + /** + * Get the displayed field + * + * @return string + */ + public function getDisplayField() + { + return $this->displayField; + } + + /** + * Get the db and table separated with a dot + * + * @return string + */ + public function getDbTableString(): string + { + return $this->databaseName . '.' . $this->tableName; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Database/MultiTableQuery.php b/srcs/phpmyadmin/libraries/classes/Database/MultiTableQuery.php new file mode 100644 index 0000000..b9fa888 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Database/MultiTableQuery.php @@ -0,0 +1,145 @@ +dbi = $dbi; + $this->db = $dbName; + $this->defaultNoOfColumns = $defaultNoOfColumns; + + $this->template = $template; + + $this->tables = $this->dbi->getTables($this->db); + } + + /** + * Get Multi-Table query page HTML + * + * @return string Multi-Table query page HTML + */ + public function getFormHtml() + { + $tables = []; + foreach ($this->tables as $table) { + $tables[$table]['hash'] = md5($table); + $tables[$table]['columns'] = array_keys( + $this->dbi->getColumns($this->db, $table) + ); + } + return $this->template->render('database/multi_table_query/form', [ + 'db' => $this->db, + 'tables' => $tables, + 'default_no_of_columns' => $this->defaultNoOfColumns, + ]); + } + + /** + * Displays multi-table query results + * + * @param string $sqlQuery The query to parse + * @param string $db The current database + * @param string $pmaThemeImage Uri of the PMA theme image + * + * @return void + */ + public static function displayResults($sqlQuery, $db, $pmaThemeImage) + { + list( + $analyzedSqlResults, + $db, + ) = ParseAnalyze::sqlQuery($sqlQuery, $db); + + extract($analyzedSqlResults); + $goto = 'db_multi_table_query.php'; + $sql = new Sql(); + $sql->executeQueryAndSendQueryResponse( + null, // analyzed_sql_results + false, // is_gotofile + $db, // db + null, // table + null, // find_real_end + null, // sql_query_for_bookmark - see below + null, // extra_data + null, // message_to_show + null, // message + null, // sql_data + $goto, // goto + $pmaThemeImage, // pmaThemeImage + null, // disp_query + null, // disp_message + null, // query_type + $sqlQuery, // sql_query + null, // selectedTables + null // complete_query + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Database/Qbe.php b/srcs/phpmyadmin/libraries/classes/Database/Qbe.php new file mode 100644 index 0000000..27116d0 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Database/Qbe.php @@ -0,0 +1,1963 @@ +_db = $dbname; + $this->_savedSearchList = $savedSearchList; + $this->_currentSearch = $currentSearch; + $this->dbi = $dbi; + $this->relation = $relation; + $this->template = $template; + + $this->_loadCriterias(); + // Sets criteria parameters + $this->_setSearchParams(); + $this->_setCriteriaTablesAndColumns(); + } + + /** + * Initialize criterias + * + * @return static + */ + private function _loadCriterias() + { + if (null === $this->_currentSearch + || null === $this->_currentSearch->getCriterias() + ) { + return $this; + } + + $criterias = $this->_currentSearch->getCriterias(); + $_POST = $criterias + $_POST; + + return $this; + } + + /** + * Getter for current search + * + * @return SavedSearches + */ + private function _getCurrentSearch() + { + return $this->_currentSearch; + } + + /** + * Sets search parameters + * + * @return void + */ + private function _setSearchParams() + { + $criteriaColumnCount = $this->_initializeCriteriasCount(); + + $this->_criteriaColumnInsert = Core::ifSetOr( + $_POST['criteriaColumnInsert'], + null, + 'array' + ); + $this->_criteriaColumnDelete = Core::ifSetOr( + $_POST['criteriaColumnDelete'], + null, + 'array' + ); + + $this->_prev_criteria = isset($_POST['prev_criteria']) + ? $_POST['prev_criteria'] + : []; + $this->_criteria = isset($_POST['criteria']) + ? $_POST['criteria'] + : array_fill(0, $criteriaColumnCount, ''); + + $this->_criteriaRowInsert = isset($_POST['criteriaRowInsert']) + ? $_POST['criteriaRowInsert'] + : array_fill(0, $criteriaColumnCount, ''); + $this->_criteriaRowDelete = isset($_POST['criteriaRowDelete']) + ? $_POST['criteriaRowDelete'] + : array_fill(0, $criteriaColumnCount, ''); + $this->_criteriaAndOrRow = isset($_POST['criteriaAndOrRow']) + ? $_POST['criteriaAndOrRow'] + : array_fill(0, $criteriaColumnCount, ''); + $this->_criteriaAndOrColumn = isset($_POST['criteriaAndOrColumn']) + ? $_POST['criteriaAndOrColumn'] + : array_fill(0, $criteriaColumnCount, ''); + // sets minimum width + $this->_form_column_width = 12; + $this->_formColumns = []; + $this->_formSorts = []; + $this->_formShows = []; + $this->_formCriterions = []; + $this->_formAndOrRows = []; + $this->_formAndOrCols = []; + } + + /** + * Sets criteria tables and columns + * + * @return void + */ + private function _setCriteriaTablesAndColumns() + { + // The tables list sent by a previously submitted form + if (Core::isValid($_POST['TableList'], 'array')) { + foreach ($_POST['TableList'] as $each_table) { + $this->_criteriaTables[$each_table] = ' selected="selected"'; + } + } // end if + $all_tables = $this->dbi->query( + 'SHOW TABLES FROM ' . Util::backquote($this->_db) . ';', + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + $all_tables_count = $this->dbi->numRows($all_tables); + if (0 == $all_tables_count) { + Message::error(__('No tables found in database.'))->display(); + exit; + } + // The tables list gets from MySQL + while (list($table) = $this->dbi->fetchRow($all_tables)) { + $columns = $this->dbi->getColumns($this->_db, $table); + + if (empty($this->_criteriaTables[$table]) + && ! empty($_POST['TableList']) + ) { + $this->_criteriaTables[$table] = ''; + } else { + $this->_criteriaTables[$table] = ' selected="selected"'; + } // end if + + // The fields list per selected tables + if ($this->_criteriaTables[$table] == ' selected="selected"') { + $each_table = Util::backquote($table); + $this->_columnNames[] = $each_table . '.*'; + foreach ($columns as $each_column) { + $each_column = $each_table . '.' + . Util::backquote($each_column['Field']); + $this->_columnNames[] = $each_column; + // increase the width if necessary + $this->_form_column_width = max( + mb_strlen($each_column), + $this->_form_column_width + ); + } // end foreach + } // end if + } // end while + $this->dbi->freeResult($all_tables); + + // sets the largest width found + $this->_realwidth = $this->_form_column_width . 'ex'; + } + /** + * Provides select options list containing column names + * + * @param integer $column_number Column Number (0,1,2) or more + * @param string $selected Selected criteria column name + * + * @return string HTML for select options + */ + private function _showColumnSelectCell($column_number, $selected = '') + { + return $this->template->render('database/qbe/column_select_cell', [ + 'column_number' => $column_number, + 'column_names' => $this->_columnNames, + 'selected' => $selected, + ]); + } + + /** + * Provides select options list containing sort options (ASC/DESC) + * + * @param integer $columnNumber Column Number (0,1,2) or more + * @param string $selected Selected criteria 'ASC' or 'DESC' + * + * @return string HTML for select options + */ + private function _getSortSelectCell( + $columnNumber, + $selected = '' + ) { + return $this->template->render('database/qbe/sort_select_cell', [ + 'real_width' => $this->_realwidth, + 'column_number' => $columnNumber, + 'selected' => $selected, + ]); + } + + /** + * Provides select options list containing sort order + * + * @param integer $columnNumber Column Number (0,1,2) or more + * @param integer $sortOrder Sort order + * + * @return string HTML for select options + */ + private function _getSortOrderSelectCell($columnNumber, $sortOrder) + { + $totalColumnCount = $this->_getNewColumnCount(); + return $this->template->render('database/qbe/sort_order_select_cell', [ + 'total_column_count' => $totalColumnCount, + 'column_number' => $columnNumber, + 'sort_order' => $sortOrder, + ]); + } + + /** + * Returns the new column count after adding and removing columns as instructed + * + * @return int new column count + */ + private function _getNewColumnCount() + { + $totalColumnCount = $this->_criteria_column_count; + if (! empty($this->_criteriaColumnInsert)) { + $totalColumnCount += count($this->_criteriaColumnInsert); + } + if (! empty($this->_criteriaColumnDelete)) { + $totalColumnCount -= count($this->_criteriaColumnDelete); + } + return $totalColumnCount; + } + + /** + * Provides search form's row containing column select options + * + * @return string HTML for search table's row + */ + private function _getColumnNamesRow() + { + $html_output = ''; + $html_output .= '' . __('Column:') . ''; + $new_column_count = 0; + for ($column_index = 0; $column_index < $this->_criteria_column_count; $column_index++) { + if (isset($this->_criteriaColumnInsert[$column_index]) + && $this->_criteriaColumnInsert[$column_index] == 'on' + ) { + $html_output .= $this->_showColumnSelectCell( + $new_column_count + ); + $new_column_count++; + } + if (! empty($this->_criteriaColumnDelete) + && isset($this->_criteriaColumnDelete[$column_index]) + && $this->_criteriaColumnDelete[$column_index] == 'on' + ) { + continue; + } + $selected = ''; + if (isset($_POST['criteriaColumn'][$column_index])) { + $selected = $_POST['criteriaColumn'][$column_index]; + $this->_formColumns[$new_column_count] + = $_POST['criteriaColumn'][$column_index]; + } + $html_output .= $this->_showColumnSelectCell( + $new_column_count, + $selected + ); + $new_column_count++; + } // end for + $this->_new_column_count = $new_column_count; + $html_output .= ''; + return $html_output; + } + + /** + * Provides search form's row containing column aliases + * + * @return string HTML for search table's row + */ + private function _getColumnAliasRow() + { + $html_output = ''; + $html_output .= '' . __('Alias:') . ''; + $new_column_count = 0; + + for ($colInd = 0; $colInd < $this->_criteria_column_count; $colInd++) { + if (! empty($this->_criteriaColumnInsert) + && isset($this->_criteriaColumnInsert[$colInd]) + && $this->_criteriaColumnInsert[$colInd] == 'on' + ) { + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $new_column_count++; + } // end if + + if (! empty($this->_criteriaColumnDelete) + && isset($this->_criteriaColumnDelete[$colInd]) + && $this->_criteriaColumnDelete[$colInd] == 'on' + ) { + continue; + } + + $tmp_alias = ''; + if (! empty($_POST['criteriaAlias'][$colInd])) { + $tmp_alias + = $this->_formAliases[$new_column_count] + = $_POST['criteriaAlias'][$colInd]; + }// end if + + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $new_column_count++; + } // end for + $html_output .= ''; + return $html_output; + } + + /** + * Provides search form's row containing sort(ASC/DESC) select options + * + * @return string HTML for search table's row + */ + private function _getSortRow() + { + $html_output = ''; + $html_output .= '' . __('Sort:') . ''; + $new_column_count = 0; + + for ($colInd = 0; $colInd < $this->_criteria_column_count; $colInd++) { + if (! empty($this->_criteriaColumnInsert) + && isset($this->_criteriaColumnInsert[$colInd]) + && $this->_criteriaColumnInsert[$colInd] == 'on' + ) { + $html_output .= $this->_getSortSelectCell($new_column_count); + $new_column_count++; + } // end if + + if (! empty($this->_criteriaColumnDelete) + && isset($this->_criteriaColumnDelete[$colInd]) + && $this->_criteriaColumnDelete[$colInd] == 'on' + ) { + continue; + } + // If they have chosen all fields using the * selector, + // then sorting is not available, Fix for Bug #570698 + if (isset($_POST['criteriaSort'][$colInd]) + && isset($_POST['criteriaColumn'][$colInd]) + && mb_substr($_POST['criteriaColumn'][$colInd], -2) == '.*' + ) { + $_POST['criteriaSort'][$colInd] = ''; + } //end if + + $selected = ''; + if (isset($_POST['criteriaSort'][$colInd])) { + $this->_formSorts[$new_column_count] + = $_POST['criteriaSort'][$colInd]; + + if ($_POST['criteriaSort'][$colInd] == 'ASC') { + $selected = 'ASC'; + } elseif ($_POST['criteriaSort'][$colInd] == 'DESC') { + $selected = 'DESC'; + } + } else { + $this->_formSorts[$new_column_count] = ''; + } + + $html_output .= $this->_getSortSelectCell( + $new_column_count, + $selected + ); + $new_column_count++; + } // end for + $html_output .= ''; + return $html_output; + } + + /** + * Provides search form's row containing sort order + * + * @return string HTML for search table's row + */ + private function _getSortOrder() + { + $html_output = ''; + $html_output .= '' . __('Sort order:') . ''; + $new_column_count = 0; + + for ($colInd = 0; $colInd < $this->_criteria_column_count; $colInd++) { + if (! empty($this->_criteriaColumnInsert) + && isset($this->_criteriaColumnInsert[$colInd]) + && $this->_criteriaColumnInsert[$colInd] == 'on' + ) { + $html_output .= $this->_getSortOrderSelectCell( + $new_column_count, + null + ); + $new_column_count++; + } // end if + + if (! empty($this->_criteriaColumnDelete) + && isset($this->_criteriaColumnDelete[$colInd]) + && $this->_criteriaColumnDelete[$colInd] == 'on' + ) { + continue; + } + + $sortOrder = null; + if (! empty($_POST['criteriaSortOrder'][$colInd])) { + $sortOrder + = $this->_formSortOrders[$new_column_count] + = $_POST['criteriaSortOrder'][$colInd]; + } + + $html_output .= $this->_getSortOrderSelectCell( + $new_column_count, + $sortOrder + ); + $new_column_count++; + } // end for + $html_output .= ''; + return $html_output; + } + + /** + * Provides search form's row containing SHOW checkboxes + * + * @return string HTML for search table's row + */ + private function _getShowRow() + { + $html_output = ''; + $html_output .= '' . __('Show:') . ''; + $new_column_count = 0; + for ($column_index = 0; $column_index < $this->_criteria_column_count; $column_index++) { + if (! empty($this->_criteriaColumnInsert) + && isset($this->_criteriaColumnInsert[$column_index]) + && $this->_criteriaColumnInsert[$column_index] == 'on' + ) { + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $new_column_count++; + } // end if + if (! empty($this->_criteriaColumnDelete) + && isset($this->_criteriaColumnDelete[$column_index]) + && $this->_criteriaColumnDelete[$column_index] == 'on' + ) { + continue; + } + if (isset($_POST['criteriaShow'][$column_index])) { + $checked_options = ' checked="checked"'; + $this->_formShows[$new_column_count] + = $_POST['criteriaShow'][$column_index]; + } else { + $checked_options = ''; + } + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $new_column_count++; + } // end for + $html_output .= ''; + return $html_output; + } + + /** + * Provides search form's row containing criteria Inputboxes + * + * @return string HTML for search table's row + */ + private function _getCriteriaInputboxRow() + { + $html_output = ''; + $html_output .= '' . __('Criteria:') . ''; + $new_column_count = 0; + for ($column_index = 0; $column_index < $this->_criteria_column_count; $column_index++) { + if (! empty($this->_criteriaColumnInsert) + && isset($this->_criteriaColumnInsert[$column_index]) + && $this->_criteriaColumnInsert[$column_index] == 'on' + ) { + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $new_column_count++; + } // end if + if (! empty($this->_criteriaColumnDelete) + && isset($this->_criteriaColumnDelete[$column_index]) + && $this->_criteriaColumnDelete[$column_index] == 'on' + ) { + continue; + } + $tmp_criteria = ''; + if (isset($this->_criteria[$column_index])) { + $tmp_criteria = $this->_criteria[$column_index]; + } + if ((empty($this->_prev_criteria) + || ! isset($this->_prev_criteria[$column_index])) + || $this->_prev_criteria[$column_index] != htmlspecialchars($tmp_criteria) + ) { + $this->_formCriterions[$new_column_count] = $tmp_criteria; + } else { + $this->_formCriterions[$new_column_count] + = $this->_prev_criteria[$column_index]; + } + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $new_column_count++; + } // end for + $html_output .= ''; + return $html_output; + } + + /** + * Provides footer options for adding/deleting row/columns + * + * @param string $type Whether row or column + * + * @return string HTML for footer options + */ + private function _getFootersOptions($type) + { + return $this->template->render('database/qbe/footer_options', [ + 'type' => $type, + ]); + } + + /** + * Provides search form table's footer options + * + * @return string HTML for table footer + */ + private function _getTableFooters() + { + $html_output = '
'; + $html_output .= $this->_getFootersOptions("row"); + $html_output .= $this->_getFootersOptions("column"); + $html_output .= '
'; + $html_output .= ''; + $html_output .= '
'; + $html_output .= '
'; + return $html_output; + } + + /** + * Provides a select list of database tables + * + * @return string HTML for table select list + */ + private function _getTablesList() + { + $html_output = '
'; + $html_output .= '
'; + $html_output .= '' . __('Use Tables') . ''; + // Build the options list for each table name + $options = ''; + $numTableListOptions = 0; + foreach ($this->_criteriaTables as $key => $val) { + $options .= ''; + $numTableListOptions++; + } + $html_output .= ''; + $html_output .= '
'; + $html_output .= '
'; + $html_output .= ''; + $html_output .= '
'; + $html_output .= '
'; + return $html_output; + } + + /** + * Provides And/Or modification cell along with Insert/Delete options + * (For modifying search form's table columns) + * + * @param integer $column_number Column Number (0,1,2) or more + * @param array|null $selected Selected criteria column name + * @param bool $last_column Whether this is the last column + * + * @return string HTML for modification cell + */ + private function _getAndOrColCell( + $column_number, + $selected = null, + $last_column = false + ) { + $html_output = ''; + if (! $last_column) { + $html_output .= '' . __('Or:') . ''; + $html_output .= ''; + $html_output .= '  ' . __('And:') . ''; + $html_output .= ''; + } + $html_output .= '
' . __('Ins'); + $html_output .= ''; + $html_output .= '  ' . __('Del'); + $html_output .= ''; + $html_output .= ''; + return $html_output; + } + + /** + * Provides search form's row containing column modifications options + * (For modifying search form's table columns) + * + * @return string HTML for search table's row + */ + private function _getModifyColumnsRow() + { + $html_output = ''; + $html_output .= '' . __('Modify:') . ''; + $new_column_count = 0; + for ($column_index = 0; $column_index < $this->_criteria_column_count; $column_index++) { + if (! empty($this->_criteriaColumnInsert) + && isset($this->_criteriaColumnInsert[$column_index]) + && $this->_criteriaColumnInsert[$column_index] == 'on' + ) { + $html_output .= $this->_getAndOrColCell($new_column_count); + $new_column_count++; + } // end if + + if (! empty($this->_criteriaColumnDelete) + && isset($this->_criteriaColumnDelete[$column_index]) + && $this->_criteriaColumnDelete[$column_index] == 'on' + ) { + continue; + } + + if (isset($this->_criteriaAndOrColumn[$column_index])) { + $this->_formAndOrCols[$new_column_count] + = $this->_criteriaAndOrColumn[$column_index]; + } + $checked_options = []; + if (isset($this->_criteriaAndOrColumn[$column_index]) + && $this->_criteriaAndOrColumn[$column_index] == 'or' + ) { + $checked_options['or'] = ' checked="checked"'; + $checked_options['and'] = ''; + } else { + $checked_options['and'] = ' checked="checked"'; + $checked_options['or'] = ''; + } + $html_output .= $this->_getAndOrColCell( + $new_column_count, + $checked_options, + $column_index + 1 == $this->_criteria_column_count + ); + $new_column_count++; + } // end for + $html_output .= ''; + return $html_output; + } + + /** + * Provides Insert/Delete options for criteria inputbox + * with AND/OR relationship modification options + * + * @param integer $row_index Number of criteria row + * @param array $checked_options If checked + * + * @return string HTML + */ + private function _getInsDelAndOrCell($row_index, array $checked_options) + { + $html_output = ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= '
'; + $html_output .= '' . __('Ins:') . ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= '' . __('And:') . ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= '
'; + $html_output .= '' . __('Del:') . ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= '' . __('Or:') . ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= '
'; + $html_output .= ''; + return $html_output; + } + + /** + * Provides rows for criteria inputbox Insert/Delete options + * with AND/OR relationship modification options + * + * @param integer $new_row_index New row index if rows are added/deleted + * + * @return string HTML table rows + */ + private function _getInputboxRow($new_row_index) + { + $html_output = ''; + $new_column_count = 0; + for ($column_index = 0; $column_index < $this->_criteria_column_count; $column_index++) { + if (! empty($this->_criteriaColumnInsert) + && isset($this->_criteriaColumnInsert[$column_index]) + && $this->_criteriaColumnInsert[$column_index] == 'on' + ) { + $orFieldName = 'Or' . $new_row_index . '[' . $new_column_count . ']'; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $new_column_count++; + } // end if + if (! empty($this->_criteriaColumnDelete) + && isset($this->_criteriaColumnDelete[$column_index]) + && $this->_criteriaColumnDelete[$column_index] == 'on' + ) { + continue; + } + $or = 'Or' . $new_row_index; + if (! empty($_POST[$or]) && isset($_POST[$or][$column_index])) { + $tmp_or = $_POST[$or][$column_index]; + } else { + $tmp_or = ''; + } + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + if (! empty(${$or}) && isset(${$or}[$column_index])) { + $GLOBALS[${'cur' . $or}][$new_column_count] + = ${$or}[$column_index]; + } + $new_column_count++; + } // end for + return $html_output; + } + + /** + * Provides rows for criteria inputbox Insert/Delete options + * with AND/OR relationship modification options + * + * @return string HTML table rows + */ + private function _getInsDelAndOrCriteriaRows() + { + $html_output = ''; + $new_row_count = 0; + $checked_options = []; + for ($row_index = 0; $row_index <= $this->_criteria_row_count; $row_index++) { + if (isset($this->_criteriaRowInsert[$row_index]) + && $this->_criteriaRowInsert[$row_index] == 'on' + ) { + $checked_options['or'] = ' checked="checked"'; + $checked_options['and'] = ''; + $html_output .= ''; + $html_output .= $this->_getInsDelAndOrCell( + $new_row_count, + $checked_options + ); + $html_output .= $this->_getInputboxRow( + $new_row_count + ); + $new_row_count++; + $html_output .= ''; + } // end if + if (isset($this->_criteriaRowDelete[$row_index]) + && $this->_criteriaRowDelete[$row_index] == 'on' + ) { + continue; + } + if (isset($this->_criteriaAndOrRow[$row_index])) { + $this->_formAndOrRows[$new_row_count] + = $this->_criteriaAndOrRow[$row_index]; + } + if (isset($this->_criteriaAndOrRow[$row_index]) + && $this->_criteriaAndOrRow[$row_index] == 'and' + ) { + $checked_options['and'] = ' checked="checked"'; + $checked_options['or'] = ''; + } else { + $checked_options['or'] = ' checked="checked"'; + $checked_options['and'] = ''; + } + $html_output .= ''; + $html_output .= $this->_getInsDelAndOrCell( + $new_row_count, + $checked_options + ); + $html_output .= $this->_getInputboxRow( + $new_row_count + ); + $new_row_count++; + $html_output .= ''; + } // end for + $this->_new_row_count = $new_row_count; + return $html_output; + } + + /** + * Provides SELECT clause for building SQL query + * + * @return string Select clause + */ + private function _getSelectClause() + { + $select_clause = ''; + $select_clauses = []; + for ($column_index = 0; $column_index < $this->_criteria_column_count; $column_index++) { + if (! empty($this->_formColumns[$column_index]) + && isset($this->_formShows[$column_index]) + && $this->_formShows[$column_index] == 'on' + ) { + $select = $this->_formColumns[$column_index]; + if (! empty($this->_formAliases[$column_index])) { + $select .= " AS " + . Util::backquote($this->_formAliases[$column_index]); + } + $select_clauses[] = $select; + } + } // end for + if (! empty($select_clauses)) { + $select_clause = 'SELECT ' + . htmlspecialchars(implode(", ", $select_clauses)) . "\n"; + } + return $select_clause; + } + + /** + * Provides WHERE clause for building SQL query + * + * @return string Where clause + */ + private function _getWhereClause() + { + $where_clause = ''; + $criteria_cnt = 0; + for ($column_index = 0; $column_index < $this->_criteria_column_count; $column_index++) { + if (! empty($this->_formColumns[$column_index]) + && ! empty($this->_formCriterions[$column_index]) + && $column_index + && isset($last_where) + && isset($this->_formAndOrCols) + ) { + $where_clause .= ' ' + . mb_strtoupper($this->_formAndOrCols[$last_where]) + . ' '; + } + if (! empty($this->_formColumns[$column_index]) + && ! empty($this->_formCriterions[$column_index]) + ) { + $where_clause .= '(' . $this->_formColumns[$column_index] . ' ' + . $this->_formCriterions[$column_index] . ')'; + $last_where = $column_index; + $criteria_cnt++; + } + } // end for + if ($criteria_cnt > 1) { + $where_clause = '(' . $where_clause . ')'; + } + // OR rows ${'cur' . $or}[$column_index] + if (! isset($this->_formAndOrRows)) { + $this->_formAndOrRows = []; + } + for ($row_index = 0; $row_index <= $this->_criteria_row_count; $row_index++) { + $criteria_cnt = 0; + $qry_orwhere = ''; + $last_orwhere = ''; + for ($column_index = 0; $column_index < $this->_criteria_column_count; $column_index++) { + if (! empty($this->_formColumns[$column_index]) + && ! empty($_POST['Or' . $row_index][$column_index]) + && $column_index + ) { + $qry_orwhere .= ' ' + . mb_strtoupper( + $this->_formAndOrCols[$last_orwhere] + ) + . ' '; + } + if (! empty($this->_formColumns[$column_index]) + && ! empty($_POST['Or' . $row_index][$column_index]) + ) { + $qry_orwhere .= '(' . $this->_formColumns[$column_index] + . ' ' + . $_POST['Or' . $row_index][$column_index] + . ')'; + $last_orwhere = $column_index; + $criteria_cnt++; + } + } // end for + if ($criteria_cnt > 1) { + $qry_orwhere = '(' . $qry_orwhere . ')'; + } + if (! empty($qry_orwhere)) { + $where_clause .= "\n" + . mb_strtoupper( + isset($this->_formAndOrRows[$row_index]) + ? $this->_formAndOrRows[$row_index] . ' ' + : '' + ) + . $qry_orwhere; + } // end if + } // end for + + if (! empty($where_clause) && $where_clause != '()') { + $where_clause = 'WHERE ' . htmlspecialchars($where_clause) . "\n"; + } // end if + return $where_clause; + } + + /** + * Provides ORDER BY clause for building SQL query + * + * @return string Order By clause + */ + private function _getOrderByClause() + { + $orderby_clause = ''; + $orderby_clauses = []; + + // Create copy of instance variables + $columns = $this->_formColumns; + $sort = $this->_formSorts; + $sortOrder = $this->_formSortOrders; + if (! empty($sortOrder) + && count($sortOrder) == count($sort) + && count($sortOrder) == count($columns) + ) { + // Sort all three arrays based on sort order + array_multisort($sortOrder, $sort, $columns); + } + + for ($column_index = 0; $column_index < $this->_criteria_column_count; $column_index++) { + // if all columns are chosen with * selector, + // then sorting isn't available + // Fix for Bug #570698 + if (empty($columns[$column_index]) + && empty($sort[$column_index]) + ) { + continue; + } + + if (mb_substr($columns[$column_index], -2) == '.*') { + continue; + } + + if (! empty($sort[$column_index])) { + $orderby_clauses[] = $columns[$column_index] . ' ' + . $sort[$column_index]; + } + } // end for + if (! empty($orderby_clauses)) { + $orderby_clause = 'ORDER BY ' + . htmlspecialchars(implode(", ", $orderby_clauses)) . "\n"; + } + return $orderby_clause; + } + + /** + * Provides UNIQUE columns and INDEX columns present in criteria tables + * + * @param array $search_tables Tables involved in the search + * @param array $search_columns Columns involved in the search + * @param array $where_clause_columns Columns having criteria where clause + * + * @return array having UNIQUE and INDEX columns + */ + private function _getIndexes( + array $search_tables, + array $search_columns, + array $where_clause_columns + ) { + $unique_columns = []; + $index_columns = []; + + foreach ($search_tables as $table) { + $indexes = $this->dbi->getTableIndexes($this->_db, $table); + foreach ($indexes as $index) { + $column = $table . '.' . $index['Column_name']; + if (isset($search_columns[$column])) { + if ($index['Non_unique'] == 0) { + if (isset($where_clause_columns[$column])) { + $unique_columns[$column] = 'Y'; + } else { + $unique_columns[$column] = 'N'; + } + } else { + if (isset($where_clause_columns[$column])) { + $index_columns[$column] = 'Y'; + } else { + $index_columns[$column] = 'N'; + } + } + } + } // end while (each index of a table) + } // end while (each table) + + return [ + 'unique' => $unique_columns, + 'index' => $index_columns, + ]; + } + + /** + * Provides UNIQUE columns and INDEX columns present in criteria tables + * + * @param array $search_tables Tables involved in the search + * @param array $search_columns Columns involved in the search + * @param array $where_clause_columns Columns having criteria where clause + * + * @return array having UNIQUE and INDEX columns + */ + private function _getLeftJoinColumnCandidates( + array $search_tables, + array $search_columns, + array $where_clause_columns + ) { + $this->dbi->selectDb($this->_db); + + // Get unique columns and index columns + $indexes = $this->_getIndexes( + $search_tables, + $search_columns, + $where_clause_columns + ); + $unique_columns = $indexes['unique']; + $index_columns = $indexes['index']; + + list($candidate_columns, $needsort) + = $this->_getLeftJoinColumnCandidatesBest( + $search_tables, + $where_clause_columns, + $unique_columns, + $index_columns + ); + + // If we came up with $unique_columns (very good) or $index_columns (still + // good) as $candidate_columns we want to check if we have any 'Y' there + // (that would mean that they were also found in the whereclauses + // which would be great). if yes, we take only those + if ($needsort != 1) { + return $candidate_columns; + } + + $very_good = []; + $still_good = []; + foreach ($candidate_columns as $column => $is_where) { + $table = explode('.', $column); + $table = $table[0]; + if ($is_where == 'Y') { + $very_good[$column] = $table; + } else { + $still_good[$column] = $table; + } + } + if (count($very_good) > 0) { + $candidate_columns = $very_good; + // Candidates restricted in index+where + } else { + $candidate_columns = $still_good; + // None of the candidates where in a where-clause + } + + return $candidate_columns; + } + + /** + * Provides the main table to form the LEFT JOIN clause + * + * @param array $search_tables Tables involved in the search + * @param array $search_columns Columns involved in the search + * @param array $where_clause_columns Columns having criteria where clause + * @param array $where_clause_tables Tables having criteria where clause + * + * @return string table name + */ + private function _getMasterTable( + array $search_tables, + array $search_columns, + array $where_clause_columns, + array $where_clause_tables + ) { + if (count($where_clause_tables) === 1) { + // If there is exactly one column that has a decent where-clause + // we will just use this + return key($where_clause_tables); + } + + // Now let's find out which of the tables has an index + // (When the control user is the same as the normal user + // because he is using one of his databases as pmadb, + // the last db selected is not always the one where we need to work) + $candidate_columns = $this->_getLeftJoinColumnCandidates( + $search_tables, + $search_columns, + $where_clause_columns + ); + + // Generally, we need to display all the rows of foreign (referenced) + // table, whether they have any matching row in child table or not. + // So we select candidate tables which are foreign tables. + $foreign_tables = []; + foreach ($candidate_columns as $one_table) { + $foreigners = $this->relation->getForeigners($this->_db, $one_table); + foreach ($foreigners as $key => $foreigner) { + if ($key != 'foreign_keys_data') { + if (in_array($foreigner['foreign_table'], $candidate_columns)) { + $foreign_tables[$foreigner['foreign_table']] + = $foreigner['foreign_table']; + } + continue; + } + foreach ($foreigner as $one_key) { + if (in_array($one_key['ref_table_name'], $candidate_columns)) { + $foreign_tables[$one_key['ref_table_name']] + = $one_key['ref_table_name']; + } + } + } + } + if (count($foreign_tables)) { + $candidate_columns = $foreign_tables; + } + + // If our array of candidates has more than one member we'll just + // find the smallest table. + // Of course the actual query would be faster if we check for + // the Criteria which gives the smallest result set in its table, + // but it would take too much time to check this + if (! (count($candidate_columns) > 1)) { + // Only one single candidate + return reset($candidate_columns); + } + + // Of course we only want to check each table once + $checked_tables = $candidate_columns; + $tsize = []; + $maxsize = -1; + $result = ''; + foreach ($candidate_columns as $table) { + if ($checked_tables[$table] != 1) { + $_table = new Table($table, $this->_db); + $tsize[$table] = $_table->countRecords(); + $checked_tables[$table] = 1; + } + if ($tsize[$table] > $maxsize) { + $maxsize = $tsize[$table]; + $result = $table; + } + } + // Return largest table + return $result; + } + + /** + * Provides columns and tables that have valid where clause criteria + * + * @return array + */ + private function _getWhereClauseTablesAndColumns() + { + $where_clause_columns = []; + $where_clause_tables = []; + + // Now we need all tables that we have in the where clause + for ($column_index = 0, $nb = count($this->_criteria); $column_index < $nb; $column_index++) { + $current_table = explode('.', $_POST['criteriaColumn'][$column_index]); + if (empty($current_table[0]) || empty($current_table[1])) { + continue; + } // end if + $table = str_replace('`', '', $current_table[0]); + $column = str_replace('`', '', $current_table[1]); + $column = $table . '.' . $column; + // Now we know that our array has the same numbers as $criteria + // we can check which of our columns has a where clause + if (! empty($this->_criteria[$column_index])) { + if (mb_substr($this->_criteria[$column_index], 0, 1) == '=' + || false !== stripos($this->_criteria[$column_index], 'is') + ) { + $where_clause_columns[$column] = $column; + $where_clause_tables[$table] = $table; + } + } // end if + } // end for + return [ + 'where_clause_tables' => $where_clause_tables, + 'where_clause_columns' => $where_clause_columns, + ]; + } + + /** + * Provides FROM clause for building SQL query + * + * @param array $formColumns List of selected columns in the form + * + * @return string FROM clause + */ + private function _getFromClause(array $formColumns) + { + $from_clause = ''; + if (empty($formColumns)) { + return $from_clause; + } + + // Initialize some variables + $search_tables = $search_columns = []; + + // We only start this if we have fields, otherwise it would be dumb + foreach ($formColumns as $value) { + $parts = explode('.', $value); + if (! empty($parts[0]) && ! empty($parts[1])) { + $table = str_replace('`', '', $parts[0]); + $search_tables[$table] = $table; + $search_columns[] = $table . '.' . str_replace( + '`', + '', + $parts[1] + ); + } + } // end while + + // Create LEFT JOINS out of Relations + $from_clause = $this->_getJoinForFromClause( + $search_tables, + $search_columns + ); + + // In case relations are not defined, just generate the FROM clause + // from the list of tables, however we don't generate any JOIN + if (empty($from_clause)) { + // Create cartesian product + $from_clause = implode( + ', ', + array_map([Util::class, 'backquote'], $search_tables) + ); + } + + return $from_clause; + } + + /** + * Formulates the WHERE clause by JOINing tables + * + * @param array $searchTables Tables involved in the search + * @param array $searchColumns Columns involved in the search + * + * @return string table name + */ + private function _getJoinForFromClause(array $searchTables, array $searchColumns) + { + // $relations[master_table][foreign_table] => clause + $relations = []; + + // Fill $relations with inter table relationship data + foreach ($searchTables as $oneTable) { + $this->_loadRelationsForTable($relations, $oneTable); + } + + // Get tables and columns with valid where clauses + $validWhereClauses = $this->_getWhereClauseTablesAndColumns(); + $whereClauseTables = $validWhereClauses['where_clause_tables']; + $whereClauseColumns = $validWhereClauses['where_clause_columns']; + + // Get master table + $master = $this->_getMasterTable( + $searchTables, + $searchColumns, + $whereClauseColumns, + $whereClauseTables + ); + + // Will include master tables and all tables that can be combined into + // a cluster by their relation + $finalized = []; + if (strlen($master) > 0) { + // Add master tables + $finalized[$master] = ''; + } + // Fill the $finalized array with JOIN clauses for each table + $this->_fillJoinClauses($finalized, $relations, $searchTables); + + // JOIN clause + $join = ''; + + // Tables that can not be combined with the table cluster + // which includes master table + $unfinalized = array_diff($searchTables, array_keys($finalized)); + if (count($unfinalized) > 0) { + // We need to look for intermediary tables to JOIN unfinalized tables + // Heuristic to chose intermediary tables is to look for tables + // having relationships with unfinalized tables + foreach ($unfinalized as $oneTable) { + $references = $this->relation->getChildReferences($this->_db, $oneTable); + foreach ($references as $column => $columnReferences) { + foreach ($columnReferences as $reference) { + // Only from this schema + if ($reference['table_schema'] != $this->_db) { + continue; + } + + $table = $reference['table_name']; + + $this->_loadRelationsForTable($relations, $table); + + // Make copies + $tempFinalized = $finalized; + $tempSearchTables = $searchTables; + $tempSearchTables[] = $table; + + // Try joining with the added table + $this->_fillJoinClauses( + $tempFinalized, + $relations, + $tempSearchTables + ); + + $tempUnfinalized = array_diff( + $tempSearchTables, + array_keys($tempFinalized) + ); + // Take greedy approach. + // If the unfinalized count drops we keep the new table + // and switch temporary varibles with the original ones + if (count($tempUnfinalized) < count($unfinalized)) { + $finalized = $tempFinalized; + $searchTables = $tempSearchTables; + } + + // We are done if no unfinalized tables anymore + if (count($tempUnfinalized) === 0) { + break 3; + } + } + } + } + + $unfinalized = array_diff($searchTables, array_keys($finalized)); + // If there are still unfinalized tables + if (count($unfinalized) > 0) { + // Add these tables as cartesian product before joined tables + $join .= implode( + ', ', + array_map([Util::class, 'backquote'], $unfinalized) + ); + } + } + + $first = true; + // Add joined tables + foreach ($finalized as $table => $clause) { + if ($first) { + if (! empty($join)) { + $join .= ", "; + } + $join .= Util::backquote($table); + $first = false; + } else { + $join .= "\n LEFT JOIN " . Util::backquote( + $table + ) . " ON " . $clause; + } + } + + return $join; + } + + /** + * Loads relations for a given table into the $relations array + * + * @param array $relations array of relations + * @param string $oneTable the table + * + * @return void + */ + private function _loadRelationsForTable(array &$relations, $oneTable) + { + $relations[$oneTable] = []; + + $foreigners = $this->relation->getForeigners($GLOBALS['db'], $oneTable); + foreach ($foreigners as $field => $foreigner) { + // Foreign keys data + if ($field == 'foreign_keys_data') { + foreach ($foreigner as $oneKey) { + $clauses = []; + // There may be multiple column relations + foreach ($oneKey['index_list'] as $index => $oneField) { + $clauses[] + = Util::backquote($oneTable) . "." + . Util::backquote($oneField) . " = " + . Util::backquote($oneKey['ref_table_name']) . "." + . Util::backquote($oneKey['ref_index_list'][$index]); + } + // Combine multiple column relations with AND + $relations[$oneTable][$oneKey['ref_table_name']] + = implode(" AND ", $clauses); + } + } else { // Internal relations + $relations[$oneTable][$foreigner['foreign_table']] + = Util::backquote($oneTable) . "." + . Util::backquote($field) . " = " + . Util::backquote($foreigner['foreign_table']) . "." + . Util::backquote($foreigner['foreign_field']); + } + } + } + + /** + * Fills the $finalized arrays with JOIN clauses for each of the tables + * + * @param array $finalized JOIN clauses for each table + * @param array $relations Relations among tables + * @param array $searchTables Tables involved in the search + * + * @return void + */ + private function _fillJoinClauses(array &$finalized, array $relations, array $searchTables) + { + while (true) { + $added = false; + foreach ($searchTables as $masterTable) { + $foreignData = $relations[$masterTable]; + foreach ($foreignData as $foreignTable => $clause) { + if (! isset($finalized[$masterTable]) + && isset($finalized[$foreignTable]) + ) { + $finalized[$masterTable] = $clause; + $added = true; + } elseif (! isset($finalized[$foreignTable]) + && isset($finalized[$masterTable]) + && in_array($foreignTable, $searchTables) + ) { + $finalized[$foreignTable] = $clause; + $added = true; + } + if ($added) { + // We are done if all tables are in $finalized + if (count($finalized) == count($searchTables)) { + return; + } + } + } + } + // If no new tables were added during this iteration, break; + if (! $added) { + return; + } + } + } + + /** + * Provides the generated SQL query + * + * @param array $formColumns List of selected columns in the form + * + * @return string SQL query + */ + private function _getSQLQuery(array $formColumns) + { + $sql_query = ''; + // get SELECT clause + $sql_query .= $this->_getSelectClause(); + // get FROM clause + $from_clause = $this->_getFromClause($formColumns); + if (! empty($from_clause)) { + $sql_query .= 'FROM ' . htmlspecialchars($from_clause) . "\n"; + } + // get WHERE clause + $sql_query .= $this->_getWhereClause(); + // get ORDER BY clause + $sql_query .= $this->_getOrderByClause(); + return $sql_query; + } + + /** + * Provides the generated QBE form + * + * @return string QBE form + */ + public function getSelectionForm() + { + $html_output = ''; + $html_output .= '
'; + $html_output .= '
'; + + if ($GLOBALS['cfgRelation']['savedsearcheswork']) { + $html_output .= $this->_getSavedSearchesField(); + } + + $html_output .= '
'; + $html_output .= ''; + // Get table's elements + $html_output .= $this->_getColumnNamesRow(); + $html_output .= $this->_getColumnAliasRow(); + $html_output .= $this->_getShowRow(); + $html_output .= $this->_getSortRow(); + $html_output .= $this->_getSortOrder(); + $html_output .= $this->_getCriteriaInputboxRow(); + $html_output .= $this->_getInsDelAndOrCriteriaRows(); + $html_output .= $this->_getModifyColumnsRow(); + $html_output .= '
'; + $this->_new_row_count--; + $url_params = []; + $url_params['db'] = $this->_db; + $url_params['criteriaColumnCount'] = $this->_new_column_count; + $url_params['rows'] = $this->_new_row_count; + $html_output .= Url::getHiddenInputs($url_params); + $html_output .= '
'; + $html_output .= '
'; + $html_output .= '
'; + // get footers + $html_output .= $this->_getTableFooters(); + // get tables select list + $html_output .= $this->_getTablesList(); + $html_output .= ''; + $html_output .= '
'; + $html_output .= Url::getHiddenInputs(['db' => $this->_db]); + // get SQL query + $html_output .= '
'; + $html_output .= '
'; + $html_output .= '' + . sprintf( + __('SQL query on database %s:'), + Util::getDbLink($this->_db) + ); + $html_output .= ''; + $text_dir = 'ltr'; + $html_output .= ''; + $html_output .= '
'; + // displays form's footers + $html_output .= '
'; + $html_output .= ''; + $html_output .= ''; + $html_output .= '
'; + $html_output .= '
'; + $html_output .= '
'; + return $html_output; + } + + /** + * Get fields to display + * + * @return string + */ + private function _getSavedSearchesField() + { + $html_output = __('Saved bookmarked search:'); + $html_output .= ' '; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + if (null !== $currentSearchId) { + $html_output .= ''; + $html_output .= ''; + } + + return $html_output; + } + + /** + * Initialize _criteria_column_count + * + * @return int Previous number of columns + */ + private function _initializeCriteriasCount(): int + { + // sets column count + $criteriaColumnCount = Core::ifSetOr( + $_POST['criteriaColumnCount'], + 3, + 'numeric' + ); + $criteriaColumnAdd = Core::ifSetOr( + $_POST['criteriaColumnAdd'], + 0, + 'numeric' + ); + $this->_criteria_column_count = max( + $criteriaColumnCount + $criteriaColumnAdd, + 0 + ); + + // sets row count + $rows = Core::ifSetOr($_POST['rows'], 0, 'numeric'); + $criteriaRowAdd = Core::ifSetOr($_POST['criteriaRowAdd'], 0, 'numeric'); + $this->_criteria_row_count = min( + 100, + max($rows + $criteriaRowAdd, 0) + ); + + return (int) $criteriaColumnCount; + } + + /** + * Get best + * + * @param array $search_tables Tables involved in the search + * @param array|null $where_clause_columns Columns with where clause + * @param array|null $unique_columns Unique columns + * @param array|null $index_columns Indexed columns + * + * @return array + */ + private function _getLeftJoinColumnCandidatesBest( + array $search_tables, + ?array $where_clause_columns, + ?array $unique_columns, + ?array $index_columns + ) { + // now we want to find the best. + if (isset($unique_columns) && count($unique_columns) > 0) { + $candidate_columns = $unique_columns; + $needsort = 1; + return [ + $candidate_columns, + $needsort, + ]; + } elseif (isset($index_columns) && count($index_columns) > 0) { + $candidate_columns = $index_columns; + $needsort = 1; + return [ + $candidate_columns, + $needsort, + ]; + } elseif (isset($where_clause_columns) && count($where_clause_columns) > 0) { + $candidate_columns = $where_clause_columns; + $needsort = 0; + return [ + $candidate_columns, + $needsort, + ]; + } + + $candidate_columns = $search_tables; + $needsort = 0; + return [ + $candidate_columns, + $needsort, + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Database/Search.php b/srcs/phpmyadmin/libraries/classes/Database/Search.php new file mode 100644 index 0000000..3f1b250 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Database/Search.php @@ -0,0 +1,347 @@ +db = $db; + $this->dbi = $dbi; + $this->searchTypes = [ + '1' => __('at least one of the words'), + '2' => __('all of the words'), + '3' => __('the exact phrase as substring'), + '4' => __('the exact phrase as whole field'), + '5' => __('as regular expression'), + ]; + $this->template = $template; + // Sets criteria parameters + $this->setSearchParams(); + } + + /** + * Sets search parameters + * + * @return void + */ + private function setSearchParams() + { + $this->tablesNamesOnly = $this->dbi->getTables($this->db); + + if (empty($_POST['criteriaSearchType']) + || ! is_string($_POST['criteriaSearchType']) + || ! array_key_exists( + $_POST['criteriaSearchType'], + $this->searchTypes + ) + ) { + $this->criteriaSearchType = 1; + unset($_POST['submit_search']); + } else { + $this->criteriaSearchType = (int) $_POST['criteriaSearchType']; + $this->searchTypeDescription + = $this->searchTypes[$_POST['criteriaSearchType']]; + } + + if (empty($_POST['criteriaSearchString']) + || ! is_string($_POST['criteriaSearchString']) + ) { + $this->criteriaSearchString = ''; + unset($_POST['submit_search']); + } else { + $this->criteriaSearchString = $_POST['criteriaSearchString']; + } + + $this->criteriaTables = []; + if (empty($_POST['criteriaTables']) + || ! is_array($_POST['criteriaTables']) + ) { + unset($_POST['submit_search']); + } else { + $this->criteriaTables = array_intersect( + $_POST['criteriaTables'], + $this->tablesNamesOnly + ); + } + + if (empty($_POST['criteriaColumnName']) + || ! is_string($_POST['criteriaColumnName']) + ) { + unset($this->criteriaColumnName); + } else { + $this->criteriaColumnName = $this->dbi->escapeString( + $_POST['criteriaColumnName'] + ); + } + } + + /** + * Builds the SQL search query + * + * @param string $table The table name + * + * @return array 3 SQL queries (for count, display and delete results) + * + * @todo can we make use of fulltextsearch IN BOOLEAN MODE for this? + * PMA_backquote + * DatabaseInterface::freeResult + * DatabaseInterface::fetchAssoc + * $GLOBALS['db'] + * explode + * count + * strlen + */ + private function getSearchSqls($table) + { + // Statement types + $sqlstr_select = 'SELECT'; + $sqlstr_delete = 'DELETE'; + // Table to use + $sqlstr_from = ' FROM ' + . Util::backquote($GLOBALS['db']) . '.' + . Util::backquote($table); + // Gets where clause for the query + $where_clause = $this->getWhereClause($table); + // Builds complete queries + $sql = []; + $sql['select_columns'] = $sqlstr_select . ' * ' . $sqlstr_from + . $where_clause; + // here, I think we need to still use the COUNT clause, even for + // VIEWs, anyway we have a WHERE clause that should limit results + $sql['select_count'] = $sqlstr_select . ' COUNT(*) AS `count`' + . $sqlstr_from . $where_clause; + $sql['delete'] = $sqlstr_delete . $sqlstr_from . $where_clause; + + return $sql; + } + + /** + * Provides where clause for building SQL query + * + * @param string $table The table name + * + * @return string The generated where clause + */ + private function getWhereClause($table) + { + // Columns to select + $allColumns = $this->dbi->getColumns($GLOBALS['db'], $table); + $likeClauses = []; + // Based on search type, decide like/regex & '%'/'' + $like_or_regex = (($this->criteriaSearchType == 5) ? 'REGEXP' : 'LIKE'); + $automatic_wildcard = (($this->criteriaSearchType < 4) ? '%' : ''); + // For "as regular expression" (search option 5), LIKE won't be used + // Usage example: If user is searching for a literal $ in a regexp search, + // he should enter \$ as the value. + $criteriaSearchStringEscaped = $this->dbi->escapeString( + $this->criteriaSearchString + ); + // Extract search words or pattern + $search_words = (($this->criteriaSearchType > 2) + ? [$criteriaSearchStringEscaped] + : explode(' ', $criteriaSearchStringEscaped)); + + foreach ($search_words as $search_word) { + // Eliminates empty values + if (strlen($search_word) === 0) { + continue; + } + $likeClausesPerColumn = []; + // for each column in the table + foreach ($allColumns as $column) { + if (! isset($this->criteriaColumnName) + || strlen($this->criteriaColumnName) === 0 + || $column['Field'] == $this->criteriaColumnName + ) { + $column = 'CONVERT(' . Util::backquote($column['Field']) + . ' USING utf8)'; + $likeClausesPerColumn[] = $column . ' ' . $like_or_regex . ' ' + . "'" + . $automatic_wildcard . $search_word . $automatic_wildcard + . "'"; + } + } // end for + if (count($likeClausesPerColumn) > 0) { + $likeClauses[] = implode(' OR ', $likeClausesPerColumn); + } + } // end for + // Use 'OR' if 'at least one word' is to be searched, else use 'AND' + $implode_str = ($this->criteriaSearchType == 1 ? ' OR ' : ' AND '); + if (empty($likeClauses)) { + // this could happen when the "inside column" does not exist + // in any selected tables + $where_clause = ' WHERE FALSE'; + } else { + $where_clause = ' WHERE (' + . implode(') ' . $implode_str . ' (', $likeClauses) + . ')'; + } + return $where_clause; + } + + /** + * Displays database search results + * + * @return string HTML for search results + */ + public function getSearchResults() + { + $resultTotal = 0; + $rows = []; + // For each table selected as search criteria + foreach ($this->criteriaTables as $eachTable) { + // Gets the SQL statements + $newSearchSqls = $this->getSearchSqls($eachTable); + // Executes the "COUNT" statement + $resultCount = intval($this->dbi->fetchValue( + $newSearchSqls['select_count'] + )); + $resultTotal += $resultCount; + // Gets the result row's HTML for a table + $rows[] = [ + 'table' => htmlspecialchars($eachTable), + 'new_search_sqls' => $newSearchSqls, + 'result_count' => $resultCount, + ]; + } + + return $this->template->render('database/search/results', [ + 'db' => $this->db, + 'rows' => $rows, + 'result_total' => $resultTotal, + 'criteria_tables' => $this->criteriaTables, + 'criteria_search_string' => htmlspecialchars($this->criteriaSearchString), + 'search_type_description' => $this->searchTypeDescription, + ]); + } + + /** + * Provides the main search form's html + * + * @return string HTML for selection form + */ + public function getMainHtml() + { + $choices = [ + '1' => $this->searchTypes[1] . ' ' + . Util::showHint( + __('Words are separated by a space character (" ").') + ), + '2' => $this->searchTypes[2] . ' ' + . Util::showHint( + __('Words are separated by a space character (" ").') + ), + '3' => $this->searchTypes[3], + '4' => $this->searchTypes[4], + '5' => $this->searchTypes[5] . ' ' . Util::showMySQLDocu('Regexp'), + ]; + return $this->template->render('database/search/main', [ + 'db' => $this->db, + 'choices' => $choices, + 'criteria_search_string' => $this->criteriaSearchString, + 'criteria_search_type' => $this->criteriaSearchType, + 'criteria_tables' => $this->criteriaTables, + 'tables_names_only' => $this->tablesNamesOnly, + 'criteria_column_name' => isset($this->criteriaColumnName) + ? $this->criteriaColumnName : null, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/DatabaseInterface.php b/srcs/phpmyadmin/libraries/classes/DatabaseInterface.php new file mode 100644 index 0000000..3e12302 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/DatabaseInterface.php @@ -0,0 +1,3187 @@ +_extension = $ext; + $this->_links = []; + if (defined('TESTSUITE')) { + $this->_links[DatabaseInterface::CONNECT_USER] = 1; + $this->_links[DatabaseInterface::CONNECT_CONTROL] = 2; + } + $this->_table_cache = []; + $this->_current_user = []; + $this->types = new Types($this); + $this->relation = new Relation($this); + } + + /** + * Checks whether database extension is loaded + * + * @param string $extension mysql extension to check + * + * @return bool + */ + public static function checkDbExtension(string $extension = 'mysqli'): bool + { + return function_exists($extension . '_connect'); + } + + /** + * runs a query + * + * @param string $query SQL query to execute + * @param mixed $link optional database link to use + * @param int $options optional query options + * @param bool $cache_affected_rows whether to cache affected rows + * + * @return mixed + */ + public function query( + string $query, + $link = DatabaseInterface::CONNECT_USER, + int $options = 0, + bool $cache_affected_rows = true + ) { + $res = $this->tryQuery($query, $link, $options, $cache_affected_rows) + or Util::mysqlDie($this->getError($link), $query); + + return $res; + } + + /** + * Get a cached value from table cache. + * + * @param array $contentPath Array of the name of the target value + * @param mixed $default Return value on cache miss + * + * @return mixed cached value or default + */ + public function getCachedTableContent(array $contentPath, $default = null) + { + return Util::getValueByKey($this->_table_cache, $contentPath, $default); + } + + /** + * Set an item in table cache using dot notation. + * + * @param array $contentPath Array with the target path + * @param mixed $value Target value + * + * @return void + */ + public function cacheTableContent(array $contentPath, $value): void + { + $loc = &$this->_table_cache; + + if (! isset($contentPath)) { + $loc = $value; + return; + } + + while (count($contentPath) > 1) { + $key = array_shift($contentPath); + + // If the key doesn't exist at this depth, we will just create an empty + // array to hold the next value, allowing us to create the arrays to hold + // final values at the correct depth. Then we'll keep digging into the + // array. + if (! isset($loc[$key]) || ! is_array($loc[$key])) { + $loc[$key] = []; + } + $loc = &$loc[$key]; + } + + $loc[array_shift($contentPath)] = $value; + } + + /** + * Clear the table cache. + * + * @return void + */ + public function clearTableCache(): void + { + $this->_table_cache = []; + } + + /** + * Caches table data so Table does not require to issue + * SHOW TABLE STATUS again + * + * @param array $tables information for tables of some databases + * @param string|bool $table table name + * + * @return void + */ + private function _cacheTableData(array $tables, $table): void + { + // Note: I don't see why we would need array_merge_recursive() here, + // as it creates double entries for the same table (for example a double + // entry for Comment when changing the storage engine in Operations) + // Note 2: Instead of array_merge(), simply use the + operator because + // array_merge() renumbers numeric keys starting with 0, therefore + // we would lose a db name that consists only of numbers + + foreach ($tables as $one_database => $its_tables) { + if (isset($this->_table_cache[$one_database])) { + // the + operator does not do the intended effect + // when the cache for one table already exists + if ($table + && isset($this->_table_cache[$one_database][$table]) + ) { + unset($this->_table_cache[$one_database][$table]); + } + $this->_table_cache[$one_database] + += $tables[$one_database]; + } else { + $this->_table_cache[$one_database] = $tables[$one_database]; + } + } + } + + /** + * Stores query data into session data for debugging purposes + * + * @param string $query Query text + * @param mixed $link link type + * @param object|boolean $result Query result + * @param integer|float $time Time to execute query + * + * @return void + */ + private function _dbgQuery(string $query, $link, $result, $time): void + { + $dbgInfo = []; + $error_message = $this->getError($link); + if ($result == false && is_string($error_message)) { + $dbgInfo['error'] + = '' + . htmlspecialchars($error_message) . ''; + } + $dbgInfo['query'] = htmlspecialchars($query); + $dbgInfo['time'] = $time; + // Get and slightly format backtrace, this is used + // in the javascript console. + // Strip call to _dbgQuery + $dbgInfo['trace'] = Error::processBacktrace( + array_slice(debug_backtrace(), 1) + ); + $dbgInfo['hash'] = md5($query); + + $_SESSION['debug']['queries'][] = $dbgInfo; + } + + /** + * runs a query and returns the result + * + * @param string $query query to run + * @param mixed $link link type + * @param integer $options query options + * @param bool $cache_affected_rows whether to cache affected row + * + * @return mixed + */ + public function tryQuery( + string $query, + $link = DatabaseInterface::CONNECT_USER, + int $options = 0, + bool $cache_affected_rows = true + ) { + $debug = isset($GLOBALS['cfg']['DBG']) ? $GLOBALS['cfg']['DBG']['sql'] : false; + if (! isset($this->_links[$link])) { + return false; + } + + if ($debug) { + $time = microtime(true); + } + + $result = $this->_extension->realQuery($query, $this->_links[$link], $options); + + if ($cache_affected_rows) { + $GLOBALS['cached_affected_rows'] = $this->affectedRows($link, false); + } + + if ($debug) { + $time = microtime(true) - $time; + $this->_dbgQuery($query, $link, $result, $time); + if ($GLOBALS['cfg']['DBG']['sqllog']) { + $warningsCount = ''; + if (($options & DatabaseInterface::QUERY_STORE) == DatabaseInterface::QUERY_STORE) { + if (isset($this->_links[$link]->warning_count)) { + $warningsCount = $this->_links[$link]->warning_count; + } + } + + openlog('phpMyAdmin', LOG_NDELAY | LOG_PID, LOG_USER); + + syslog( + LOG_INFO, + 'SQL[' . basename($_SERVER['SCRIPT_NAME']) . ']: ' + . sprintf('%0.3f', $time) . '(W:' . $warningsCount . ') > ' . $query + ); + closelog(); + } + } + + if ($result !== false && Tracker::isActive()) { + Tracker::handleQuery($query); + } + + return $result; + } + + /** + * Run multi query statement and return results + * + * @param string $multiQuery multi query statement to execute + * @param int $linkIndex index of the opened database link + * + * @return mysqli_result[]|boolean (false) + */ + public function tryMultiQuery( + string $multiQuery = '', + $linkIndex = DatabaseInterface::CONNECT_USER + ) { + if (! isset($this->_links[$linkIndex])) { + return false; + } + return $this->_extension->realMultiQuery($this->_links[$linkIndex], $multiQuery); + } + + /** + * returns array with table names for given db + * + * @param string $database name of database + * @param mixed $link mysql link resource|object + * + * @return array tables names + */ + public function getTables(string $database, $link = DatabaseInterface::CONNECT_USER): array + { + $tables = $this->fetchResult( + 'SHOW TABLES FROM ' . Util::backquote($database) . ';', + null, + 0, + $link, + self::QUERY_STORE + ); + if ($GLOBALS['cfg']['NaturalOrder']) { + usort($tables, 'strnatcasecmp'); + } + return $tables; + } + + + /** + * returns + * + * @param string $database name of database + * @param array $tables list of tables to search for for relations + * @param int $link mysql link resource|object + * + * @return array array of found foreign keys + */ + public function getForeignKeyConstrains(string $database, array $tables, $link = DatabaseInterface::CONNECT_USER): array + { + $tablesListForQuery = ''; + foreach ($tables as $table) { + $tablesListForQuery .= "'" . $this->escapeString($table) . "',"; + } + $tablesListForQuery = rtrim($tablesListForQuery, ','); + + $foreignKeyConstrains = $this->fetchResult( + "SELECT" + . " TABLE_NAME," + . " COLUMN_NAME," + . " REFERENCED_TABLE_NAME," + . " REFERENCED_COLUMN_NAME" + . " FROM information_schema.key_column_usage" + . " WHERE referenced_table_name IS NOT NULL" + . " AND TABLE_SCHEMA = '" . $this->escapeString($database) . "'" + . " AND TABLE_NAME IN (" . $tablesListForQuery . ")" + . " AND REFERENCED_TABLE_NAME IN (" . $tablesListForQuery . ");", + null, + null, + $link, + self::QUERY_STORE + ); + return $foreignKeyConstrains; + } + + /** + * returns a segment of the SQL WHERE clause regarding table name and type + * + * @param array|string $table table(s) + * @param boolean $tbl_is_group $table is a table group + * @param string $table_type whether table or view + * + * @return string a segment of the WHERE clause + */ + private function _getTableCondition( + $table, + bool $tbl_is_group, + ?string $table_type + ): string { + // get table information from information_schema + if ($table) { + if (is_array($table)) { + $sql_where_table = 'AND t.`TABLE_NAME` ' + . Util::getCollateForIS() . ' IN (\'' + . implode( + '\', \'', + array_map( + [ + $this, + 'escapeString', + ], + $table + ) + ) + . '\')'; + } elseif (true === $tbl_is_group) { + $sql_where_table = 'AND t.`TABLE_NAME` LIKE \'' + . Util::escapeMysqlWildcards( + $this->escapeString($table) + ) + . '%\''; + } else { + $sql_where_table = 'AND t.`TABLE_NAME` ' + . Util::getCollateForIS() . ' = \'' + . $this->escapeString($table) . '\''; + } + } else { + $sql_where_table = ''; + } + + if ($table_type) { + if ($table_type == 'view') { + $sql_where_table .= " AND t.`TABLE_TYPE` NOT IN ('BASE TABLE', 'SYSTEM VERSIONED')"; + } elseif ($table_type == 'table') { + $sql_where_table .= " AND t.`TABLE_TYPE` IN ('BASE TABLE', 'SYSTEM VERSIONED')"; + } + } + return $sql_where_table; + } + + /** + * returns the beginning of the SQL statement to fetch the list of tables + * + * @param string[] $this_databases databases to list + * @param string $sql_where_table additional condition + * + * @return string the SQL statement + */ + private function _getSqlForTablesFull($this_databases, string $sql_where_table): string + { + return ' + SELECT *, + `TABLE_SCHEMA` AS `Db`, + `TABLE_NAME` AS `Name`, + `TABLE_TYPE` AS `TABLE_TYPE`, + `ENGINE` AS `Engine`, + `ENGINE` AS `Type`, + `VERSION` AS `Version`, + `ROW_FORMAT` AS `Row_format`, + `TABLE_ROWS` AS `Rows`, + `AVG_ROW_LENGTH` AS `Avg_row_length`, + `DATA_LENGTH` AS `Data_length`, + `MAX_DATA_LENGTH` AS `Max_data_length`, + `INDEX_LENGTH` AS `Index_length`, + `DATA_FREE` AS `Data_free`, + `AUTO_INCREMENT` AS `Auto_increment`, + `CREATE_TIME` AS `Create_time`, + `UPDATE_TIME` AS `Update_time`, + `CHECK_TIME` AS `Check_time`, + `TABLE_COLLATION` AS `Collation`, + `CHECKSUM` AS `Checksum`, + `CREATE_OPTIONS` AS `Create_options`, + `TABLE_COMMENT` AS `Comment` + FROM `information_schema`.`TABLES` t + WHERE `TABLE_SCHEMA` ' . Util::getCollateForIS() . ' + IN (\'' . implode("', '", $this_databases) . '\') + ' . $sql_where_table; + } + + /** + * returns array of all tables in given db or dbs + * this function expects unquoted names: + * RIGHT: my_database + * WRONG: `my_database` + * WRONG: my\_database + * if $tbl_is_group is true, $table is used as filter for table names + * + * + * $dbi->getTablesFull('my_database'); + * $dbi->getTablesFull('my_database', 'my_table')); + * $dbi->getTablesFull('my_database', 'my_tables_', true)); + * + * + * @param string $database database + * @param string|array $table table name(s) + * @param boolean $tbl_is_group $table is a table group + * @param integer $limit_offset zero-based offset for the count + * @param boolean|integer $limit_count number of tables to return + * @param string $sort_by table attribute to sort by + * @param string $sort_order direction to sort (ASC or DESC) + * @param string $table_type whether table or view + * @param mixed $link link type + * + * @todo move into Table + * + * @return array list of tables in given db(s) + */ + public function getTablesFull( + string $database, + $table = '', + bool $tbl_is_group = false, + int $limit_offset = 0, + $limit_count = false, + string $sort_by = 'Name', + string $sort_order = 'ASC', + ?string $table_type = null, + $link = DatabaseInterface::CONNECT_USER + ): array { + if (true === $limit_count) { + $limit_count = $GLOBALS['cfg']['MaxTableList']; + } + // prepare and check parameters + if (! is_array($database)) { + $databases = [$database]; + } else { + $databases = $database; + } + + $tables = []; + + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $sql_where_table = $this->_getTableCondition( + $table, + $tbl_is_group, + $table_type + ); + + // for PMA bc: + // `SCHEMA_FIELD_NAME` AS `SHOW_TABLE_STATUS_FIELD_NAME` + // + // on non-Windows servers, + // added BINARY in the WHERE clause to force a case sensitive + // comparison (if we are looking for the db Aa we don't want + // to find the db aa) + $this_databases = array_map( + [ + $this, + 'escapeString', + ], + $databases + ); + + $sql = $this->_getSqlForTablesFull($this_databases, $sql_where_table); + + // Sort the tables + $sql .= " ORDER BY $sort_by $sort_order"; + + if ($limit_count) { + $sql .= ' LIMIT ' . $limit_count . ' OFFSET ' . $limit_offset; + } + + $tables = $this->fetchResult( + $sql, + [ + 'TABLE_SCHEMA', + 'TABLE_NAME', + ], + null, + $link + ); + + if ($sort_by == 'Name' && $GLOBALS['cfg']['NaturalOrder']) { + // here, the array's first key is by schema name + foreach ($tables as $one_database_name => $one_database_tables) { + uksort($one_database_tables, 'strnatcasecmp'); + + if ($sort_order == 'DESC') { + $one_database_tables = array_reverse($one_database_tables); + } + $tables[$one_database_name] = $one_database_tables; + } + } elseif ($sort_by == 'Data_length') { + // Size = Data_length + Index_length + foreach ($tables as $one_database_name => $one_database_tables) { + uasort( + $one_database_tables, + function ($a, $b) { + $aLength = $a['Data_length'] + $a['Index_length']; + $bLength = $b['Data_length'] + $b['Index_length']; + return $aLength <=> $bLength; + } + ); + + if ($sort_order == 'DESC') { + $one_database_tables = array_reverse($one_database_tables); + } + $tables[$one_database_name] = $one_database_tables; + } + } + } // end (get information from table schema) + + // If permissions are wrong on even one database directory, + // information_schema does not return any table info for any database + // this is why we fall back to SHOW TABLE STATUS even for MySQL >= 50002 + if (empty($tables)) { + foreach ($databases as $each_database) { + if ($table || (true === $tbl_is_group) || ! empty($table_type)) { + $sql = 'SHOW TABLE STATUS FROM ' + . Util::backquote($each_database) + . ' WHERE'; + $needAnd = false; + if ($table || (true === $tbl_is_group)) { + if (is_array($table)) { + $sql .= ' `Name` IN (\'' + . implode( + '\', \'', + array_map( + [ + $this, + 'escapeString', + ], + $table, + $link + ) + ) . '\')'; + } else { + $sql .= " `Name` LIKE '" + . Util::escapeMysqlWildcards( + $this->escapeString($table, $link) + ) + . "%'"; + } + $needAnd = true; + } + if (! empty($table_type)) { + if ($needAnd) { + $sql .= " AND"; + } + if ($table_type == 'view') { + $sql .= " `Comment` = 'VIEW'"; + } elseif ($table_type == 'table') { + $sql .= " `Comment` != 'VIEW'"; + } + } + } else { + $sql = 'SHOW TABLE STATUS FROM ' + . Util::backquote($each_database); + } + + $each_tables = $this->fetchResult($sql, 'Name', null, $link); + + // Sort naturally if the config allows it and we're sorting + // the Name column. + if ($sort_by == 'Name' && $GLOBALS['cfg']['NaturalOrder']) { + uksort($each_tables, 'strnatcasecmp'); + + if ($sort_order == 'DESC') { + $each_tables = array_reverse($each_tables); + } + } else { + // Prepare to sort by creating array of the selected sort + // value to pass to array_multisort + + // Size = Data_length + Index_length + if ($sort_by == 'Data_length') { + foreach ($each_tables as $table_name => $table_data) { + ${$sort_by}[$table_name] = strtolower( + $table_data['Data_length'] + + $table_data['Index_length'] + ); + } + } else { + foreach ($each_tables as $table_name => $table_data) { + ${$sort_by}[$table_name] + = strtolower($table_data[$sort_by]); + } + } + + if (! empty($$sort_by)) { + if ($sort_order == 'DESC') { + array_multisort($$sort_by, SORT_DESC, $each_tables); + } else { + array_multisort($$sort_by, SORT_ASC, $each_tables); + } + } + + // cleanup the temporary sort array + unset($$sort_by); + } + + if ($limit_count) { + $each_tables = array_slice( + $each_tables, + $limit_offset, + $limit_count + ); + } + + foreach ($each_tables as $table_name => $each_table) { + if (! isset($each_tables[$table_name]['Type']) + && isset($each_tables[$table_name]['Engine']) + ) { + // pma BC, same parts of PMA still uses 'Type' + $each_tables[$table_name]['Type'] + =& $each_tables[$table_name]['Engine']; + } elseif (! isset($each_tables[$table_name]['Engine']) + && isset($each_tables[$table_name]['Type']) + ) { + // old MySQL reports Type, newer MySQL reports Engine + $each_tables[$table_name]['Engine'] + =& $each_tables[$table_name]['Type']; + } + + // Compatibility with INFORMATION_SCHEMA output + $each_tables[$table_name]['TABLE_SCHEMA'] + = $each_database; + $each_tables[$table_name]['TABLE_NAME'] + =& $each_tables[$table_name]['Name']; + $each_tables[$table_name]['ENGINE'] + =& $each_tables[$table_name]['Engine']; + $each_tables[$table_name]['VERSION'] + =& $each_tables[$table_name]['Version']; + $each_tables[$table_name]['ROW_FORMAT'] + =& $each_tables[$table_name]['Row_format']; + $each_tables[$table_name]['TABLE_ROWS'] + =& $each_tables[$table_name]['Rows']; + $each_tables[$table_name]['AVG_ROW_LENGTH'] + =& $each_tables[$table_name]['Avg_row_length']; + $each_tables[$table_name]['DATA_LENGTH'] + =& $each_tables[$table_name]['Data_length']; + $each_tables[$table_name]['MAX_DATA_LENGTH'] + =& $each_tables[$table_name]['Max_data_length']; + $each_tables[$table_name]['INDEX_LENGTH'] + =& $each_tables[$table_name]['Index_length']; + $each_tables[$table_name]['DATA_FREE'] + =& $each_tables[$table_name]['Data_free']; + $each_tables[$table_name]['AUTO_INCREMENT'] + =& $each_tables[$table_name]['Auto_increment']; + $each_tables[$table_name]['CREATE_TIME'] + =& $each_tables[$table_name]['Create_time']; + $each_tables[$table_name]['UPDATE_TIME'] + =& $each_tables[$table_name]['Update_time']; + $each_tables[$table_name]['CHECK_TIME'] + =& $each_tables[$table_name]['Check_time']; + $each_tables[$table_name]['TABLE_COLLATION'] + =& $each_tables[$table_name]['Collation']; + $each_tables[$table_name]['CHECKSUM'] + =& $each_tables[$table_name]['Checksum']; + $each_tables[$table_name]['CREATE_OPTIONS'] + =& $each_tables[$table_name]['Create_options']; + $each_tables[$table_name]['TABLE_COMMENT'] + =& $each_tables[$table_name]['Comment']; + + if (strtoupper($each_tables[$table_name]['Comment']) === 'VIEW' + && $each_tables[$table_name]['Engine'] == null + ) { + $each_tables[$table_name]['TABLE_TYPE'] = 'VIEW'; + } elseif ($each_database == 'information_schema') { + $each_tables[$table_name]['TABLE_TYPE'] = 'SYSTEM VIEW'; + } else { + /** + * @todo difference between 'TEMPORARY' and 'BASE TABLE' + * but how to detect? + */ + $each_tables[$table_name]['TABLE_TYPE'] = 'BASE TABLE'; + } + } + + $tables[$each_database] = $each_tables; + } + } + + // cache table data + // so Table does not require to issue SHOW TABLE STATUS again + $this->_cacheTableData($tables, $table); + + if (is_array($database)) { + return $tables; + } + + if (isset($tables[$database])) { + return $tables[$database]; + } + + if (isset($tables[mb_strtolower($database)])) { + // on windows with lower_case_table_names = 1 + // MySQL returns + // with SHOW DATABASES or information_schema.SCHEMATA: `Test` + // but information_schema.TABLES gives `test` + // see https://github.com/phpmyadmin/phpmyadmin/issues/8402 + return $tables[mb_strtolower($database)]; + } + + return $tables; + } + + /** + * Get VIEWs in a particular database + * + * @param string $db Database name to look in + * + * @return array Set of VIEWs inside the database + */ + public function getVirtualTables(string $db): array + { + $tables_full = $this->getTablesFull($db); + $views = []; + + foreach ($tables_full as $table => $tmp) { + $_table = $this->getTable($db, (string) $table); + if ($_table->isView()) { + $views[] = $table; + } + } + + return $views; + } + + + /** + * returns array with databases containing extended infos about them + * + * @param string $database database + * @param boolean $force_stats retrieve stats also for MySQL < 5 + * @param integer $link link type + * @param string $sort_by column to order by + * @param string $sort_order ASC or DESC + * @param integer $limit_offset starting offset for LIMIT + * @param bool|int $limit_count row count for LIMIT or true + * for $GLOBALS['cfg']['MaxDbList'] + * + * @todo move into ListDatabase? + * + * @return array + */ + public function getDatabasesFull( + ?string $database = null, + bool $force_stats = false, + $link = DatabaseInterface::CONNECT_USER, + string $sort_by = 'SCHEMA_NAME', + string $sort_order = 'ASC', + int $limit_offset = 0, + $limit_count = false + ): array { + $sort_order = strtoupper($sort_order); + + if (true === $limit_count) { + $limit_count = $GLOBALS['cfg']['MaxDbList']; + } + + $apply_limit_and_order_manual = true; + + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + /** + * if $GLOBALS['cfg']['NaturalOrder'] is enabled, we cannot use LIMIT + * cause MySQL does not support natural ordering, + * we have to do it afterward + */ + $limit = ''; + if (! $GLOBALS['cfg']['NaturalOrder']) { + if ($limit_count) { + $limit = ' LIMIT ' . $limit_count . ' OFFSET ' . $limit_offset; + } + + $apply_limit_and_order_manual = false; + } + + // get table information from information_schema + if (! empty($database)) { + $sql_where_schema = 'WHERE `SCHEMA_NAME` LIKE \'' + . $this->escapeString($database, $link) . '\''; + } else { + $sql_where_schema = ''; + } + + $sql = 'SELECT *, + CAST(BIN_NAME AS CHAR CHARACTER SET utf8) AS SCHEMA_NAME + FROM ('; + $sql .= 'SELECT + BINARY s.SCHEMA_NAME AS BIN_NAME, + s.DEFAULT_COLLATION_NAME'; + if ($force_stats) { + $sql .= ', + COUNT(t.TABLE_SCHEMA) AS SCHEMA_TABLES, + SUM(t.TABLE_ROWS) AS SCHEMA_TABLE_ROWS, + SUM(t.DATA_LENGTH) AS SCHEMA_DATA_LENGTH, + SUM(t.MAX_DATA_LENGTH) AS SCHEMA_MAX_DATA_LENGTH, + SUM(t.INDEX_LENGTH) AS SCHEMA_INDEX_LENGTH, + SUM(t.DATA_LENGTH + t.INDEX_LENGTH) + AS SCHEMA_LENGTH, + SUM(IF(t.ENGINE <> \'InnoDB\', t.DATA_FREE, 0)) + AS SCHEMA_DATA_FREE'; + } + $sql .= ' + FROM `information_schema`.SCHEMATA s '; + if ($force_stats) { + $sql .= ' + LEFT JOIN `information_schema`.TABLES t + ON BINARY t.TABLE_SCHEMA = BINARY s.SCHEMA_NAME'; + } + $sql .= $sql_where_schema . ' + GROUP BY BINARY s.SCHEMA_NAME, s.DEFAULT_COLLATION_NAME + ORDER BY '; + if ($sort_by == 'SCHEMA_NAME' + || $sort_by == 'DEFAULT_COLLATION_NAME' + ) { + $sql .= 'BINARY '; + } + $sql .= Util::backquote($sort_by) + . ' ' . $sort_order + . $limit; + $sql .= ') a'; + + $databases = $this->fetchResult($sql, 'SCHEMA_NAME', null, $link); + + $mysql_error = $this->getError($link); + if (! count($databases) && $GLOBALS['errno']) { + Util::mysqlDie($mysql_error, $sql); + } + + // display only databases also in official database list + // f.e. to apply hide_db and only_db + $drops = array_diff( + array_keys($databases), + (array) $GLOBALS['dblist']->databases + ); + foreach ($drops as $drop) { + unset($databases[$drop]); + } + } else { + $databases = []; + foreach ($GLOBALS['dblist']->databases as $database_name) { + // Compatibility with INFORMATION_SCHEMA output + $databases[$database_name]['SCHEMA_NAME'] = $database_name; + + $databases[$database_name]['DEFAULT_COLLATION_NAME'] + = $this->getDbCollation($database_name); + + if (! $force_stats) { + continue; + } + + // get additional info about tables + $databases[$database_name]['SCHEMA_TABLES'] = 0; + $databases[$database_name]['SCHEMA_TABLE_ROWS'] = 0; + $databases[$database_name]['SCHEMA_DATA_LENGTH'] = 0; + $databases[$database_name]['SCHEMA_MAX_DATA_LENGTH'] = 0; + $databases[$database_name]['SCHEMA_INDEX_LENGTH'] = 0; + $databases[$database_name]['SCHEMA_LENGTH'] = 0; + $databases[$database_name]['SCHEMA_DATA_FREE'] = 0; + + $res = $this->query( + 'SHOW TABLE STATUS FROM ' + . Util::backquote($database_name) . ';' + ); + + if ($res === false) { + unset($res); + continue; + } + + while ($row = $this->fetchAssoc($res)) { + $databases[$database_name]['SCHEMA_TABLES']++; + $databases[$database_name]['SCHEMA_TABLE_ROWS'] + += $row['Rows']; + $databases[$database_name]['SCHEMA_DATA_LENGTH'] + += $row['Data_length']; + $databases[$database_name]['SCHEMA_MAX_DATA_LENGTH'] + += $row['Max_data_length']; + $databases[$database_name]['SCHEMA_INDEX_LENGTH'] + += $row['Index_length']; + + // for InnoDB, this does not contain the number of + // overhead bytes but the total free space + if ('InnoDB' != $row['Engine']) { + $databases[$database_name]['SCHEMA_DATA_FREE'] + += $row['Data_free']; + } + $databases[$database_name]['SCHEMA_LENGTH'] + += $row['Data_length'] + $row['Index_length']; + } + $this->freeResult($res); + unset($res); + } + } + + /** + * apply limit and order manually now + * (caused by older MySQL < 5 or $GLOBALS['cfg']['NaturalOrder']) + */ + if ($apply_limit_and_order_manual) { + $GLOBALS['callback_sort_order'] = $sort_order; + $GLOBALS['callback_sort_by'] = $sort_by; + usort( + $databases, + [ + self::class, + '_usortComparisonCallback', + ] + ); + unset($GLOBALS['callback_sort_order'], $GLOBALS['callback_sort_by']); + + /** + * now apply limit + */ + if ($limit_count) { + $databases = array_slice($databases, $limit_offset, $limit_count); + } + } + + return $databases; + } + + /** + * usort comparison callback + * + * @param array $a first argument to sort + * @param array $b second argument to sort + * + * @return int a value representing whether $a should be before $b in the + * sorted array or not + * + * @access private + */ + private static function _usortComparisonCallback($a, $b): int + { + if ($GLOBALS['cfg']['NaturalOrder']) { + $sorter = 'strnatcasecmp'; + } else { + $sorter = 'strcasecmp'; + } + /* No sorting when key is not present */ + if (! isset($a[$GLOBALS['callback_sort_by']]) + || ! isset($b[$GLOBALS['callback_sort_by']]) + ) { + return 0; + } + // produces f.e.: + // return -1 * strnatcasecmp($a["SCHEMA_TABLES"], $b["SCHEMA_TABLES"]) + return ($GLOBALS['callback_sort_order'] == 'ASC' ? 1 : -1) * $sorter( + $a[$GLOBALS['callback_sort_by']], + $b[$GLOBALS['callback_sort_by']] + ); + } + + /** + * returns detailed array with all columns for sql + * + * @param string $sql_query target SQL query to get columns + * @param array $view_columns alias for columns + * + * @return array + */ + public function getColumnMapFromSql(string $sql_query, array $view_columns = []): array + { + $result = $this->tryQuery($sql_query); + + if ($result === false) { + return []; + } + + $meta = $this->getFieldsMeta( + $result + ); + + $nbFields = count($meta); + if ($nbFields <= 0) { + return []; + } + + $column_map = []; + $nbColumns = count($view_columns); + + for ($i = 0; $i < $nbFields; $i++) { + $map = []; + $map['table_name'] = $meta[$i]->table; + $map['refering_column'] = $meta[$i]->name; + + if ($nbColumns > 1) { + $map['real_column'] = $view_columns[$i]; + } + + $column_map[] = $map; + } + + return $column_map; + } + + /** + * returns detailed array with all columns for given table in database, + * or all tables/databases + * + * @param string $database name of database + * @param string $table name of table to retrieve columns from + * @param string $column name of specific column + * @param mixed $link mysql link resource + * + * @return array + */ + public function getColumnsFull( + ?string $database = null, + ?string $table = null, + ?string $column = null, + $link = DatabaseInterface::CONNECT_USER + ): array { + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $sql_wheres = []; + $array_keys = []; + + // get columns information from information_schema + if (null !== $database) { + $sql_wheres[] = '`TABLE_SCHEMA` = \'' + . $this->escapeString($database, $link) . '\' '; + } else { + $array_keys[] = 'TABLE_SCHEMA'; + } + if (null !== $table) { + $sql_wheres[] = '`TABLE_NAME` = \'' + . $this->escapeString($table, $link) . '\' '; + } else { + $array_keys[] = 'TABLE_NAME'; + } + if (null !== $column) { + $sql_wheres[] = '`COLUMN_NAME` = \'' + . $this->escapeString($column, $link) . '\' '; + } else { + $array_keys[] = 'COLUMN_NAME'; + } + + // for PMA bc: + // `[SCHEMA_FIELD_NAME]` AS `[SHOW_FULL_COLUMNS_FIELD_NAME]` + $sql = ' + SELECT *, + `COLUMN_NAME` AS `Field`, + `COLUMN_TYPE` AS `Type`, + `COLLATION_NAME` AS `Collation`, + `IS_NULLABLE` AS `Null`, + `COLUMN_KEY` AS `Key`, + `COLUMN_DEFAULT` AS `Default`, + `EXTRA` AS `Extra`, + `PRIVILEGES` AS `Privileges`, + `COLUMN_COMMENT` AS `Comment` + FROM `information_schema`.`COLUMNS`'; + + if (count($sql_wheres)) { + $sql .= "\n" . ' WHERE ' . implode(' AND ', $sql_wheres); + } + return $this->fetchResult($sql, $array_keys, null, $link); + } + + $columns = []; + if (null === $database) { + foreach ($GLOBALS['dblist']->databases as $database) { + $columns[$database] = $this->getColumnsFull( + $database, + null, + null, + $link + ); + } + return $columns; + } elseif (null === $table) { + $tables = $this->getTables($database); + foreach ($tables as $table) { + $columns[$table] = $this->getColumnsFull( + $database, + $table, + null, + $link + ); + } + return $columns; + } + $sql = 'SHOW FULL COLUMNS FROM ' + . Util::backquote($database) . '.' . Util::backquote($table); + if (null !== $column) { + $sql .= " LIKE '" . $this->escapeString($column, $link) . "'"; + } + + $columns = $this->fetchResult($sql, 'Field', null, $link); + $ordinal_position = 1; + foreach ($columns as $column_name => $each_column) { + // Compatibility with INFORMATION_SCHEMA output + $columns[$column_name]['COLUMN_NAME'] + =& $columns[$column_name]['Field']; + $columns[$column_name]['COLUMN_TYPE'] + =& $columns[$column_name]['Type']; + $columns[$column_name]['COLLATION_NAME'] + =& $columns[$column_name]['Collation']; + $columns[$column_name]['IS_NULLABLE'] + =& $columns[$column_name]['Null']; + $columns[$column_name]['COLUMN_KEY'] + =& $columns[$column_name]['Key']; + $columns[$column_name]['COLUMN_DEFAULT'] + =& $columns[$column_name]['Default']; + $columns[$column_name]['EXTRA'] + =& $columns[$column_name]['Extra']; + $columns[$column_name]['PRIVILEGES'] + =& $columns[$column_name]['Privileges']; + $columns[$column_name]['COLUMN_COMMENT'] + =& $columns[$column_name]['Comment']; + + $columns[$column_name]['TABLE_CATALOG'] = null; + $columns[$column_name]['TABLE_SCHEMA'] = $database; + $columns[$column_name]['TABLE_NAME'] = $table; + $columns[$column_name]['ORDINAL_POSITION'] = $ordinal_position; + $colType = $columns[$column_name]['COLUMN_TYPE']; + $colType = is_string($colType) ? $colType : ''; + $colTypePosComa = strpos($colType, '('); + $colTypePosComa = $colTypePosComa !== false ? $colTypePosComa : strlen($colType); + $columns[$column_name]['DATA_TYPE'] + = substr( + $colType, + 0, + $colTypePosComa + ); + /** + * @todo guess CHARACTER_MAXIMUM_LENGTH from COLUMN_TYPE + */ + $columns[$column_name]['CHARACTER_MAXIMUM_LENGTH'] = null; + /** + * @todo guess CHARACTER_OCTET_LENGTH from CHARACTER_MAXIMUM_LENGTH + */ + $columns[$column_name]['CHARACTER_OCTET_LENGTH'] = null; + $columns[$column_name]['NUMERIC_PRECISION'] = null; + $columns[$column_name]['NUMERIC_SCALE'] = null; + $colCollation = $columns[$column_name]['COLLATION_NAME']; + $colCollation = is_string($colCollation) ? $colCollation : ''; + $colCollationPosUnderscore = strpos($colCollation, '_'); + $colCollationPosUnderscore = $colCollationPosUnderscore !== false ? $colCollationPosUnderscore : strlen($colCollation); + $columns[$column_name]['CHARACTER_SET_NAME'] + = substr( + $colCollation, + 0, + $colCollationPosUnderscore + ); + + $ordinal_position++; + } + + if (null !== $column) { + return reset($columns); + } + + return $columns; + } + + /** + * Returns SQL query for fetching columns for a table + * + * The 'Key' column is not calculated properly, use $dbi->getColumns() + * to get correct values. + * + * @param string $database name of database + * @param string $table name of table to retrieve columns from + * @param string $column name of column, null to show all columns + * @param boolean $full whether to return full info or only column names + * + * @see getColumns() + * + * @return string + */ + public function getColumnsSql( + string $database, + string $table, + ?string $column = null, + bool $full = false + ): string { + $sql = 'SHOW ' . ($full ? 'FULL' : '') . ' COLUMNS FROM ' + . Util::backquote($database) . '.' . Util::backquote($table) + . ($column !== null ? "LIKE '" + . $this->escapeString($column) . "'" : ''); + + return $sql; + } + + /** + * Returns descriptions of columns in given table (all or given by $column) + * + * @param string $database name of database + * @param string $table name of table to retrieve columns from + * @param string $column name of column, null to show all columns + * @param boolean $full whether to return full info or only column names + * @param integer $link link type + * + * @return array array indexed by column names or, + * if $column is given, flat array description + */ + public function getColumns( + string $database, + string $table, + ?string $column = null, + bool $full = false, + $link = DatabaseInterface::CONNECT_USER + ): array { + $sql = $this->getColumnsSql($database, $table, $column, $full); + $fields = $this->fetchResult($sql, 'Field', null, $link); + if (! is_array($fields) || count($fields) === 0) { + return []; + } + // Check if column is a part of multiple-column index and set its 'Key'. + $indexes = Index::getFromTable($table, $database); + foreach ($fields as $field => $field_data) { + if (! empty($field_data['Key'])) { + continue; + } + + foreach ($indexes as $index) { + /** @var Index $index */ + if (! $index->hasColumn($field)) { + continue; + } + + $index_columns = $index->getColumns(); + if ($index_columns[$field]->getSeqInIndex() > 1) { + if ($index->isUnique()) { + $fields[$field]['Key'] = 'UNI'; + } else { + $fields[$field]['Key'] = 'MUL'; + } + } + } + } + + return $column != null ? array_shift($fields) : $fields; + } + + /** + * Returns all column names in given table + * + * @param string $database name of database + * @param string $table name of table to retrieve columns from + * @param mixed $link mysql link resource + * + * @return null|array + */ + public function getColumnNames( + string $database, + string $table, + $link = DatabaseInterface::CONNECT_USER + ): ?array { + $sql = $this->getColumnsSql($database, $table); + // We only need the 'Field' column which contains the table's column names + $fields = array_keys($this->fetchResult($sql, 'Field', null, $link)); + + if (! is_array($fields) || count($fields) === 0) { + return null; + } + return $fields; + } + + /** + * Returns SQL for fetching information on table indexes (SHOW INDEXES) + * + * @param string $database name of database + * @param string $table name of the table whose indexes are to be retrieved + * @param string $where additional conditions for WHERE + * + * @return string SQL for getting indexes + */ + public function getTableIndexesSql( + string $database, + string $table, + ?string $where = null + ): string { + $sql = 'SHOW INDEXES FROM ' . Util::backquote($database) . '.' + . Util::backquote($table); + if ($where) { + $sql .= ' WHERE (' . $where . ')'; + } + return $sql; + } + + /** + * Returns indexes of a table + * + * @param string $database name of database + * @param string $table name of the table whose indexes are to be retrieved + * @param mixed $link mysql link resource + * + * @return array + */ + public function getTableIndexes( + string $database, + string $table, + $link = DatabaseInterface::CONNECT_USER + ): array { + $sql = $this->getTableIndexesSql($database, $table); + $indexes = $this->fetchResult($sql, null, null, $link); + + if (! is_array($indexes) || count($indexes) < 1) { + return []; + } + return $indexes; + } + + /** + * returns value of given mysql server variable + * + * @param string $var mysql server variable name + * @param int $type DatabaseInterface::GETVAR_SESSION | + * DatabaseInterface::GETVAR_GLOBAL + * @param mixed $link mysql link resource|object + * + * @return mixed value for mysql server variable + */ + public function getVariable( + string $var, + int $type = self::GETVAR_SESSION, + $link = DatabaseInterface::CONNECT_USER + ) { + switch ($type) { + case self::GETVAR_SESSION: + $modifier = ' SESSION'; + break; + case self::GETVAR_GLOBAL: + $modifier = ' GLOBAL'; + break; + default: + $modifier = ''; + } + return $this->fetchValue( + 'SHOW' . $modifier . ' VARIABLES LIKE \'' . $var . '\';', + 0, + 1, + $link + ); + } + + /** + * Sets new value for a variable if it is different from the current value + * + * @param string $var variable name + * @param string $value value to set + * @param mixed $link mysql link resource|object + * + * @return bool whether query was a successful + */ + public function setVariable( + string $var, + string $value, + $link = DatabaseInterface::CONNECT_USER + ): bool { + $current_value = $this->getVariable( + $var, + self::GETVAR_SESSION, + $link + ); + if ($current_value == $value) { + return true; + } + + return $this->query("SET " . $var . " = " . $value . ';', $link); + } + + /** + * Convert version string to integer. + * + * @param string $version MySQL server version + * + * @return int + */ + public static function versionToInt(string $version): int + { + $match = explode('.', $version); + return (int) sprintf('%d%02d%02d', $match[0], $match[1], intval($match[2])); + } + + /** + * Function called just after a connection to the MySQL database server has + * been established. It sets the connection collation, and determines the + * version of MySQL which is running. + * + * @return void + */ + public function postConnect(): void + { + $version = $this->fetchSingleRow( + 'SELECT @@version, @@version_comment', + 'ASSOC', + DatabaseInterface::CONNECT_USER + ); + + if ($version) { + $this->_version_str = isset($version['@@version']) ? $version['@@version'] : ''; + $this->_version_int = self::versionToInt($this->_version_str); + $this->_version_comment = isset($version['@@version_comment']) ? $version['@@version_comment'] : ''; + if (stripos($this->_version_str, 'mariadb') !== false) { + $this->_is_mariadb = true; + } + if (stripos($this->_version_comment, 'percona') !== false) { + $this->_is_percona = true; + } + } + + if ($this->_version_int > 50503) { + $default_charset = 'utf8mb4'; + $default_collation = 'utf8mb4_general_ci'; + } else { + $default_charset = 'utf8'; + $default_collation = 'utf8_general_ci'; + } + $GLOBALS['collation_connection'] = $default_collation; + $GLOBALS['charset_connection'] = $default_charset; + $this->query( + "SET NAMES '$default_charset' COLLATE '$default_collation';", + DatabaseInterface::CONNECT_USER, + self::QUERY_STORE + ); + + /* Locale for messages */ + $locale = LanguageManager::getInstance()->getCurrentLanguage()->getMySQLLocale(); + if (! empty($locale)) { + $this->query( + "SET lc_messages = '" . $locale . "';", + DatabaseInterface::CONNECT_USER, + self::QUERY_STORE + ); + } + + // Set timezone for the session, if required. + if ($GLOBALS['cfg']['Server']['SessionTimeZone'] != '') { + $sql_query_tz = 'SET ' . Util::backquote('time_zone') . ' = ' + . '\'' + . $this->escapeString($GLOBALS['cfg']['Server']['SessionTimeZone']) + . '\''; + + if (! $this->tryQuery($sql_query_tz)) { + $error_message_tz = sprintf( + __( + 'Unable to use timezone "%1$s" for server %2$d. ' + . 'Please check your configuration setting for ' + . '[em]$cfg[\'Servers\'][%3$d][\'SessionTimeZone\'][/em]. ' + . 'phpMyAdmin is currently using the default time zone ' + . 'of the database server.' + ), + $GLOBALS['cfg']['Server']['SessionTimeZone'], + $GLOBALS['server'], + $GLOBALS['server'] + ); + + trigger_error($error_message_tz, E_USER_WARNING); + } + } + + /* Loads closest context to this version. */ + Context::loadClosest( + ($this->_is_mariadb ? 'MariaDb' : 'MySql') . $this->_version_int + ); + + /** + * the DatabaseList class as a stub for the ListDatabase class + */ + $GLOBALS['dblist'] = new DatabaseList(); + } + + /** + * Sets collation connection for user link + * + * @param string $collation collation to set + * + * @return void + */ + public function setCollation(string $collation): void + { + $charset = $GLOBALS['charset_connection']; + /* Automatically adjust collation if not supported by server */ + if ($charset == 'utf8' && strncmp('utf8mb4_', $collation, 8) == 0) { + $collation = 'utf8_' . substr($collation, 8); + } + $result = $this->tryQuery( + "SET collation_connection = '" + . $this->escapeString($collation, DatabaseInterface::CONNECT_USER) + . "';", + DatabaseInterface::CONNECT_USER, + self::QUERY_STORE + ); + if ($result === false) { + trigger_error( + __('Failed to set configured collation connection!'), + E_USER_WARNING + ); + } else { + $GLOBALS['collation_connection'] = $collation; + } + } + + /** + * This function checks and initialises the phpMyAdmin configuration + * storage state before it is used into session cache. + * + * @return void + */ + public function initRelationParamsCache() + { + if (strlen($GLOBALS['db'])) { + $cfgRelation = $this->relation->getRelationsParam(); + if (empty($cfgRelation['db'])) { + $this->relation->fixPmaTables($GLOBALS['db'], false); + } + } + $cfgRelation = $this->relation->getRelationsParam(); + if (empty($cfgRelation['db']) && isset($GLOBALS['dblist'])) { + if ($GLOBALS['dblist']->databases->exists('phpmyadmin')) { + $this->relation->fixPmaTables('phpmyadmin', false); + } + } + } + + /** + * Function called just after a connection to the MySQL database server has + * been established. It sets the connection collation, and determines the + * version of MySQL which is running. + * + * @return void + */ + public function postConnectControl(): void + { + // If Zero configuration mode enabled, check PMA tables in current db. + if ($GLOBALS['cfg']['ZeroConf'] == true) { + /** + * the DatabaseList class as a stub for the ListDatabase class + */ + $GLOBALS['dblist'] = new DatabaseList(); + + $this->initRelationParamsCache(); + } + } + + /** + * returns a single value from the given result or query, + * if the query or the result has more than one row or field + * the first field of the first row is returned + * + * + * $sql = 'SELECT `name` FROM `user` WHERE `id` = 123'; + * $user_name = $dbi->fetchValue($sql); + * // produces + * // $user_name = 'John Doe' + * + * + * @param string $query The query to execute + * @param integer $row_number row to fetch the value from, + * starting at 0, with 0 being default + * @param integer|string $field field to fetch the value from, + * starting at 0, with 0 being default + * @param integer $link link type + * + * @return mixed value of first field in first row from result + * or false if not found + */ + public function fetchValue( + string $query, + int $row_number = 0, + $field = 0, + $link = DatabaseInterface::CONNECT_USER + ) { + $value = false; + + $result = $this->tryQuery( + $query, + $link, + self::QUERY_STORE, + false + ); + if ($result === false) { + return false; + } + + // return false if result is empty or false + // or requested row is larger than rows in result + if ($this->numRows($result) < ($row_number + 1)) { + return $value; + } + + // if $field is an integer use non associative mysql fetch function + if (is_int($field)) { + $fetch_function = 'fetchRow'; + } else { + $fetch_function = 'fetchAssoc'; + } + + // get requested row + for ($i = 0; $i <= $row_number; $i++) { + $row = $this->$fetch_function($result); + } + $this->freeResult($result); + + // return requested field + if (isset($row[$field])) { + $value = $row[$field]; + } + + return $value; + } + + /** + * returns only the first row from the result + * + * + * $sql = 'SELECT * FROM `user` WHERE `id` = 123'; + * $user = $dbi->fetchSingleRow($sql); + * // produces + * // $user = array('id' => 123, 'name' => 'John Doe') + * + * + * @param string $query The query to execute + * @param string $type NUM|ASSOC|BOTH returned array should either numeric + * associative or both + * @param integer $link link type + * + * @return array|boolean first row from result + * or false if result is empty + */ + public function fetchSingleRow( + string $query, + string $type = 'ASSOC', + $link = DatabaseInterface::CONNECT_USER + ) { + $result = $this->tryQuery( + $query, + $link, + self::QUERY_STORE, + false + ); + if ($result === false) { + return false; + } + + // return false if result is empty or false + if (! $this->numRows($result)) { + return false; + } + + switch ($type) { + case 'NUM': + $fetch_function = 'fetchRow'; + break; + case 'ASSOC': + $fetch_function = 'fetchAssoc'; + break; + case 'BOTH': + default: + $fetch_function = 'fetchArray'; + break; + } + + $row = $this->$fetch_function($result); + $this->freeResult($result); + return $row; + } + + /** + * Returns row or element of a row + * + * @param array $row Row to process + * @param string|null|int $value Which column to return + * + * @return mixed + */ + private function _fetchValue(array $row, $value) + { + if ($value === null) { + return $row; + } + + return $row[$value]; + } + + /** + * returns all rows in the resultset in one array + * + * + * $sql = 'SELECT * FROM `user`'; + * $users = $dbi->fetchResult($sql); + * // produces + * // $users[] = array('id' => 123, 'name' => 'John Doe') + * + * $sql = 'SELECT `id`, `name` FROM `user`'; + * $users = $dbi->fetchResult($sql, 'id'); + * // produces + * // $users['123'] = array('id' => 123, 'name' => 'John Doe') + * + * $sql = 'SELECT `id`, `name` FROM `user`'; + * $users = $dbi->fetchResult($sql, 0); + * // produces + * // $users['123'] = array(0 => 123, 1 => 'John Doe') + * + * $sql = 'SELECT `id`, `name` FROM `user`'; + * $users = $dbi->fetchResult($sql, 'id', 'name'); + * // or + * $users = $dbi->fetchResult($sql, 0, 1); + * // produces + * // $users['123'] = 'John Doe' + * + * $sql = 'SELECT `name` FROM `user`'; + * $users = $dbi->fetchResult($sql); + * // produces + * // $users[] = 'John Doe' + * + * $sql = 'SELECT `group`, `name` FROM `user`' + * $users = $dbi->fetchResult($sql, array('group', null), 'name'); + * // produces + * // $users['admin'][] = 'John Doe' + * + * $sql = 'SELECT `group`, `name` FROM `user`' + * $users = $dbi->fetchResult($sql, array('group', 'name'), 'id'); + * // produces + * // $users['admin']['John Doe'] = '123' + * + * + * @param string $query query to execute + * @param string|integer|array $key field-name or offset + * used as key for array + * or array of those + * @param string|integer $value value-name or offset + * used as value for array + * @param integer $link link type + * @param integer $options query options + * + * @return array resultrows or values indexed by $key + */ + public function fetchResult( + string $query, + $key = null, + $value = null, + $link = DatabaseInterface::CONNECT_USER, + int $options = 0 + ) { + $resultrows = []; + + $result = $this->tryQuery($query, $link, $options, false); + + // return empty array if result is empty or false + if ($result === false) { + return $resultrows; + } + + $fetch_function = 'fetchAssoc'; + + // no nested array if only one field is in result + if (null === $key && 1 === $this->numFields($result)) { + $value = 0; + $fetch_function = 'fetchRow'; + } + + // if $key is an integer use non associative mysql fetch function + if (is_int($key)) { + $fetch_function = 'fetchRow'; + } + + if (null === $key) { + while ($row = $this->$fetch_function($result)) { + $resultrows[] = $this->_fetchValue($row, $value); + } + } else { + if (is_array($key)) { + while ($row = $this->$fetch_function($result)) { + $result_target =& $resultrows; + foreach ($key as $key_index) { + if (null === $key_index) { + $result_target =& $result_target[]; + continue; + } + + if (! isset($result_target[$row[$key_index]])) { + $result_target[$row[$key_index]] = []; + } + $result_target =& $result_target[$row[$key_index]]; + } + $result_target = $this->_fetchValue($row, $value); + } + } else { + while ($row = $this->$fetch_function($result)) { + $resultrows[$row[$key]] = $this->_fetchValue($row, $value); + } + } + } + + $this->freeResult($result); + return $resultrows; + } + + /** + * Get supported SQL compatibility modes + * + * @return array supported SQL compatibility modes + */ + public function getCompatibilities(): array + { + $compats = ['NONE']; + $compats[] = 'ANSI'; + $compats[] = 'DB2'; + $compats[] = 'MAXDB'; + $compats[] = 'MYSQL323'; + $compats[] = 'MYSQL40'; + $compats[] = 'MSSQL'; + $compats[] = 'ORACLE'; + // removed; in MySQL 5.0.33, this produces exports that + // can't be read by POSTGRESQL (see our bug #1596328) + //$compats[] = 'POSTGRESQL'; + $compats[] = 'TRADITIONAL'; + + return $compats; + } + + /** + * returns warnings for last query + * + * @param integer $link link type + * + * @return array warnings + */ + public function getWarnings($link = DatabaseInterface::CONNECT_USER): array + { + return $this->fetchResult('SHOW WARNINGS', null, null, $link); + } + + /** + * returns an array of PROCEDURE or FUNCTION names for a db + * + * @param string $db db name + * @param string $which PROCEDURE | FUNCTION + * @param integer $link link type + * + * @return array the procedure names or function names + */ + public function getProceduresOrFunctions( + string $db, + string $which, + $link = DatabaseInterface::CONNECT_USER + ): array { + $shows = $this->fetchResult( + 'SHOW ' . $which . ' STATUS;', + null, + null, + $link + ); + $result = []; + foreach ($shows as $one_show) { + if ($one_show['Db'] == $db && $one_show['Type'] == $which) { + $result[] = $one_show['Name']; + } + } + return $result; + } + + /** + * returns the definition of a specific PROCEDURE, FUNCTION, EVENT or VIEW + * + * @param string $db db name + * @param string $which PROCEDURE | FUNCTION | EVENT | VIEW + * @param string $name the procedure|function|event|view name + * @param integer $link link type + * + * @return string|null the definition + */ + public function getDefinition( + string $db, + string $which, + string $name, + $link = DatabaseInterface::CONNECT_USER + ): ?string { + $returned_field = [ + 'PROCEDURE' => 'Create Procedure', + 'FUNCTION' => 'Create Function', + 'EVENT' => 'Create Event', + 'VIEW' => 'Create View', + ]; + $query = 'SHOW CREATE ' . $which . ' ' + . Util::backquote($db) . '.' + . Util::backquote($name); + $result = $this->fetchValue($query, 0, $returned_field[$which], $link); + return is_string($result) ? $result : null; + } + + /** + * returns details about the PROCEDUREs or FUNCTIONs for a specific database + * or details about a specific routine + * + * @param string $db db name + * @param string $which PROCEDURE | FUNCTION or null for both + * @param string $name name of the routine (to fetch a specific routine) + * + * @return array information about ROCEDUREs or FUNCTIONs + */ + public function getRoutines( + string $db, + ?string $which = null, + string $name = '' + ): array { + $routines = []; + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $query = "SELECT" + . " `ROUTINE_SCHEMA` AS `Db`," + . " `SPECIFIC_NAME` AS `Name`," + . " `ROUTINE_TYPE` AS `Type`," + . " `DEFINER` AS `Definer`," + . " `LAST_ALTERED` AS `Modified`," + . " `CREATED` AS `Created`," + . " `SECURITY_TYPE` AS `Security_type`," + . " `ROUTINE_COMMENT` AS `Comment`," + . " `CHARACTER_SET_CLIENT` AS `character_set_client`," + . " `COLLATION_CONNECTION` AS `collation_connection`," + . " `DATABASE_COLLATION` AS `Database Collation`," + . " `DTD_IDENTIFIER`" + . " FROM `information_schema`.`ROUTINES`" + . " WHERE `ROUTINE_SCHEMA` " . Util::getCollateForIS() + . " = '" . $this->escapeString($db) . "'"; + if (Core::isValid($which, ['FUNCTION', 'PROCEDURE'])) { + $query .= " AND `ROUTINE_TYPE` = '" . $which . "'"; + } + if (! empty($name)) { + $query .= " AND `SPECIFIC_NAME`" + . " = '" . $this->escapeString($name) . "'"; + } + $result = $this->fetchResult($query); + if (! empty($result)) { + $routines = $result; + } + } else { + if ($which == 'FUNCTION' || $which == null) { + $query = "SHOW FUNCTION STATUS" + . " WHERE `Db` = '" . $this->escapeString($db) . "'"; + if (! empty($name)) { + $query .= " AND `Name` = '" + . $this->escapeString($name) . "'"; + } + $result = $this->fetchResult($query); + if (! empty($result)) { + $routines = array_merge($routines, $result); + } + } + if ($which == 'PROCEDURE' || $which == null) { + $query = "SHOW PROCEDURE STATUS" + . " WHERE `Db` = '" . $this->escapeString($db) . "'"; + if (! empty($name)) { + $query .= " AND `Name` = '" + . $this->escapeString($name) . "'"; + } + $result = $this->fetchResult($query); + if (! empty($result)) { + $routines = array_merge($routines, $result); + } + } + } + + $ret = []; + foreach ($routines as $routine) { + $one_result = []; + $one_result['db'] = $routine['Db']; + $one_result['name'] = $routine['Name']; + $one_result['type'] = $routine['Type']; + $one_result['definer'] = $routine['Definer']; + $one_result['returns'] = isset($routine['DTD_IDENTIFIER']) + ? $routine['DTD_IDENTIFIER'] : ""; + $ret[] = $one_result; + } + + // Sort results by name + $name = []; + foreach ($ret as $value) { + $name[] = $value['name']; + } + array_multisort($name, SORT_ASC, $ret); + + return $ret; + } + + /** + * returns details about the EVENTs for a specific database + * + * @param string $db db name + * @param string $name event name + * + * @return array information about EVENTs + */ + public function getEvents(string $db, string $name = ''): array + { + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $query = "SELECT" + . " `EVENT_SCHEMA` AS `Db`," + . " `EVENT_NAME` AS `Name`," + . " `DEFINER` AS `Definer`," + . " `TIME_ZONE` AS `Time zone`," + . " `EVENT_TYPE` AS `Type`," + . " `EXECUTE_AT` AS `Execute at`," + . " `INTERVAL_VALUE` AS `Interval value`," + . " `INTERVAL_FIELD` AS `Interval field`," + . " `STARTS` AS `Starts`," + . " `ENDS` AS `Ends`," + . " `STATUS` AS `Status`," + . " `ORIGINATOR` AS `Originator`," + . " `CHARACTER_SET_CLIENT` AS `character_set_client`," + . " `COLLATION_CONNECTION` AS `collation_connection`, " + . "`DATABASE_COLLATION` AS `Database Collation`" + . " FROM `information_schema`.`EVENTS`" + . " WHERE `EVENT_SCHEMA` " . Util::getCollateForIS() + . " = '" . $this->escapeString($db) . "'"; + if (! empty($name)) { + $query .= " AND `EVENT_NAME`" + . " = '" . $this->escapeString($name) . "'"; + } + } else { + $query = "SHOW EVENTS FROM " . Util::backquote($db); + if (! empty($name)) { + $query .= " AND `Name` = '" + . $this->escapeString($name) . "'"; + } + } + + $result = []; + if ($events = $this->fetchResult($query)) { + foreach ($events as $event) { + $one_result = []; + $one_result['name'] = $event['Name']; + $one_result['type'] = $event['Type']; + $one_result['status'] = $event['Status']; + $result[] = $one_result; + } + } + + // Sort results by name + $name = []; + foreach ($result as $value) { + $name[] = $value['name']; + } + array_multisort($name, SORT_ASC, $result); + + return $result; + } + + /** + * returns details about the TRIGGERs for a specific table or database + * + * @param string $db db name + * @param string $table table name + * @param string $delimiter the delimiter to use (may be empty) + * + * @return array information about triggers (may be empty) + */ + public function getTriggers(string $db, string $table = '', $delimiter = '//') + { + $result = []; + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $query = 'SELECT TRIGGER_SCHEMA, TRIGGER_NAME, EVENT_MANIPULATION' + . ', EVENT_OBJECT_TABLE, ACTION_TIMING, ACTION_STATEMENT' + . ', EVENT_OBJECT_SCHEMA, EVENT_OBJECT_TABLE, DEFINER' + . ' FROM information_schema.TRIGGERS' + . ' WHERE EVENT_OBJECT_SCHEMA ' . Util::getCollateForIS() . '=' + . ' \'' . $this->escapeString($db) . '\''; + + if (! empty($table)) { + $query .= " AND EVENT_OBJECT_TABLE " . Util::getCollateForIS() + . " = '" . $this->escapeString($table) . "';"; + } + } else { + $query = "SHOW TRIGGERS FROM " . Util::backquote($db); + if (! empty($table)) { + $query .= " LIKE '" . $this->escapeString($table) . "';"; + } + } + + if ($triggers = $this->fetchResult($query)) { + foreach ($triggers as $trigger) { + if ($GLOBALS['cfg']['Server']['DisableIS']) { + $trigger['TRIGGER_NAME'] = $trigger['Trigger']; + $trigger['ACTION_TIMING'] = $trigger['Timing']; + $trigger['EVENT_MANIPULATION'] = $trigger['Event']; + $trigger['EVENT_OBJECT_TABLE'] = $trigger['Table']; + $trigger['ACTION_STATEMENT'] = $trigger['Statement']; + $trigger['DEFINER'] = $trigger['Definer']; + } + $one_result = []; + $one_result['name'] = $trigger['TRIGGER_NAME']; + $one_result['table'] = $trigger['EVENT_OBJECT_TABLE']; + $one_result['action_timing'] = $trigger['ACTION_TIMING']; + $one_result['event_manipulation'] = $trigger['EVENT_MANIPULATION']; + $one_result['definition'] = $trigger['ACTION_STATEMENT']; + $one_result['definer'] = $trigger['DEFINER']; + + // do not prepend the schema name; this way, importing the + // definition into another schema will work + $one_result['full_trigger_name'] = Util::backquote( + $trigger['TRIGGER_NAME'] + ); + $one_result['drop'] = 'DROP TRIGGER IF EXISTS ' + . $one_result['full_trigger_name']; + $one_result['create'] = 'CREATE TRIGGER ' + . $one_result['full_trigger_name'] . ' ' + . $trigger['ACTION_TIMING'] . ' ' + . $trigger['EVENT_MANIPULATION'] + . ' ON ' . Util::backquote($trigger['EVENT_OBJECT_TABLE']) + . "\n" . ' FOR EACH ROW ' + . $trigger['ACTION_STATEMENT'] . "\n" . $delimiter . "\n"; + + $result[] = $one_result; + } + } + + // Sort results by name + $name = []; + foreach ($result as $value) { + $name[] = $value['name']; + } + array_multisort($name, SORT_ASC, $result); + + return $result; + } + + /** + * Formats database error message in a friendly way. + * This is needed because some errors messages cannot + * be obtained by mysql_error(). + * + * @param int $error_number Error code + * @param string $error_message Error message as returned by server + * + * @return string HML text with error details + */ + public static function formatError(int $error_number, string $error_message): string + { + $error_message = htmlspecialchars($error_message); + + $error = '#' . ((string) $error_number); + $separator = ' — '; + + if ($error_number == 2002) { + $error .= ' - ' . $error_message; + $error .= $separator; + $error .= __( + 'The server is not responding (or the local server\'s socket' + . ' is not correctly configured).' + ); + } elseif ($error_number == 2003) { + $error .= ' - ' . $error_message; + $error .= $separator . __('The server is not responding.'); + } elseif ($error_number == 1698) { + $error .= ' - ' . $error_message; + $error .= $separator . ''; + $error .= __('Logout and try as another user.') . ''; + } elseif ($error_number == 1005) { + if (strpos($error_message, 'errno: 13') !== false) { + $error .= ' - ' . $error_message; + $error .= $separator + . __( + 'Please check privileges of directory containing database.' + ); + } else { + /* InnoDB constraints, see + * https://dev.mysql.com/doc/refman/5.0/en/ + * innodb-foreign-key-constraints.html + */ + $error .= ' - ' . $error_message . + ' (' . __('Details…') . ')'; + } + } else { + $error .= ' - ' . $error_message; + } + + return $error; + } + + /** + * gets the current user with host + * + * @return string the current user i.e. user@host + */ + public function getCurrentUser(): string + { + if (Util::cacheExists('mysql_cur_user')) { + return Util::cacheGet('mysql_cur_user'); + } + $user = $this->fetchValue('SELECT CURRENT_USER();'); + if ($user !== false) { + Util::cacheSet('mysql_cur_user', $user); + return $user; + } + return '@'; + } + + /** + * Checks if current user is superuser + * + * @return bool Whether user is a superuser + */ + public function isSuperuser(): bool + { + return self::isUserType('super'); + } + + /** + * Checks if current user has global create user/grant privilege + * or is a superuser (i.e. SELECT on mysql.users) + * while caching the result in session. + * + * @param string $type type of user to check for + * i.e. 'create', 'grant', 'super' + * + * @return bool Whether user is a given type of user + */ + public function isUserType(string $type): bool + { + if (Util::cacheExists('is_' . $type . 'user')) { + return Util::cacheGet('is_' . $type . 'user'); + } + + // when connection failed we don't have a $userlink + if (! isset($this->_links[DatabaseInterface::CONNECT_USER])) { + return false; + } + + // checking if user is logged in + if ($type === 'logged') { + return true; + } + + if (! $GLOBALS['cfg']['Server']['DisableIS'] || $type === 'super') { + // Prepare query for each user type check + $query = ''; + if ($type === 'super') { + $query = 'SELECT 1 FROM mysql.user LIMIT 1'; + } elseif ($type === 'create') { + list($user, $host) = $this->getCurrentUserAndHost(); + $query = "SELECT 1 FROM `INFORMATION_SCHEMA`.`USER_PRIVILEGES` " + . "WHERE `PRIVILEGE_TYPE` = 'CREATE USER' AND " + . "'''" . $user . "''@''" . $host . "''' LIKE `GRANTEE` LIMIT 1"; + } elseif ($type === 'grant') { + list($user, $host) = $this->getCurrentUserAndHost(); + $query = "SELECT 1 FROM (" + . "SELECT `GRANTEE`, `IS_GRANTABLE` FROM " + . "`INFORMATION_SCHEMA`.`COLUMN_PRIVILEGES` UNION " + . "SELECT `GRANTEE`, `IS_GRANTABLE` FROM " + . "`INFORMATION_SCHEMA`.`TABLE_PRIVILEGES` UNION " + . "SELECT `GRANTEE`, `IS_GRANTABLE` FROM " + . "`INFORMATION_SCHEMA`.`SCHEMA_PRIVILEGES` UNION " + . "SELECT `GRANTEE`, `IS_GRANTABLE` FROM " + . "`INFORMATION_SCHEMA`.`USER_PRIVILEGES`) t " + . "WHERE `IS_GRANTABLE` = 'YES' AND " + . "'''" . $user . "''@''" . $host . "''' LIKE `GRANTEE` LIMIT 1"; + } + + $is = false; + $result = $this->tryQuery( + $query, + self::CONNECT_USER, + self::QUERY_STORE + ); + if ($result) { + $is = (bool) $this->numRows($result); + } + $this->freeResult($result); + } else { + $is = false; + $grants = $this->fetchResult( + "SHOW GRANTS FOR CURRENT_USER();", + null, + null, + self::CONNECT_USER, + self::QUERY_STORE + ); + if ($grants) { + foreach ($grants as $grant) { + if ($type === 'create') { + if (strpos($grant, "ALL PRIVILEGES ON *.*") !== false + || strpos($grant, "CREATE USER") !== false + ) { + $is = true; + break; + } + } elseif ($type === 'grant') { + if (strpos($grant, "WITH GRANT OPTION") !== false) { + $is = true; + break; + } + } + } + } + } + + Util::cacheSet('is_' . $type . 'user', $is); + return $is; + } + + /** + * Get the current user and host + * + * @return array array of username and hostname + */ + public function getCurrentUserAndHost(): array + { + if (count($this->_current_user) === 0) { + $user = $this->getCurrentUser(); + $this->_current_user = explode("@", $user); + } + return $this->_current_user; + } + + /** + * Returns value for lower_case_table_names variable + * + * @return string|bool + */ + public function getLowerCaseNames() + { + if ($this->_lower_case_table_names === null) { + $this->_lower_case_table_names = $this->fetchValue( + "SELECT @@lower_case_table_names" + ); + } + return $this->_lower_case_table_names; + } + + /** + * Get the list of system schemas + * + * @return array list of system schemas + */ + public function getSystemSchemas(): array + { + $schemas = [ + 'information_schema', + 'performance_schema', + 'mysql', + 'sys', + ]; + $systemSchemas = []; + foreach ($schemas as $schema) { + if ($this->isSystemSchema($schema, true)) { + $systemSchemas[] = $schema; + } + } + return $systemSchemas; + } + + /** + * Checks whether given schema is a system schema + * + * @param string $schema_name Name of schema (database) to test + * @param bool $testForMysqlSchema Whether 'mysql' schema should + * be treated the same as IS and DD + * + * @return bool + */ + public function isSystemSchema( + string $schema_name, + bool $testForMysqlSchema = false + ): bool { + $schema_name = strtolower($schema_name); + return $schema_name == 'information_schema' + || $schema_name == 'performance_schema' + || ($schema_name == 'mysql' && $testForMysqlSchema) + || $schema_name == 'sys'; + } + + /** + * Return connection parameters for the database server + * + * @param integer $mode Connection mode on of CONNECT_USER, CONNECT_CONTROL + * or CONNECT_AUXILIARY. + * @param array|null $server Server information like host/port/socket/persistent + * + * @return array user, host and server settings array + */ + public function getConnectionParams(int $mode, ?array $server = null): array + { + global $cfg; + + $user = null; + $password = null; + + if ($mode == DatabaseInterface::CONNECT_USER) { + $user = $cfg['Server']['user']; + $password = $cfg['Server']['password']; + $server = $cfg['Server']; + } elseif ($mode == DatabaseInterface::CONNECT_CONTROL) { + $user = $cfg['Server']['controluser']; + $password = $cfg['Server']['controlpass']; + + $server = []; + + if (! empty($cfg['Server']['controlhost'])) { + $server['host'] = $cfg['Server']['controlhost']; + } else { + $server['host'] = $cfg['Server']['host']; + } + // Share the settings if the host is same + if ($server['host'] == $cfg['Server']['host']) { + $shared = [ + 'port', + 'socket', + 'compress', + 'ssl', + 'ssl_key', + 'ssl_cert', + 'ssl_ca', + 'ssl_ca_path', + 'ssl_ciphers', + 'ssl_verify', + ]; + foreach ($shared as $item) { + if (isset($cfg['Server'][$item])) { + $server[$item] = $cfg['Server'][$item]; + } + } + } + // Set configured port + if (! empty($cfg['Server']['controlport'])) { + $server['port'] = $cfg['Server']['controlport']; + } + // Set any configuration with control_ prefix + foreach ($cfg['Server'] as $key => $val) { + if (substr($key, 0, 8) === 'control_') { + $server[substr($key, 8)] = $val; + } + } + } else { + if ($server === null) { + return [ + null, + null, + null, + ]; + } + if (isset($server['user'])) { + $user = $server['user']; + } + if (isset($server['password'])) { + $password = $server['password']; + } + } + + // Perform sanity checks on some variables + if (empty($server['port'])) { + $server['port'] = 0; + } else { + $server['port'] = intval($server['port']); + } + if (empty($server['socket'])) { + $server['socket'] = null; + } + if (empty($server['host'])) { + $server['host'] = 'localhost'; + } + if (! isset($server['ssl'])) { + $server['ssl'] = false; + } + if (! isset($server['compress'])) { + $server['compress'] = false; + } + + return [ + $user, + $password, + $server, + ]; + } + + /** + * connects to the database server + * + * @param integer $mode Connection mode on of CONNECT_USER, CONNECT_CONTROL + * or CONNECT_AUXILIARY. + * @param array|null $server Server information like host/port/socket/persistent + * @param integer $target How to store connection link, defaults to $mode + * + * @return mixed false on error or a connection object on success + */ + public function connect(int $mode, ?array $server = null, ?int $target = null) + { + list($user, $password, $server) = $this->getConnectionParams($mode, $server); + + if ($target === null) { + $target = $mode; + } + + if ($user === null || $password === null) { + trigger_error( + __('Missing connection parameters!'), + E_USER_WARNING + ); + return false; + } + + // Do not show location and backtrace for connection errors + $GLOBALS['error_handler']->setHideLocation(true); + $result = $this->_extension->connect( + $user, + $password, + $server + ); + $GLOBALS['error_handler']->setHideLocation(false); + + if ($result) { + $this->_links[$target] = $result; + /* Run post connect for user connections */ + if ($target == DatabaseInterface::CONNECT_USER) { + $this->postConnect(); + } + return $result; + } + + if ($mode == DatabaseInterface::CONNECT_CONTROL) { + trigger_error( + __( + 'Connection for controluser as defined in your ' + . 'configuration failed.' + ), + E_USER_WARNING + ); + return false; + } elseif ($mode == DatabaseInterface::CONNECT_AUXILIARY) { + // Do not go back to main login if connection failed + // (currently used only in unit testing) + return false; + } + + return $result; + } + + /** + * selects given database + * + * @param string $dbname database name to select + * @param integer $link link type + * + * @return boolean + */ + public function selectDb(string $dbname, $link = DatabaseInterface::CONNECT_USER): bool + { + if (! isset($this->_links[$link])) { + return false; + } + return $this->_extension->selectDb($dbname, $this->_links[$link]); + } + + /** + * returns array of rows with associative and numeric keys from $result + * + * @param object $result result set identifier + * + * @return array + */ + public function fetchArray($result) + { + return $this->_extension->fetchArray($result); + } + + /** + * returns array of rows with associative keys from $result + * + * @param object $result result set identifier + * + * @return array|bool + */ + public function fetchAssoc($result) + { + return $this->_extension->fetchAssoc($result); + } + + /** + * returns array of rows with numeric keys from $result + * + * @param object $result result set identifier + * + * @return array|bool + */ + public function fetchRow($result) + { + return $this->_extension->fetchRow($result); + } + + /** + * Adjusts the result pointer to an arbitrary row in the result + * + * @param object $result database result + * @param integer $offset offset to seek + * + * @return bool true on success, false on failure + */ + public function dataSeek($result, int $offset): bool + { + return $this->_extension->dataSeek($result, $offset); + } + + /** + * Frees memory associated with the result + * + * @param object $result database result + * + * @return void + */ + public function freeResult($result): void + { + $this->_extension->freeResult($result); + } + + /** + * Check if there are any more query results from a multi query + * + * @param integer $link link type + * + * @return bool true or false + */ + public function moreResults($link = DatabaseInterface::CONNECT_USER): bool + { + if (! isset($this->_links[$link])) { + return false; + } + return $this->_extension->moreResults($this->_links[$link]); + } + + /** + * Prepare next result from multi_query + * + * @param integer $link link type + * + * @return bool true or false + */ + public function nextResult($link = DatabaseInterface::CONNECT_USER): bool + { + if (! isset($this->_links[$link])) { + return false; + } + return $this->_extension->nextResult($this->_links[$link]); + } + + /** + * Store the result returned from multi query + * + * @param integer $link link type + * + * @return mixed false when empty results / result set when not empty + */ + public function storeResult($link = DatabaseInterface::CONNECT_USER) + { + if (! isset($this->_links[$link])) { + return false; + } + return $this->_extension->storeResult($this->_links[$link]); + } + + /** + * Returns a string representing the type of connection used + * + * @param integer $link link type + * + * @return string|bool type of connection used + */ + public function getHostInfo($link = DatabaseInterface::CONNECT_USER) + { + if (! isset($this->_links[$link])) { + return false; + } + return $this->_extension->getHostInfo($this->_links[$link]); + } + + /** + * Returns the version of the MySQL protocol used + * + * @param integer $link link type + * + * @return int|bool version of the MySQL protocol used + */ + public function getProtoInfo($link = DatabaseInterface::CONNECT_USER) + { + if (! isset($this->_links[$link])) { + return false; + } + return $this->_extension->getProtoInfo($this->_links[$link]); + } + + /** + * returns a string that represents the client library version + * + * @param integer $link link type + * + * @return string MySQL client library version + */ + public function getClientInfo($link = DatabaseInterface::CONNECT_USER): string + { + if (! isset($this->_links[$link])) { + return ''; + } + return $this->_extension->getClientInfo($this->_links[$link]); + } + + /** + * returns last error message or false if no errors occurred + * + * @param integer $link link type + * + * @return string|bool error or false + */ + public function getError($link = DatabaseInterface::CONNECT_USER) + { + if (! isset($this->_links[$link])) { + return false; + } + return $this->_extension->getError($this->_links[$link]); + } + + /** + * returns the number of rows returned by last query + * + * @param object $result result set identifier + * + * @return string|int + */ + public function numRows($result) + { + return $this->_extension->numRows($result); + } + + /** + * returns last inserted auto_increment id for given $link + * or $GLOBALS['userlink'] + * + * @param integer $link link type + * + * @return int|boolean + */ + public function insertId($link = DatabaseInterface::CONNECT_USER) + { + // If the primary key is BIGINT we get an incorrect result + // (sometimes negative, sometimes positive) + // and in the present function we don't know if the PK is BIGINT + // so better play safe and use LAST_INSERT_ID() + // + // When no controluser is defined, using mysqli_insert_id($link) + // does not always return the last insert id due to a mixup with + // the tracking mechanism, but this works: + return $this->fetchValue('SELECT LAST_INSERT_ID();', 0, 0, $link); + } + + /** + * returns the number of rows affected by last query + * + * @param integer $link link type + * @param bool $get_from_cache whether to retrieve from cache + * + * @return int|boolean + */ + public function affectedRows( + $link = DatabaseInterface::CONNECT_USER, + bool $get_from_cache = true + ) { + if (! isset($this->_links[$link])) { + return false; + } + + if ($get_from_cache) { + return $GLOBALS['cached_affected_rows']; + } + + return $this->_extension->affectedRows($this->_links[$link]); + } + + /** + * returns metainfo for fields in $result + * + * @param object $result result set identifier + * + * @return mixed meta info for fields in $result + */ + public function getFieldsMeta($result) + { + $result = $this->_extension->getFieldsMeta($result); + + if ($this->getLowerCaseNames() === '2') { + /** + * Fixup orgtable for lower_case_table_names = 2 + * + * In this setup MySQL server reports table name lower case + * but we still need to operate on original case to properly + * match existing strings + */ + foreach ($result as $value) { + if (strlen($value->orgtable) !== 0 && + mb_strtolower($value->orgtable) === mb_strtolower($value->table)) { + $value->orgtable = $value->table; + } + } + } + + return $result; + } + + /** + * return number of fields in given $result + * + * @param object $result result set identifier + * + * @return int field count + */ + public function numFields($result): int + { + return $this->_extension->numFields($result); + } + + /** + * returns the length of the given field $i in $result + * + * @param object $result result set identifier + * @param int $i field + * + * @return int|bool length of field + */ + public function fieldLen($result, int $i) + { + return $this->_extension->fieldLen($result, $i); + } + + /** + * returns name of $i. field in $result + * + * @param object $result result set identifier + * @param int $i field + * + * @return string name of $i. field in $result + */ + public function fieldName($result, int $i): string + { + return $this->_extension->fieldName($result, $i); + } + + /** + * returns concatenated string of human readable field flags + * + * @param object $result result set identifier + * @param int $i field + * + * @return string field flags + */ + public function fieldFlags($result, $i): string + { + return $this->_extension->fieldFlags($result, $i); + } + + /** + * returns properly escaped string for use in MySQL queries + * + * @param string $str string to be escaped + * @param mixed $link optional database link to use + * + * @return string a MySQL escaped string + */ + public function escapeString(string $str, $link = DatabaseInterface::CONNECT_USER) + { + if ($this->_extension === null || ! isset($this->_links[$link])) { + return $str; + } + + return $this->_extension->escapeString($this->_links[$link], $str); + } + + /** + * Checks if this database server is running on Amazon RDS. + * + * @return boolean + */ + public function isAmazonRds(): bool + { + if (Util::cacheExists('is_amazon_rds')) { + return Util::cacheGet('is_amazon_rds'); + } + $sql = 'SELECT @@basedir'; + $result = $this->fetchValue($sql); + $rds = (substr($result, 0, 10) == '/rdsdbbin/'); + Util::cacheSet('is_amazon_rds', $rds); + + return $rds; + } + + /** + * Gets SQL for killing a process. + * + * @param int $process Process ID + * + * @return string + */ + public function getKillQuery(int $process): string + { + if ($this->isAmazonRds()) { + return 'CALL mysql.rds_kill(' . $process . ');'; + } + + return 'KILL ' . $process . ';'; + } + + /** + * Get the phpmyadmin database manager + * + * @return SystemDatabase + */ + public function getSystemDatabase(): SystemDatabase + { + return new SystemDatabase($this); + } + + /** + * Get a table with database name and table name + * + * @param string $db_name DB name + * @param string $table_name Table name + * + * @return Table + */ + public function getTable(string $db_name, string $table_name): Table + { + return new Table($table_name, $db_name, $this); + } + + /** + * returns collation of given db + * + * @param string $db name of db + * + * @return string collation of $db + */ + public function getDbCollation(string $db): string + { + if ($this->isSystemSchema($db)) { + // We don't have to check the collation of the virtual + // information_schema database: We know it! + return 'utf8_general_ci'; + } + + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + // this is slow with thousands of databases + $sql = 'SELECT DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA' + . ' WHERE SCHEMA_NAME = \'' . $this->escapeString($db) + . '\' LIMIT 1'; + return $this->fetchValue($sql); + } + + $this->selectDb($db); + $return = $this->fetchValue('SELECT @@collation_database'); + if ($db !== $GLOBALS['db']) { + $this->selectDb($GLOBALS['db']); + } + return $return; + } + + /** + * returns default server collation from show variables + * + * @return string + */ + public function getServerCollation(): string + { + return $this->fetchValue('SELECT @@collation_server'); + } + + /** + * Server version as number + * + * @return integer + */ + public function getVersion(): int + { + return $this->_version_int; + } + + /** + * Server version + * + * @return string + */ + public function getVersionString(): string + { + return $this->_version_str; + } + + /** + * Server version comment + * + * @return string + */ + public function getVersionComment(): string + { + return $this->_version_comment; + } + + /** + * Whether connection is MariaDB + * + * @return boolean + */ + public function isMariaDB(): bool + { + return $this->_is_mariadb; + } + + /** + * Whether connection is Percona + * + * @return boolean + */ + public function isPercona(): bool + { + return $this->_is_percona; + } + + /** + * Load correct database driver + * + * @param DbiExtension|null $extension Force the use of an alternative extension + * + * @return self + */ + public static function load(?DbiExtension $extension = null): self + { + global $dbi; + + if ($extension !== null) { + $dbi = new self($extension); + return $dbi; + } + + if (! self::checkDbExtension('mysqli')) { + $docUrl = Util::getDocuLink('faq', 'faqmysql'); + $docLink = sprintf( + __('See %sour documentation%s for more information.'), + '[a@' . $docUrl . '@documentation]', + '[/a]' + ); + Core::warnMissingExtension( + 'mysqli', + true, + $docLink + ); + } + + $dbi = new self(new DbiMysqli()); + return $dbi; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Dbi/DbiExtension.php b/srcs/phpmyadmin/libraries/classes/Dbi/DbiExtension.php new file mode 100644 index 0000000..8cfa9fc --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Dbi/DbiExtension.php @@ -0,0 +1,248 @@ + 'num', + MYSQLI_PART_KEY_FLAG => 'part_key', + MYSQLI_SET_FLAG => 'set', + MYSQLI_TIMESTAMP_FLAG => 'timestamp', + MYSQLI_AUTO_INCREMENT_FLAG => 'auto_increment', + MYSQLI_ENUM_FLAG => 'enum', + MYSQLI_ZEROFILL_FLAG => 'zerofill', + MYSQLI_UNSIGNED_FLAG => 'unsigned', + MYSQLI_BLOB_FLAG => 'blob', + MYSQLI_MULTIPLE_KEY_FLAG => 'multiple_key', + MYSQLI_UNIQUE_KEY_FLAG => 'unique_key', + MYSQLI_PRI_KEY_FLAG => 'primary_key', + MYSQLI_NOT_NULL_FLAG => 'not_null', + ]; + + /** + * connects to the database server + * + * @param string $user mysql user name + * @param string $password mysql user password + * @param array $server host/port/socket/persistent + * + * @return mysqli|bool false on error or a mysqli object on success + */ + public function connect($user, $password, array $server) + { + if ($server) { + $server['host'] = empty($server['host']) + ? 'localhost' + : $server['host']; + } + + $mysqli = mysqli_init(); + + $client_flags = 0; + + /* Optionally compress connection */ + if ($server['compress'] && defined('MYSQLI_CLIENT_COMPRESS')) { + $client_flags |= MYSQLI_CLIENT_COMPRESS; + } + + /* Optionally enable SSL */ + if ($server['ssl']) { + $client_flags |= MYSQLI_CLIENT_SSL; + if (! empty($server['ssl_key']) || + ! empty($server['ssl_cert']) || + ! empty($server['ssl_ca']) || + ! empty($server['ssl_ca_path']) || + ! empty($server['ssl_ciphers']) + ) { + if (! isset($server['ssl_key']) || is_null($server['ssl_key'])) { + $server['ssl_key'] = ''; + } + if (! isset($server['ssl_cert']) || is_null($server['ssl_cert'])) { + $server['ssl_cert'] = ''; + } + if (! isset($server['ssl_ca']) || is_null($server['ssl_ca'])) { + $server['ssl_ca'] = ''; + } + if (! isset($server['ssl_ca_path']) || is_null($server['ssl_ca_path'])) { + $server['ssl_ca_path'] = ''; + } + if (! isset($server['ssl_ciphers']) || is_null($server['ssl_ciphers'])) { + $server['ssl_ciphers'] = ''; + } + $mysqli->ssl_set( + $server['ssl_key'], + $server['ssl_cert'], + $server['ssl_ca'], + $server['ssl_ca_path'], + $server['ssl_ciphers'] + ); + } + /* + * disables SSL certificate validation on mysqlnd for MySQL 5.6 or later + * @link https://bugs.php.net/bug.php?id=68344 + * @link https://github.com/phpmyadmin/phpmyadmin/pull/11838 + */ + if (! $server['ssl_verify']) { + $mysqli->options( + MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, + $server['ssl_verify'] + ); + $client_flags |= MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT; + } + } + + if ($GLOBALS['cfg']['PersistentConnections']) { + $host = 'p:' . $server['host']; + } else { + $host = $server['host']; + } + + $return_value = $mysqli->real_connect( + $host, + $user, + $password, + '', + $server['port'], + (string) $server['socket'], + $client_flags + ); + + if ($return_value === false || $return_value === null) { + /* + * Switch to SSL if server asked us to do so, unfortunately + * there are more ways MySQL server can tell this: + * + * - MySQL 8.0 and newer should return error 3159 + * - #2001 - SSL Connection is required. Please specify SSL options and retry. + * - #9002 - SSL connection is required. Please specify SSL options and retry. + */ + $error_number = $mysqli->connect_errno; + $error_message = $mysqli->connect_error; + if (! $server['ssl'] && ($error_number == 3159 || + (($error_number == 2001 || $error_number == 9002) && stripos($error_message, 'SSL Connection is required') !== false)) + ) { + trigger_error( + __('SSL connection enforced by server, automatically enabling it.'), + E_USER_WARNING + ); + $server['ssl'] = true; + return self::connect($user, $password, $server); + } + return false; + } + + if (defined('PMA_ENABLE_LDI')) { + $mysqli->options(MYSQLI_OPT_LOCAL_INFILE, true); + } else { + $mysqli->options(MYSQLI_OPT_LOCAL_INFILE, false); + } + + return $mysqli; + } + + /** + * selects given database + * + * @param string $databaseName database name to select + * @param mysqli $mysqli the mysqli object + * + * @return boolean + */ + public function selectDb($databaseName, $mysqli) + { + return $mysqli->select_db($databaseName); + } + + /** + * runs a query and returns the result + * + * @param string $query query to execute + * @param mysqli $mysqli mysqli object + * @param int $options query options + * + * @return mysqli_result|bool + */ + public function realQuery($query, $mysqli, $options) + { + if ($options == ($options | DatabaseInterface::QUERY_STORE)) { + $method = MYSQLI_STORE_RESULT; + } elseif ($options == ($options | DatabaseInterface::QUERY_UNBUFFERED)) { + $method = MYSQLI_USE_RESULT; + } else { + $method = 0; + } + + return $mysqli->query($query, $method); + } + + /** + * Run the multi query and output the results + * + * @param mysqli $mysqli mysqli object + * @param string $query multi query statement to execute + * + * @return bool + */ + public function realMultiQuery($mysqli, $query) + { + return $mysqli->multi_query($query); + } + + /** + * returns array of rows with associative and numeric keys from $result + * + * @param mysqli_result $result result set identifier + * + * @return array|null + */ + public function fetchArray($result) + { + if (! $result instanceof mysqli_result) { + return null; + } + return $result->fetch_array(MYSQLI_BOTH); + } + + /** + * returns array of rows with associative keys from $result + * + * @param mysqli_result $result result set identifier + * + * @return array|null + */ + public function fetchAssoc($result) + { + if (! $result instanceof mysqli_result) { + return null; + } + return $result->fetch_array(MYSQLI_ASSOC); + } + + /** + * returns array of rows with numeric keys from $result + * + * @param mysqli_result $result result set identifier + * + * @return array|null + */ + public function fetchRow($result) + { + if (! $result instanceof mysqli_result) { + return null; + } + return $result->fetch_array(MYSQLI_NUM); + } + + /** + * Adjusts the result pointer to an arbitrary row in the result + * + * @param mysqli_result $result database result + * @param integer $offset offset to seek + * + * @return bool true on success, false on failure + */ + public function dataSeek($result, $offset) + { + return $result->data_seek($offset); + } + + /** + * Frees memory associated with the result + * + * @param mysqli_result $result database result + * + * @return void + */ + public function freeResult($result) + { + if ($result instanceof mysqli_result) { + $result->close(); + } + } + + /** + * Check if there are any more query results from a multi query + * + * @param mysqli $mysqli the mysqli object + * + * @return bool true or false + */ + public function moreResults($mysqli) + { + return $mysqli->more_results(); + } + + /** + * Prepare next result from multi_query + * + * @param mysqli $mysqli the mysqli object + * + * @return bool true or false + */ + public function nextResult($mysqli) + { + return $mysqli->next_result(); + } + + /** + * Store the result returned from multi query + * + * @param mysqli $mysqli the mysqli object + * + * @return mysqli_result|bool false when empty results / result set when not empty + */ + public function storeResult($mysqli) + { + return $mysqli->store_result(); + } + + /** + * Returns a string representing the type of connection used + * + * @param mysqli $mysqli mysql link + * + * @return string type of connection used + */ + public function getHostInfo($mysqli) + { + return $mysqli->host_info; + } + + /** + * Returns the version of the MySQL protocol used + * + * @param mysqli $mysqli mysql link + * + * @return string version of the MySQL protocol used + */ + public function getProtoInfo($mysqli) + { + return $mysqli->protocol_version; + } + + /** + * returns a string that represents the client library version + * + * @param mysqli $mysqli mysql link + * + * @return string MySQL client library version + */ + public function getClientInfo($mysqli) + { + return $mysqli->get_client_info(); + } + + /** + * returns last error message or false if no errors occurred + * + * @param mysqli $mysqli mysql link + * + * @return string|bool error or false + */ + public function getError($mysqli) + { + $GLOBALS['errno'] = 0; + + if (null !== $mysqli && false !== $mysqli) { + $error_number = $mysqli->errno; + $error_message = $mysqli->error; + } else { + $error_number = $mysqli->connect_errno; + $error_message = $mysqli->connect_error; + } + if (0 == $error_number) { + return false; + } + + // keep the error number for further check after + // the call to getError() + $GLOBALS['errno'] = $error_number; + + return $GLOBALS['dbi']->formatError($error_number, $error_message); + } + + /** + * returns the number of rows returned by last query + * + * @param mysqli_result $result result set identifier + * + * @return string|int + */ + public function numRows($result) + { + // see the note for tryQuery(); + if (is_bool($result)) { + return 0; + } + + return $result->num_rows; + } + + /** + * returns the number of rows affected by last query + * + * @param mysqli $mysqli the mysqli object + * + * @return int + */ + public function affectedRows($mysqli) + { + return $mysqli->affected_rows; + } + + /** + * returns meta info for fields in $result + * + * @param mysqli_result $result result set identifier + * + * @return array|bool meta info for fields in $result + */ + public function getFieldsMeta($result) + { + if (! $result instanceof mysqli_result) { + return false; + } + // Build an associative array for a type look up + $typeAr = []; + $typeAr[MYSQLI_TYPE_DECIMAL] = 'real'; + $typeAr[MYSQLI_TYPE_NEWDECIMAL] = 'real'; + $typeAr[MYSQLI_TYPE_BIT] = 'int'; + $typeAr[MYSQLI_TYPE_TINY] = 'int'; + $typeAr[MYSQLI_TYPE_SHORT] = 'int'; + $typeAr[MYSQLI_TYPE_LONG] = 'int'; + $typeAr[MYSQLI_TYPE_FLOAT] = 'real'; + $typeAr[MYSQLI_TYPE_DOUBLE] = 'real'; + $typeAr[MYSQLI_TYPE_NULL] = 'null'; + $typeAr[MYSQLI_TYPE_TIMESTAMP] = 'timestamp'; + $typeAr[MYSQLI_TYPE_LONGLONG] = 'int'; + $typeAr[MYSQLI_TYPE_INT24] = 'int'; + $typeAr[MYSQLI_TYPE_DATE] = 'date'; + $typeAr[MYSQLI_TYPE_TIME] = 'time'; + $typeAr[MYSQLI_TYPE_DATETIME] = 'datetime'; + $typeAr[MYSQLI_TYPE_YEAR] = 'year'; + $typeAr[MYSQLI_TYPE_NEWDATE] = 'date'; + $typeAr[MYSQLI_TYPE_ENUM] = 'unknown'; + $typeAr[MYSQLI_TYPE_SET] = 'unknown'; + $typeAr[MYSQLI_TYPE_TINY_BLOB] = 'blob'; + $typeAr[MYSQLI_TYPE_MEDIUM_BLOB] = 'blob'; + $typeAr[MYSQLI_TYPE_LONG_BLOB] = 'blob'; + $typeAr[MYSQLI_TYPE_BLOB] = 'blob'; + $typeAr[MYSQLI_TYPE_VAR_STRING] = 'string'; + $typeAr[MYSQLI_TYPE_STRING] = 'string'; + // MySQL returns MYSQLI_TYPE_STRING for CHAR + // and MYSQLI_TYPE_CHAR === MYSQLI_TYPE_TINY + // so this would override TINYINT and mark all TINYINT as string + // see https://github.com/phpmyadmin/phpmyadmin/issues/8569 + //$typeAr[MYSQLI_TYPE_CHAR] = 'string'; + $typeAr[MYSQLI_TYPE_GEOMETRY] = 'geometry'; + $typeAr[MYSQLI_TYPE_BIT] = 'bit'; + $typeAr[MYSQLI_TYPE_JSON] = 'json'; + + $fields = $result->fetch_fields(); + + if (! is_array($fields)) { + return false; + } + + foreach ($fields as $k => $field) { + $fields[$k]->_type = $field->type; + $fields[$k]->type = $typeAr[$field->type]; + $fields[$k]->_flags = $field->flags; + $fields[$k]->flags = $this->fieldFlags($result, $k); + + // Enhance the field objects for mysql-extension compatibility + //$flags = explode(' ', $fields[$k]->flags); + //array_unshift($flags, 'dummy'); + $fields[$k]->multiple_key + = (int) (bool) ($fields[$k]->_flags & MYSQLI_MULTIPLE_KEY_FLAG); + $fields[$k]->primary_key + = (int) (bool) ($fields[$k]->_flags & MYSQLI_PRI_KEY_FLAG); + $fields[$k]->unique_key + = (int) (bool) ($fields[$k]->_flags & MYSQLI_UNIQUE_KEY_FLAG); + $fields[$k]->not_null + = (int) (bool) ($fields[$k]->_flags & MYSQLI_NOT_NULL_FLAG); + $fields[$k]->unsigned + = (int) (bool) ($fields[$k]->_flags & MYSQLI_UNSIGNED_FLAG); + $fields[$k]->zerofill + = (int) (bool) ($fields[$k]->_flags & MYSQLI_ZEROFILL_FLAG); + $fields[$k]->numeric + = (int) (bool) ($fields[$k]->_flags & MYSQLI_NUM_FLAG); + $fields[$k]->blob + = (int) (bool) ($fields[$k]->_flags & MYSQLI_BLOB_FLAG); + } + return $fields; + } + + /** + * return number of fields in given $result + * + * @param mysqli_result $result result set identifier + * + * @return int field count + */ + public function numFields($result) + { + return $result->field_count; + } + + /** + * returns the length of the given field $i in $result + * + * @param mysqli_result $result result set identifier + * @param int $i field + * + * @return int|bool length of field + */ + public function fieldLen($result, $i) + { + if ($i >= $this->numFields($result)) { + return false; + } + /** @var stdClass $fieldDefinition */ + $fieldDefinition = $result->fetch_field_direct($i); + if ($fieldDefinition !== false) { + return $fieldDefinition->length; + } + return false; + } + + /** + * returns name of $i. field in $result + * + * @param mysqli_result $result result set identifier + * @param int $i field + * + * @return string|bool name of $i. field in $result + */ + public function fieldName($result, $i) + { + if ($i >= $this->numFields($result)) { + return false; + } + /** @var stdClass $fieldDefinition */ + $fieldDefinition = $result->fetch_field_direct($i); + if ($fieldDefinition !== false) { + return $fieldDefinition->name; + } + return false; + } + + /** + * returns concatenated string of human readable field flags + * + * @param mysqli_result $result result set identifier + * @param int $i field + * + * @return string|false field flags + */ + public function fieldFlags($result, $i) + { + if ($i >= $this->numFields($result)) { + return false; + } + /** @var stdClass $fieldDefinition */ + $fieldDefinition = $result->fetch_field_direct($i); + if ($fieldDefinition !== false) { + $type = $fieldDefinition->type; + $charsetNumber = $fieldDefinition->charsetnr; + $fieldDefinitionFlags = $fieldDefinition->flags; + $flags = []; + foreach (self::$pma_mysqli_flag_names as $flag => $name) { + if ($fieldDefinitionFlags & $flag) { + $flags[] = $name; + } + } + // See https://dev.mysql.com/doc/refman/6.0/en/c-api-datatypes.html: + // to determine if a string is binary, we should not use MYSQLI_BINARY_FLAG + // but instead the charsetnr member of the MYSQL_FIELD + // structure. Watch out: some types like DATE returns 63 in charsetnr + // so we have to check also the type. + // Unfortunately there is no equivalent in the mysql extension. + if (($type == MYSQLI_TYPE_TINY_BLOB || $type == MYSQLI_TYPE_BLOB + || $type == MYSQLI_TYPE_MEDIUM_BLOB || $type == MYSQLI_TYPE_LONG_BLOB + || $type == MYSQLI_TYPE_VAR_STRING || $type == MYSQLI_TYPE_STRING) + && 63 == $charsetNumber + ) { + $flags[] = 'binary'; + } + return implode(' ', $flags); + } else { + return ''; + } + } + + /** + * returns properly escaped string for use in MySQL queries + * + * @param mysqli $mysqli database link + * @param string $string string to be escaped + * + * @return string a MySQL escaped string + */ + public function escapeString($mysqli, $string) + { + return $mysqli->real_escape_string($string); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Di/Migration.php b/srcs/phpmyadmin/libraries/classes/Di/Migration.php new file mode 100644 index 0000000..2990610 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Di/Migration.php @@ -0,0 +1,71 @@ +containerBuilder = $containerBuilder; + } + + /** + * Get the instance of the service + * + * @param string $key Key of data to store + * @param mixed $value Data to store + * + * @return void + */ + public function setGlobal(string $key, $value) + { + $GLOBALS[$key] = $value; + $this->containerBuilder->setParameter($key, $value); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Display/ChangePassword.php b/srcs/phpmyadmin/libraries/classes/Display/ChangePassword.php new file mode 100644 index 0000000..a786b6c --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Display/ChangePassword.php @@ -0,0 +1,182 @@ +'; + + $html .= Url::getHiddenInputs(); + + if (strpos($GLOBALS['PMA_PHP_SELF'], 'server_privileges') !== false) { + $html .= '' + . ''; + } + $html .= '
' + . '' . __('Change password') . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . ''; + + $serverType = Util::getServerType(); + $serverVersion = $GLOBALS['dbi']->getVersion(); + $orig_auth_plugin = $serverPrivileges->getCurrentAuthenticationPlugin( + 'change', + $username, + $hostname + ); + + if (($serverType == 'MySQL' + && $serverVersion >= 50507) + || ($serverType == 'MariaDB' + && $serverVersion >= 50200) + ) { + // Provide this option only for 5.7.6+ + // OR for privileged users in 5.5.7+ + if (($serverType == 'MySQL' + && $serverVersion >= 50706) + || ($GLOBALS['dbi']->isSuperuser() && $mode == 'edit_other') + ) { + $auth_plugin_dropdown = $serverPrivileges->getHtmlForAuthPluginsDropdown( + $orig_auth_plugin, + 'change_pw', + 'new' + ); + + $html .= '' + . '' + . '' + . '
' + . '' + . '' + . '
' + . '' + . '' + . '' + . __('Enter:') . '     ' + . '' + . 'Strength: ' + . ' ' + . 'Good' + . '
' . __('Re-type:') . ' ' + . '' + . '
' . __('Password Hashing:') . ''; + $html .= $auth_plugin_dropdown; + $html .= '
'; + + $html .= '' + . Message::notice( + __( + 'This method requires using an \'SSL connection\' ' + . 'or an \'unencrypted connection that encrypts the ' + . 'password using RSA\'; while connecting to the server.' + ) + . Util::showMySQLDocu( + 'sha256-authentication-plugin' + ) + ) + ->getDisplay() + . '
'; + } else { + $html .= '' + . ''; + } + } else { + $auth_plugin_dropdown = $serverPrivileges->getHtmlForAuthPluginsDropdown( + $orig_auth_plugin, + 'change_pw', + 'old' + ); + + $html .= '' + . '' . __('Password Hashing:') . ''; + $html .= $auth_plugin_dropdown . '' + . '' + . ''; + } + + $html .= '' + . '' + . ''; + return $html; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Display/CreateTable.php b/srcs/phpmyadmin/libraries/classes/Display/CreateTable.php new file mode 100644 index 0000000..d984e55 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Display/CreateTable.php @@ -0,0 +1,56 @@ += 4.1.0, we should be able to detect if user has a CREATE + * privilege by looking at SHOW GRANTS output; + * for < 4.1.0, it could be more difficult because the logic tries to + * detect the current host and it might be expressed in many ways; also + * on a shared server, the user might be unable to define a controluser + * that has the proper rights to the "mysql" db; + * so we give up and assume that user has the right to create a table + * + * Note: in this case we could even skip the following "foreach" logic + * + * Addendum, 2006-01-19: ok, I give up. We got some reports about servers + * where the hostname field in mysql.user is not the same as the one + * in mysql.db for a user. In this case, SHOW GRANTS does not return + * the db-specific privileges. And probably, those users are on a shared + * server, so can't set up a control user with rights to the "mysql" db. + * We cannot reliably detect the db-specific privileges, so no more + * warnings about the lack of privileges for CREATE TABLE. Tested + * on MySQL 5.0.18. + * + * @package PhpMyAdmin + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Display; + +use PhpMyAdmin\CheckUserPrivileges; +use PhpMyAdmin\Template; + +/** + * PhpMyAdmin\Display\CreateTable class + * + * @package PhpMyAdmin + */ +class CreateTable +{ + /** + * Returns the html for create table. + * + * @param string $db database name + * + * @return string + */ + public static function getHtml($db) + { + $checkUserPrivileges = new CheckUserPrivileges($GLOBALS['dbi']); + $checkUserPrivileges->getPrivileges(); + + $template = new Template(); + return $template->render('database/create_table', ['db' => $db]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Display/Error.php b/srcs/phpmyadmin/libraries/classes/Display/Error.php new file mode 100644 index 0000000..a601532 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Display/Error.php @@ -0,0 +1,56 @@ +render( + 'error/generic', + [ + 'lang' => $lang, + 'dir' => $dir, + 'error_header' => $errorHeader, + 'error_message' => Sanitize::sanitizeMessage($errorMessage), + ] + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Display/Export.php b/srcs/phpmyadmin/libraries/classes/Display/Export.php new file mode 100644 index 0000000..db02a5e --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Display/Export.php @@ -0,0 +1,825 @@ +relation = new Relation($GLOBALS['dbi']); + $this->template = new Template(); + } + + /** + * Outputs appropriate checked statement for checkbox. + * + * @param string $str option name + * + * @return boolean + */ + private function checkboxCheck($str) + { + return isset($GLOBALS['cfg']['Export'][$str]) + && $GLOBALS['cfg']['Export'][$str]; + } + + /** + * Prints Html For Export Selection Options + * + * @param string $tmpSelect Tmp selected method of export + * + * @return string + */ + public function getHtmlForSelectOptions($tmpSelect = '') + { + // Check if the selected databases are defined in $_POST + // (from clicking Back button on export.php) + if (isset($_POST['db_select'])) { + $_POST['db_select'] = urldecode($_POST['db_select']); + $_POST['db_select'] = explode(",", $_POST['db_select']); + } + + $databases = []; + foreach ($GLOBALS['dblist']->databases as $currentDb) { + if ($GLOBALS['dbi']->isSystemSchema($currentDb, true)) { + continue; + } + $isSelected = false; + if (isset($_POST['db_select'])) { + if (in_array($currentDb, $_POST['db_select'])) { + $isSelected = true; + } + } elseif (! empty($tmpSelect)) { + if (mb_strpos( + ' ' . $tmpSelect, + '|' . $currentDb . '|' + )) { + $isSelected = true; + } + } else { + $isSelected = true; + } + $databases[] = [ + 'name' => $currentDb, + 'is_selected' => $isSelected, + ]; + } + + return $this->template->render('display/export/select_options', [ + 'databases' => $databases, + ]); + } + + /** + * Prints Html For Export Hidden Input + * + * @param string $exportType Selected Export Type + * @param string $db Selected DB + * @param string $table Selected Table + * @param string $singleTable Single Table + * @param string $sqlQuery SQL Query + * + * @return string + */ + public function getHtmlForHiddenInputs( + $exportType, + $db, + $table, + $singleTable, + $sqlQuery + ) { + global $cfg; + + // If the export method was not set, the default is quick + if (isset($_POST['export_method'])) { + $cfg['Export']['method'] = $_POST['export_method']; + } elseif (! isset($cfg['Export']['method'])) { + $cfg['Export']['method'] = 'quick'; + } + + if (empty($sqlQuery) && isset($_POST['sql_query'])) { + $sqlQuery = $_POST['sql_query']; + } + + return $this->template->render('display/export/hidden_inputs', [ + 'db' => $db, + 'table' => $table, + 'export_type' => $exportType, + 'export_method' => $cfg['Export']['method'], + 'single_table' => $singleTable, + 'sql_query' => $sqlQuery, + 'template_id' => isset($_POST['template_id']) ? $_POST['template_id'] : '', + ]); + } + + /** + * Returns HTML for the options in template dropdown + * + * @param string $exportType export type - server, database, or table + * + * @return string HTML for the options in teplate dropdown + */ + private function getOptionsForTemplates($exportType) + { + // Get the relation settings + $cfgRelation = $this->relation->getRelationsParam(); + + $query = "SELECT `id`, `template_name` FROM " + . Util::backquote($cfgRelation['db']) . '.' + . Util::backquote($cfgRelation['export_templates']) + . " WHERE `username` = " + . "'" . $GLOBALS['dbi']->escapeString($GLOBALS['cfg']['Server']['user']) + . "' AND `export_type` = '" . $GLOBALS['dbi']->escapeString($exportType) . "'" + . " ORDER BY `template_name`;"; + + $result = $this->relation->queryAsControlUser($query); + + $templates = []; + if ($result !== false) { + while ($row = $GLOBALS['dbi']->fetchAssoc($result, DatabaseInterface::CONNECT_CONTROL)) { + $templates[] = [ + 'name' => $row['template_name'], + 'id' => $row['id'], + ]; + } + } + + return $this->template->render('display/export/template_options', [ + 'templates' => $templates, + 'selected_template' => ! empty($_POST['template_id']) ? $_POST['template_id'] : null, + ]); + } + + /** + * Prints Html For Export Options Method + * + * @return string + */ + private function getHtmlForOptionsMethod() + { + global $cfg; + if (isset($_POST['quick_or_custom'])) { + $exportMethod = $_POST['quick_or_custom']; + } else { + $exportMethod = $cfg['Export']['method']; + } + + return $this->template->render('display/export/method', [ + 'export_method' => $exportMethod, + ]); + } + + /** + * Prints Html For Export Options Selection + * + * @param string $exportType Selected Export Type + * @param string $multiValues Export Options + * + * @return string + */ + private function getHtmlForOptionsSelection($exportType, $multiValues) + { + return $this->template->render('display/export/selection', [ + 'export_type' => $exportType, + 'multi_values' => $multiValues, + ]); + } + + /** + * Prints Html For Export Options Format dropdown + * + * @param ExportPlugin[] $exportList Export List + * + * @return string + */ + private function getHtmlForOptionsFormatDropdown($exportList) + { + $dropdown = Plugins::getChoice('Export', 'what', $exportList, 'format'); + return $this->template->render('display/export/format_dropdown', [ + 'dropdown' => $dropdown, + ]); + } + + /** + * Prints Html For Export Options Format-specific options + * + * @param ExportPlugin[] $exportList Export List + * + * @return string + */ + private function getHtmlForOptionsFormat($exportList) + { + global $cfg; + $options = Plugins::getOptions('Export', $exportList); + + return $this->template->render('display/export/options_format', [ + 'options' => $options, + 'can_convert_kanji' => Encoding::canConvertKanji(), + 'exec_time_limit' => $cfg['ExecTimeLimit'], + ]); + } + + /** + * Prints Html For Export Options Rows + * + * @param string $db Selected DB + * @param string $table Selected Table + * @param string $unlimNumRows Num of Rows + * + * @return string + */ + private function getHtmlForOptionsRows($db, $table, $unlimNumRows) + { + $tableObject = new Table($table, $db); + $numberOfRows = $tableObject->countRecords(); + + return $this->template->render('display/export/options_rows', [ + 'allrows' => isset($_POST['allrows']) ? $_POST['allrows'] : null, + 'limit_to' => isset($_POST['limit_to']) ? $_POST['limit_to'] : null, + 'limit_from' => isset($_POST['limit_from']) ? $_POST['limit_from'] : null, + 'unlim_num_rows' => $unlimNumRows, + 'number_of_rows' => $numberOfRows, + ]); + } + + /** + * Prints Html For Export Options Quick Export + * + * @return string + */ + private function getHtmlForOptionsQuickExport() + { + global $cfg; + $saveDir = Util::userDir($cfg['SaveDir']); + $exportIsChecked = $this->checkboxCheck( + 'quick_export_onserver' + ); + $exportOverwriteIsChecked = $this->checkboxCheck( + 'quick_export_onserver_overwrite' + ); + + return $this->template->render('display/export/options_quick_export', [ + 'save_dir' => $saveDir, + 'export_is_checked' => $exportIsChecked, + 'export_overwrite_is_checked' => $exportOverwriteIsChecked, + ]); + } + + /** + * Prints Html For Export Options Save Dir + * + * @return string + */ + private function getHtmlForOptionsOutputSaveDir() + { + global $cfg; + $saveDir = Util::userDir($cfg['SaveDir']); + $exportIsChecked = $this->checkboxCheck( + 'onserver' + ); + $exportOverwriteIsChecked = $this->checkboxCheck( + 'onserver_overwrite' + ); + + return $this->template->render('display/export/options_output_save_dir', [ + 'save_dir' => $saveDir, + 'export_is_checked' => $exportIsChecked, + 'export_overwrite_is_checked' => $exportOverwriteIsChecked, + ]); + } + + + /** + * Prints Html For Export Options + * + * @param string $exportType Selected Export Type + * + * @return string + */ + private function getHtmlForOptionsOutputFormat($exportType) + { + $trans = new Message(); + $trans->addText(__('@SERVER@ will become the server name')); + if ($exportType == 'database' || $exportType == 'table') { + $trans->addText(__(', @DATABASE@ will become the database name')); + if ($exportType == 'table') { + $trans->addText(__(', @TABLE@ will become the table name')); + } + } + + $msg = new Message( + __( + 'This value is interpreted using %1$sstrftime%2$s, ' + . 'so you can use time formatting strings. ' + . 'Additionally the following transformations will happen: %3$s. ' + . 'Other text will be kept as is. See the %4$sFAQ%5$s for details.' + ) + ); + $msg->addParamHtml( + '' + ); + $msg->addParamHtml(''); + $msg->addParam($trans); + $docUrl = Util::getDocuLink('faq', 'faq6-27'); + $msg->addParamHtml( + '' + ); + $msg->addParamHtml(''); + + if (isset($_POST['filename_template'])) { + $filenameTemplate = $_POST['filename_template']; + } else { + if ($exportType == 'database') { + $filenameTemplate = $GLOBALS['PMA_Config']->getUserValue( + 'pma_db_filename_template', + $GLOBALS['cfg']['Export']['file_template_database'] + ); + } elseif ($exportType == 'table') { + $filenameTemplate = $GLOBALS['PMA_Config']->getUserValue( + 'pma_table_filename_template', + $GLOBALS['cfg']['Export']['file_template_table'] + ); + } else { + $filenameTemplate = $GLOBALS['PMA_Config']->getUserValue( + 'pma_server_filename_template', + $GLOBALS['cfg']['Export']['file_template_server'] + ); + } + } + + return $this->template->render('display/export/options_output_format', [ + 'message' => $msg->getMessage(), + 'filename_template' => $filenameTemplate, + 'is_checked' => $this->checkboxCheck('remember_file_template'), + ]); + } + + /** + * Prints Html For Export Options Charset + * + * @return string + */ + private function getHtmlForOptionsOutputCharset() + { + global $cfg; + + return $this->template->render('display/export/options_output_charset', [ + 'encodings' => Encoding::listEncodings(), + 'export_charset' => $cfg['Export']['charset'], + ]); + } + + /** + * Prints Html For Export Options Compression + * + * @return string + */ + private function getHtmlForOptionsOutputCompression() + { + global $cfg; + if (isset($_POST['compression'])) { + $selectedCompression = $_POST['compression']; + } elseif (isset($cfg['Export']['compression'])) { + $selectedCompression = $cfg['Export']['compression']; + } else { + $selectedCompression = 'none'; + } + + // Since separate files export works with ZIP only + if (isset($cfg['Export']['as_separate_files']) + && $cfg['Export']['as_separate_files'] + ) { + $selectedCompression = 'zip'; + } + + // zip and gzip encode features + $isZip = ($cfg['ZipDump'] && function_exists('gzcompress')); + $isGzip = ($cfg['GZipDump'] && function_exists('gzencode')); + + return $this->template->render('display/export/options_output_compression', [ + 'is_zip' => $isZip, + 'is_gzip' => $isGzip, + 'selected_compression' => $selectedCompression, + ]); + } + + /** + * Prints Html For Export Options Radio + * + * @return string + */ + private function getHtmlForOptionsOutputRadio() + { + return $this->template->render('display/export/options_output_radio', [ + 'has_repopulate' => isset($_POST['repopulate']), + 'export_asfile' => $GLOBALS['cfg']['Export']['asfile'], + ]); + } + + /** + * Prints Html For Export Options Checkbox - Separate files + * + * @param string $exportType Selected Export Type + * + * @return string + */ + private function getHtmlForOptionsOutputSeparateFiles($exportType) + { + $isChecked = $this->checkboxCheck('as_separate_files'); + + return $this->template->render('display/export/options_output_separate_files', [ + 'is_checked' => $isChecked, + 'export_type' => $exportType, + ]); + } + + /** + * Prints Html For Export Options + * + * @param string $exportType Selected Export Type + * + * @return string + */ + private function getHtmlForOptionsOutput($exportType) + { + global $cfg; + + $hasAliases = isset($_SESSION['tmpval']['aliases']) + && ! Core::emptyRecursive($_SESSION['tmpval']['aliases']); + unset($_SESSION['tmpval']['aliases']); + + $isCheckedLockTables = $this->checkboxCheck('lock_tables'); + $isCheckedAsfile = $this->checkboxCheck('asfile'); + + $optionsOutputSaveDir = ''; + if (isset($cfg['SaveDir']) && ! empty($cfg['SaveDir'])) { + $optionsOutputSaveDir = $this->getHtmlForOptionsOutputSaveDir(); + } + $optionsOutputFormat = $this->getHtmlForOptionsOutputFormat($exportType); + $optionsOutputCharset = ''; + if (Encoding::isSupported()) { + $optionsOutputCharset = $this->getHtmlForOptionsOutputCharset(); + } + $optionsOutputCompression = $this->getHtmlForOptionsOutputCompression(); + $optionsOutputSeparateFiles = ''; + if ($exportType == 'server' || $exportType == 'database') { + $optionsOutputSeparateFiles = $this->getHtmlForOptionsOutputSeparateFiles( + $exportType + ); + } + $optionsOutputRadio = $this->getHtmlForOptionsOutputRadio(); + + return $this->template->render('display/export/options_output', [ + 'has_aliases' => $hasAliases, + 'export_type' => $exportType, + 'is_checked_lock_tables' => $isCheckedLockTables, + 'is_checked_asfile' => $isCheckedAsfile, + 'repopulate' => isset($_POST['repopulate']), + 'lock_tables' => isset($_POST['lock_tables']), + 'save_dir' => isset($cfg['SaveDir']) ? $cfg['SaveDir'] : null, + 'is_encoding_supported' => Encoding::isSupported(), + 'options_output_save_dir' => $optionsOutputSaveDir, + 'options_output_format' => $optionsOutputFormat, + 'options_output_charset' => $optionsOutputCharset, + 'options_output_compression' => $optionsOutputCompression, + 'options_output_separate_files' => $optionsOutputSeparateFiles, + 'options_output_radio' => $optionsOutputRadio, + ]); + } + + /** + * Prints Html For Export Options + * + * @param string $exportType Selected Export Type + * @param string $db Selected DB + * @param string $table Selected Table + * @param string $multiValues Export selection + * @param string $numTables number of tables + * @param ExportPlugin[] $exportList Export List + * @param string $unlimNumRows Number of Rows + * + * @return string + */ + public function getHtmlForOptions( + $exportType, + $db, + $table, + $multiValues, + $numTables, + $exportList, + $unlimNumRows + ) { + global $cfg; + $html = $this->getHtmlForOptionsMethod(); + $html .= $this->getHtmlForOptionsFormatDropdown($exportList); + $html .= $this->getHtmlForOptionsSelection($exportType, $multiValues); + + $tableObject = new Table($table, $db); + if (strlen($table) > 0 && empty($numTables) && ! $tableObject->isMerge()) { + $html .= $this->getHtmlForOptionsRows($db, $table, $unlimNumRows); + } + + if (isset($cfg['SaveDir']) && ! empty($cfg['SaveDir'])) { + $html .= $this->getHtmlForOptionsQuickExport(); + } + + $html .= $this->getHtmlForAliasModalDialog(); + $html .= $this->getHtmlForOptionsOutput($exportType); + $html .= $this->getHtmlForOptionsFormat($exportList); + return $html; + } + + /** + * Generate Html For currently defined aliases + * + * @return string + * @throws Throwable + * @throws Twig_Error_Loader + * @throws Twig_Error_Runtime + * @throws Twig_Error_Syntax + */ + private function getHtmlForCurrentAlias() + { + $result = ''; + + $template = $this->template->load('export/alias_item'); + if (isset($_SESSION['tmpval']['aliases'])) { + foreach ($_SESSION['tmpval']['aliases'] as $db => $dbData) { + if (isset($dbData['alias'])) { + $result .= $template->render([ + 'type' => _pgettext('Alias', 'Database'), + 'name' => $db, + 'field' => 'aliases[' . $db . '][alias]', + 'value' => $dbData['alias'], + ]); + } + if (! isset($dbData['tables'])) { + continue; + } + foreach ($dbData['tables'] as $table => $tableData) { + if (isset($tableData['alias'])) { + $result .= $template->render([ + 'type' => _pgettext('Alias', 'Table'), + 'name' => $db . '.' . $table, + 'field' => 'aliases[' . $db . '][tables][' . $table . '][alias]', + 'value' => $tableData['alias'], + ]); + } + if (! isset($tableData['columns'])) { + continue; + } + foreach ($tableData['columns'] as $column => $columnName) { + $result .= $template->render([ + 'type' => _pgettext('Alias', 'Column'), + 'name' => $db . '.' . $table . '.' . $column, + 'field' => 'aliases[' . $db . '][tables][' . $table . '][colums][' . $column . ']', + 'value' => $columnName, + ]); + } + } + } + } + + // Empty row for javascript manipulations + $result .= '' . $template->render([ + 'type' => '', + 'name' => '', + 'field' => 'aliases_new', + 'value' => '', + ]) . ''; + + return $result . '
' + . __('Defined aliases') + . '
'; + } + + /** + * Generate Html For Alias Modal Dialog + * + * @return string + */ + public function getHtmlForAliasModalDialog() + { + $title = __('Rename exported databases/tables/columns'); + + $html = '
'; + $html .= $this->getHtmlForCurrentAlias(); + $html .= $this->template->render('export/alias_add'); + + $html .= '
'; + return $html; + } + + /** + * Gets HTML to display export dialogs + * + * @param string $exportType export type: server|database|table + * @param string $db selected DB + * @param string $table selected table + * @param string $sqlQuery SQL query + * @param int $numTables number of tables + * @param int $unlimNumRows unlimited number of rows + * @param string $multiValues selector options + * + * @return string + */ + public function getDisplay( + $exportType, + $db, + $table, + $sqlQuery, + $numTables, + $unlimNumRows, + $multiValues + ) { + $cfgRelation = $this->relation->getRelationsParam(); + + if (isset($_POST['single_table'])) { + $GLOBALS['single_table'] = $_POST['single_table']; + } + + // Export a single table + if (isset($_GET['single_table'])) { + $GLOBALS['single_table'] = $_GET['single_table']; + } + + /* Scan for plugins */ + /** @var ExportPlugin[] $exportList */ + $exportList = Plugins::getPlugins( + "export", + 'libraries/classes/Plugins/Export/', + [ + 'export_type' => $exportType, + 'single_table' => isset($GLOBALS['single_table']), + ] + ); + + /* Fail if we didn't find any plugin */ + if (empty($exportList)) { + Message::error( + __('Could not load export plugins, please check your installation!') + )->display(); + exit; + } + + $html = $this->template->render('display/export/option_header', [ + 'export_type' => $exportType, + 'db' => $db, + 'table' => $table, + ]); + + if ($cfgRelation['exporttemplateswork']) { + $html .= $this->template->render('display/export/template_loading', [ + 'options' => $this->getOptionsForTemplates($exportType), + ]); + } + + $html .= '
'; + + //output Hidden Inputs + $singleTableStr = isset($GLOBALS['single_table']) ? $GLOBALS['single_table'] + : ''; + $html .= $this->getHtmlForHiddenInputs( + $exportType, + $db, + $table, + $singleTableStr, + $sqlQuery + ); + + //output Export Options + $html .= $this->getHtmlForOptions( + $exportType, + $db, + $table, + $multiValues, + $numTables, + $exportList, + $unlimNumRows + ); + + $html .= '
'; + return $html; + } + + /** + * Handles export template actions + * + * @param array $cfgRelation Relation configuration + * + * @return void + */ + public function handleTemplateActions(array $cfgRelation) + { + if (isset($_POST['templateId'])) { + $id = $GLOBALS['dbi']->escapeString($_POST['templateId']); + } else { + $id = ''; + } + + $templateTable = Util::backquote($cfgRelation['db']) . '.' + . Util::backquote($cfgRelation['export_templates']); + $user = $GLOBALS['dbi']->escapeString($GLOBALS['cfg']['Server']['user']); + + switch ($_POST['templateAction']) { + case 'create': + $query = "INSERT INTO " . $templateTable . "(" + . " `username`, `export_type`," + . " `template_name`, `template_data`" + . ") VALUES (" + . "'" . $user . "', " + . "'" . $GLOBALS['dbi']->escapeString($_POST['exportType']) + . "', '" . $GLOBALS['dbi']->escapeString($_POST['templateName']) + . "', '" . $GLOBALS['dbi']->escapeString($_POST['templateData']) + . "');"; + break; + case 'load': + $query = "SELECT `template_data` FROM " . $templateTable + . " WHERE `id` = " . $id . " AND `username` = '" . $user . "'"; + break; + case 'update': + $query = "UPDATE " . $templateTable . " SET `template_data` = " + . "'" . $GLOBALS['dbi']->escapeString($_POST['templateData']) . "'" + . " WHERE `id` = " . $id . " AND `username` = '" . $user . "'"; + break; + case 'delete': + $query = "DELETE FROM " . $templateTable + . " WHERE `id` = " . $id . " AND `username` = '" . $user . "'"; + break; + default: + $query = ''; + break; + } + + $result = $this->relation->queryAsControlUser($query, false); + + $response = Response::getInstance(); + if (! $result) { + $error = $GLOBALS['dbi']->getError(DatabaseInterface::CONNECT_CONTROL); + $response->setRequestStatus(false); + $response->addJSON('message', $error); + exit; + } + + $response->setRequestStatus(true); + if ('create' == $_POST['templateAction']) { + $response->addJSON( + 'data', + $this->getOptionsForTemplates($_POST['exportType']) + ); + } elseif ('load' == $_POST['templateAction']) { + $data = null; + while ($row = $GLOBALS['dbi']->fetchAssoc( + $result, + DatabaseInterface::CONNECT_CONTROL + )) { + $data = $row['template_data']; + } + $response->addJSON('data', $data); + } + $GLOBALS['dbi']->freeResult($result); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Display/GitRevision.php b/srcs/phpmyadmin/libraries/classes/Display/GitRevision.php new file mode 100644 index 0000000..0a20f0e --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Display/GitRevision.php @@ -0,0 +1,144 @@ +response = $response; + $this->config = $config; + $this->template = $template; + } + + /** + * Returns details about the current Git commit revision + * + * @return string HTML + */ + public function display(): string + { + // load revision data from repo + $this->config->checkGitRevision(); + + if (! $this->config->get('PMA_VERSION_GIT')) { + $this->response->setRequestStatus(false); + return ''; + } + + // if using a remote commit fast-forwarded, link to GitHub + $commitHash = substr( + $this->config->get('PMA_VERSION_GIT_COMMITHASH'), + 0, + 7 + ); + $commitHash = '' . htmlspecialchars($commitHash) . ''; + if ($this->config->get('PMA_VERSION_GIT_ISREMOTECOMMIT')) { + $commitHash = '' . $commitHash . ''; + } + + $branch = $this->config->get('PMA_VERSION_GIT_BRANCH'); + $isRemoteBranch = $this->config->get('PMA_VERSION_GIT_ISREMOTEBRANCH'); + if ($isRemoteBranch) { + $branch = '' . htmlspecialchars($branch) . ''; + } + if ($branch !== false) { + $branch = sprintf( + __('%1$s from %2$s branch'), + $commitHash, + $isRemoteBranch ? $branch : htmlspecialchars($branch) + ); + } else { + $branch = $commitHash . ' (' . __('no branch') . ')'; + } + + $committer = $this->config->get('PMA_VERSION_GIT_COMMITTER'); + $author = $this->config->get('PMA_VERSION_GIT_AUTHOR'); + + $name = __('Git revision:') . ' ' + . $branch . ',
' + . sprintf( + __('committed on %1$s by %2$s'), + Util::localisedDate(strtotime($committer['date'])), + '' + . htmlspecialchars($committer['name']) . '' + ) + . ($author != $committer + ? ',
' + . sprintf( + __('authored on %1$s by %2$s'), + Util::localisedDate(strtotime($author['date'])), + '' + . htmlspecialchars($author['name']) . '' + ) + : ''); + + return $this->template->render('list/item', [ + 'content' => $name, + 'id' => 'li_pma_version_git', + 'class' => null, + 'url' => [ + 'href' => null, + 'target' => null, + 'id' => null, + 'class' => null, + ], + 'mysql_help_page' => null, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Display/Import.php b/srcs/phpmyadmin/libraries/classes/Display/Import.php new file mode 100644 index 0000000..c9e543a --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Display/Import.php @@ -0,0 +1,127 @@ +display(); + exit; + } + + if (Core::isValid($_REQUEST['offset'], 'numeric')) { + $offset = intval($_REQUEST['offset']); + } + if (isset($_REQUEST['timeout_passed'])) { + $timeoutPassed = $_REQUEST['timeout_passed']; + } + + $localImportFile = ''; + if (isset($_REQUEST['local_import_file'])) { + $localImportFile = $_REQUEST['local_import_file']; + } + + // zip, gzip and bzip2 encode features + $compressions = []; + if ($cfg['GZipDump'] && function_exists('gzopen')) { + $compressions[] = 'gzip'; + } + if ($cfg['BZipDump'] && function_exists('bzopen')) { + $compressions[] = 'bzip2'; + } + if ($cfg['ZipDump'] && function_exists('zip_open')) { + $compressions[] = 'zip'; + } + + $allCharsets = Charsets::getCharsets($GLOBALS['dbi'], $cfg['Server']['DisableIS']); + $charsets = []; + /** @var Charset $charset */ + foreach ($allCharsets as $charset) { + $charsets[] = [ + 'name' => $charset->getName(), + 'description' => $charset->getDescription(), + ]; + } + + return $template->render('display/import/import', [ + 'upload_id' => $uploadId, + 'handler' => $_SESSION[$SESSION_KEY]["handler"], + 'id_key' => $_SESSION[$SESSION_KEY]['handler']::getIdKey(), + 'pma_theme_image' => $GLOBALS['pmaThemeImage'], + 'import_type' => $importType, + 'db' => $db, + 'table' => $table, + 'max_upload_size' => $maxUploadSize, + 'import_list' => $importList, + 'local_import_file' => $localImportFile, + 'is_upload' => $GLOBALS['is_upload'], + 'upload_dir' => isset($cfg['UploadDir']) ? $cfg['UploadDir'] : null, + 'timeout_passed_global' => isset($GLOBALS['timeout_passed']) ? $GLOBALS['timeout_passed'] : null, + 'compressions' => $compressions, + 'is_encoding_supported' => Encoding::isSupported(), + 'encodings' => Encoding::listEncodings(), + 'import_charset' => isset($cfg['Import']['charset']) ? $cfg['Import']['charset'] : null, + 'timeout_passed' => isset($timeoutPassed) ? $timeoutPassed : null, + 'offset' => isset($offset) ? $offset : null, + 'can_convert_kanji' => Encoding::canConvertKanji(), + 'charsets' => $charsets, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Display/ImportAjax.php b/srcs/phpmyadmin/libraries/classes/Display/ImportAjax.php new file mode 100644 index 0000000..97c87b9 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Display/ImportAjax.php @@ -0,0 +1,140 @@ + null, + + /** string Database name */ + 'db' => null, + + /** string Table name */ + 'table' => null, + + /** string the URL to go back in case of errors */ + 'goto' => null, + + /** string the SQL query */ + 'sql_query' => null, + + /** + * integer the total number of rows returned by the SQL query without any + * appended "LIMIT" clause programmatically + */ + 'unlim_num_rows' => null, + + /** array meta information about fields */ + 'fields_meta' => null, + + /** boolean */ + 'is_count' => null, + + /** integer */ + 'is_export' => null, + + /** boolean */ + 'is_func' => null, + + /** integer */ + 'is_analyse' => null, + + /** integer the total number of rows returned by the SQL query */ + 'num_rows' => null, + + /** integer the total number of fields returned by the SQL query */ + 'fields_cnt' => null, + + /** double time taken for execute the SQL query */ + 'querytime' => null, + + /** string path for theme images directory */ + 'pma_theme_image' => null, + + /** string */ + 'text_dir' => null, + + /** boolean */ + 'is_maint' => null, + + /** boolean */ + 'is_explain' => null, + + /** boolean */ + 'is_show' => null, + + /** boolean */ + 'is_browse_distinct' => null, + + /** array table definitions */ + 'showtable' => null, + + /** string */ + 'printview' => null, + + /** string URL query */ + 'url_query' => null, + + /** array column names to highlight */ + 'highlight_columns' => null, + + /** array holding various display information */ + 'display_params' => null, + + /** array mime types information of fields */ + 'mime_map' => null, + + /** boolean */ + 'editable' => null, + + /** random unique ID to distinguish result set */ + 'unique_id' => null, + + /** where clauses for each row, each table in the row */ + 'whereClauseMap' => [], + ]; + + /** + * This variable contains the column transformation information + * for some of the system databases. + * One element of this array represent all relevant columns in all tables in + * one specific database + */ + public $transformation_info; + + /** + * @var Relation + */ + private $relation; + + /** + * @var Transformations + */ + private $transformations; + + /** + * @var Template + */ + public $template; + + /** + * Constructor for PhpMyAdmin\Display\Results class + * + * @param string $db the database name + * @param string $table the table name + * @param int $server the server id + * @param string $goto the URL to go back in case of errors + * @param string $sql_query the SQL query + * + * @access public + */ + public function __construct($db, $table, $server, $goto, $sql_query) + { + $this->relation = new Relation($GLOBALS['dbi']); + $this->transformations = new Transformations(); + $this->template = new Template(); + + $this->_setDefaultTransformations(); + + $this->__set('db', $db); + $this->__set('table', $table); + $this->__set('server', $server); + $this->__set('goto', $goto); + $this->__set('sql_query', $sql_query); + $this->__set('unique_id', mt_rand()); + } + + /** + * Get any property of this class + * + * @param string $property name of the property + * + * @return mixed|void if property exist, value of the relevant property + */ + public function __get($property) + { + return $this->_property_array[$property] ?? null; + } + + /** + * Set values for any property of this class + * + * @param string $property name of the property + * @param mixed $value value to set + * + * @return void + */ + public function __set($property, $value) + { + if (array_key_exists($property, $this->_property_array)) { + $this->_property_array[$property] = $value; + } + } + + /** + * Sets default transformations for some columns + * + * @return void + */ + private function _setDefaultTransformations() + { + $json_highlighting_data = [ + 'libraries/classes/Plugins/Transformations/Output/Text_Plain_Json.php', + Text_Plain_Json::class, + 'Text_Plain', + ]; + $sql_highlighting_data = [ + 'libraries/classes/Plugins/Transformations/Output/Text_Plain_Sql.php', + Text_Plain_Sql::class, + 'Text_Plain', + ]; + $blob_sql_highlighting_data = [ + 'libraries/classes/Plugins/Transformations/Output/Text_Octetstream_Sql.php', + Text_Octetstream_Sql::class, + 'Text_Octetstream', + ]; + $link_data = [ + 'libraries/classes/Plugins/Transformations/Text_Plain_Link.php', + Text_Plain_Link::class, + 'Text_Plain', + ]; + $this->transformation_info = [ + 'information_schema' => [ + 'events' => [ + 'event_definition' => $sql_highlighting_data, + ], + 'processlist' => [ + 'info' => $sql_highlighting_data, + ], + 'routines' => [ + 'routine_definition' => $sql_highlighting_data, + ], + 'triggers' => [ + 'action_statement' => $sql_highlighting_data, + ], + 'views' => [ + 'view_definition' => $sql_highlighting_data, + ], + ], + 'mysql' => [ + 'event' => [ + 'body' => $blob_sql_highlighting_data, + 'body_utf8' => $blob_sql_highlighting_data, + ], + 'general_log' => [ + 'argument' => $sql_highlighting_data, + ], + 'help_category' => [ + 'url' => $link_data, + ], + 'help_topic' => [ + 'example' => $sql_highlighting_data, + 'url' => $link_data, + ], + 'proc' => [ + 'param_list' => $blob_sql_highlighting_data, + 'returns' => $blob_sql_highlighting_data, + 'body' => $blob_sql_highlighting_data, + 'body_utf8' => $blob_sql_highlighting_data, + ], + 'slow_log' => [ + 'sql_text' => $sql_highlighting_data, + ], + ], + ]; + + $cfgRelation = $this->relation->getRelationsParam(); + if ($cfgRelation['db']) { + $this->transformation_info[$cfgRelation['db']] = []; + $relDb = &$this->transformation_info[$cfgRelation['db']]; + if (! empty($cfgRelation['history'])) { + $relDb[$cfgRelation['history']] = [ + 'sqlquery' => $sql_highlighting_data, + ]; + } + if (! empty($cfgRelation['bookmark'])) { + $relDb[$cfgRelation['bookmark']] = [ + 'query' => $sql_highlighting_data, + ]; + } + if (! empty($cfgRelation['tracking'])) { + $relDb[$cfgRelation['tracking']] = [ + 'schema_sql' => $sql_highlighting_data, + 'data_sql' => $sql_highlighting_data, + ]; + } + if (! empty($cfgRelation['favorite'])) { + $relDb[$cfgRelation['favorite']] = [ + 'tables' => $json_highlighting_data, + ]; + } + if (! empty($cfgRelation['recent'])) { + $relDb[$cfgRelation['recent']] = [ + 'tables' => $json_highlighting_data, + ]; + } + if (! empty($cfgRelation['savedsearches'])) { + $relDb[$cfgRelation['savedsearches']] = [ + 'search_data' => $json_highlighting_data, + ]; + } + if (! empty($cfgRelation['designer_settings'])) { + $relDb[$cfgRelation['designer_settings']] = [ + 'settings_data' => $json_highlighting_data, + ]; + } + if (! empty($cfgRelation['table_uiprefs'])) { + $relDb[$cfgRelation['table_uiprefs']] = [ + 'prefs' => $json_highlighting_data, + ]; + } + if (! empty($cfgRelation['userconfig'])) { + $relDb[$cfgRelation['userconfig']] = [ + 'config_data' => $json_highlighting_data, + ]; + } + if (! empty($cfgRelation['export_templates'])) { + $relDb[$cfgRelation['export_templates']] = [ + 'template_data' => $json_highlighting_data, + ]; + } + } + } + + /** + * Set properties which were not initialized at the constructor + * + * @param integer $unlim_num_rows the total number of rows returned by + * the SQL query without any appended + * "LIMIT" clause programmatically + * @param stdClass $fields_meta meta information about fields + * @param boolean $is_count statement is SELECT COUNT + * @param integer $is_export statement contains INTO OUTFILE + * @param boolean $is_func statement contains a function like SUM() + * @param integer $is_analyse statement contains PROCEDURE ANALYSE + * @param integer $num_rows total no. of rows returned by SQL query + * @param integer $fields_cnt total no.of fields returned by SQL query + * @param double $querytime time taken for execute the SQL query + * @param string $pmaThemeImage path for theme images directory + * @param string $text_dir text direction + * @param boolean $is_maint statement contains a maintenance command + * @param boolean $is_explain statement contains EXPLAIN + * @param boolean $is_show statement contains SHOW + * @param array $showtable table definitions + * @param string $printview print view was requested + * @param string $url_query URL query + * @param boolean $editable whether the results set is editable + * @param boolean $is_browse_dist whether browsing distinct values + * + * @return void + * + * @see sql.php + */ + public function setProperties( + $unlim_num_rows, + $fields_meta, + $is_count, + $is_export, + $is_func, + $is_analyse, + $num_rows, + $fields_cnt, + $querytime, + $pmaThemeImage, + $text_dir, + $is_maint, + $is_explain, + $is_show, + $showtable, + $printview, + $url_query, + $editable, + $is_browse_dist + ) { + + $this->__set('unlim_num_rows', $unlim_num_rows); + $this->__set('fields_meta', $fields_meta); + $this->__set('is_count', $is_count); + $this->__set('is_export', $is_export); + $this->__set('is_func', $is_func); + $this->__set('is_analyse', $is_analyse); + $this->__set('num_rows', $num_rows); + $this->__set('fields_cnt', $fields_cnt); + $this->__set('querytime', $querytime); + $this->__set('pma_theme_image', $pmaThemeImage); + $this->__set('text_dir', $text_dir); + $this->__set('is_maint', $is_maint); + $this->__set('is_explain', $is_explain); + $this->__set('is_show', $is_show); + $this->__set('showtable', $showtable); + $this->__set('printview', $printview); + $this->__set('url_query', $url_query); + $this->__set('editable', $editable); + $this->__set('is_browse_distinct', $is_browse_dist); + } + + /** + * Defines the parts to display for a print view + * + * @param array $displayParts the parts to display + * + * @return array the modified display parts + * + * @access private + * + */ + private function _setDisplayPartsForPrintView(array $displayParts) + { + // set all elements to false! + $displayParts['edit_lnk'] = self::NO_EDIT_OR_DELETE; // no edit link + $displayParts['del_lnk'] = self::NO_EDIT_OR_DELETE; // no delete link + $displayParts['sort_lnk'] = (string) '0'; + $displayParts['nav_bar'] = (string) '0'; + $displayParts['bkm_form'] = (string) '0'; + $displayParts['text_btn'] = (string) '0'; + $displayParts['pview_lnk'] = (string) '0'; + + return $displayParts; + } + + /** + * Defines the parts to display for a SHOW statement + * + * @param array $displayParts the parts to display + * + * @return array the modified display parts + * + * @access private + * + */ + private function _setDisplayPartsForShow(array $displayParts) + { + preg_match( + '@^SHOW[[:space:]]+(VARIABLES|(FULL[[:space:]]+)?' + . 'PROCESSLIST|STATUS|TABLE|GRANTS|CREATE|LOGS|DATABASES|FIELDS' + . ')@i', + $this->__get('sql_query'), + $which + ); + + $bIsProcessList = isset($which[1]); + if ($bIsProcessList) { + $str = ' ' . strtoupper($which[1]); + $bIsProcessList = $bIsProcessList + && strpos($str, 'PROCESSLIST') > 0; + } + + if ($bIsProcessList) { + // no edit link + $displayParts['edit_lnk'] = self::NO_EDIT_OR_DELETE; + // "kill process" type edit link + $displayParts['del_lnk'] = self::KILL_PROCESS; + } else { + // Default case -> no links + // no edit link + $displayParts['edit_lnk'] = self::NO_EDIT_OR_DELETE; + // no delete link + $displayParts['del_lnk'] = self::NO_EDIT_OR_DELETE; + } + // Other settings + $displayParts['sort_lnk'] = (string) '0'; + $displayParts['nav_bar'] = (string) '0'; + $displayParts['bkm_form'] = (string) '1'; + $displayParts['text_btn'] = (string) '1'; + $displayParts['pview_lnk'] = (string) '1'; + + return $displayParts; + } + + /** + * Defines the parts to display for statements not related to data + * + * @param array $displayParts the parts to display + * + * @return array the modified display parts + * + * @access private + * + */ + private function _setDisplayPartsForNonData(array $displayParts) + { + // Statement is a "SELECT COUNT", a + // "CHECK/ANALYZE/REPAIR/OPTIMIZE/CHECKSUM", an "EXPLAIN" one or + // contains a "PROC ANALYSE" part + $displayParts['edit_lnk'] = self::NO_EDIT_OR_DELETE; // no edit link + $displayParts['del_lnk'] = self::NO_EDIT_OR_DELETE; // no delete link + $displayParts['sort_lnk'] = (string) '0'; + $displayParts['nav_bar'] = (string) '0'; + $displayParts['bkm_form'] = (string) '1'; + + if ($this->__get('is_maint')) { + $displayParts['text_btn'] = (string) '1'; + } else { + $displayParts['text_btn'] = (string) '0'; + } + $displayParts['pview_lnk'] = (string) '1'; + + return $displayParts; + } + + /** + * Defines the parts to display for other statements (probably SELECT) + * + * @param array $displayParts the parts to display + * + * @return array the modified display parts + * + * @access private + * + */ + private function _setDisplayPartsForSelect(array $displayParts) + { + // Other statements (ie "SELECT" ones) -> updates + // $displayParts['edit_lnk'], $displayParts['del_lnk'] and + // $displayParts['text_btn'] (keeps other default values) + + $fields_meta = $this->__get('fields_meta'); + $prev_table = ''; + $displayParts['text_btn'] = (string) '1'; + $number_of_columns = $this->__get('fields_cnt'); + + for ($i = 0; $i < $number_of_columns; $i++) { + $is_link = ($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE) + || ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE) + || ($displayParts['sort_lnk'] != '0'); + + // Displays edit/delete/sort/insert links? + if ($is_link + && $prev_table != '' + && $fields_meta[$i]->table != '' + && $fields_meta[$i]->table != $prev_table + ) { + // don't display links + $displayParts['edit_lnk'] = self::NO_EDIT_OR_DELETE; + $displayParts['del_lnk'] = self::NO_EDIT_OR_DELETE; + /** + * @todo May be problematic with same field names + * in two joined table. + */ + // $displayParts['sort_lnk'] = (string) '0'; + if ($displayParts['text_btn'] == '1') { + break; + } + } // end if + + // Always display print view link + $displayParts['pview_lnk'] = (string) '1'; + if ($fields_meta[$i]->table != '') { + $prev_table = $fields_meta[$i]->table; + } + } // end for + + if ($prev_table == '') { // no table for any of the columns + // don't display links + $displayParts['edit_lnk'] = self::NO_EDIT_OR_DELETE; + $displayParts['del_lnk'] = self::NO_EDIT_OR_DELETE; + } + + return $displayParts; + } + + /** + * Defines the parts to display for the results of a SQL query + * and the total number of rows + * + * @param array $displayParts the parts to display (see a few + * lines above for explanations) + * + * @return array the first element is an array with explicit indexes + * for all the display elements + * the second element is the total number of rows returned + * by the SQL query without any programmatically appended + * LIMIT clause (just a copy of $unlim_num_rows if it exists, + * else computed inside this function) + * + * + * @access private + * + * @see getTable() + */ + private function _setDisplayPartsAndTotal(array $displayParts) + { + $the_total = 0; + + // 1. Following variables are needed for use in isset/empty or + // use with array indexes or safe use in foreach + $db = $this->__get('db'); + $table = $this->__get('table'); + $unlim_num_rows = $this->__get('unlim_num_rows'); + $num_rows = $this->__get('num_rows'); + $printview = $this->__get('printview'); + + // 2. Updates the display parts + if ($printview == '1') { + $displayParts = $this->_setDisplayPartsForPrintView($displayParts); + } elseif ($this->__get('is_count') || $this->__get('is_analyse') + || $this->__get('is_maint') || $this->__get('is_explain') + ) { + $displayParts = $this->_setDisplayPartsForNonData($displayParts); + } elseif ($this->__get('is_show')) { + $displayParts = $this->_setDisplayPartsForShow($displayParts); + } else { + $displayParts = $this->_setDisplayPartsForSelect($displayParts); + } // end if..elseif...else + + // 3. Gets the total number of rows if it is unknown + if (isset($unlim_num_rows) && $unlim_num_rows != '') { + $the_total = $unlim_num_rows; + } elseif (($displayParts['nav_bar'] == '1') + || ($displayParts['sort_lnk'] == '1') + && (strlen($db) > 0 && strlen($table) > 0) + ) { + $the_total = $GLOBALS['dbi']->getTable($db, $table)->countRecords(); + } + + // if for COUNT query, number of rows returned more than 1 + // (may be being used GROUP BY) + if ($this->__get('is_count') && isset($num_rows) && $num_rows > 1) { + $displayParts['nav_bar'] = (string) '1'; + $displayParts['sort_lnk'] = (string) '1'; + } + // 4. If navigation bar or sorting fields names URLs should be + // displayed but there is only one row, change these settings to + // false + if ($displayParts['nav_bar'] == '1' || $displayParts['sort_lnk'] == '1') { + // - Do not display sort links if less than 2 rows. + // - For a VIEW we (probably) did not count the number of rows + // so don't test this number here, it would remove the possibility + // of sorting VIEW results. + $_table = new Table($table, $db); + if (isset($unlim_num_rows) + && ($unlim_num_rows < 2) + && ! $_table->isView() + ) { + $displayParts['sort_lnk'] = (string) '0'; + } + } // end if (3) + + return [ + $displayParts, + $the_total, + ]; + } + + /** + * Return true if we are executing a query in the form of + * "SELECT * FROM ..." + * + * @param array $analyzed_sql_results analyzed sql results + * + * @return boolean + * + * @access private + * + * @see _getTableHeaders(), _getColumnParams() + */ + private function _isSelect(array $analyzed_sql_results) + { + return ! ($this->__get('is_count') + || $this->__get('is_export') + || $this->__get('is_func') + || $this->__get('is_analyse')) + && ! empty($analyzed_sql_results['select_from']) + && ! empty($analyzed_sql_results['statement']->from) + && (count($analyzed_sql_results['statement']->from) === 1) + && ! empty($analyzed_sql_results['statement']->from[0]->table); + } + + /** + * Get a navigation button + * + * @param string $caption iconic caption for button + * @param string $title text for button + * @param integer $pos position for next query + * @param string $html_sql_query query ready for display + * @param boolean $back whether 'begin' or 'previous' + * @param string $onsubmit optional onsubmit clause + * @param string $input_for_real_end optional hidden field for special treatment + * @param string $onclick optional onclick clause + * + * @return string html content + * + * @access private + * + * @see _getMoveBackwardButtonsForTableNavigation(), + * _getMoveForwardButtonsForTableNavigation() + */ + private function _getTableNavigationButton( + $caption, + $title, + $pos, + $html_sql_query, + $back, + $onsubmit = '', + $input_for_real_end = '', + $onclick = '' + ) { + $caption_output = ''; + if ($back) { + if (Util::showIcons('TableNavigationLinksMode')) { + $caption_output .= $caption; + } + if (Util::showText('TableNavigationLinksMode')) { + $caption_output .= ' ' . $title; + } + } else { + if (Util::showText('TableNavigationLinksMode')) { + $caption_output .= $title; + } + if (Util::showIcons('TableNavigationLinksMode')) { + $caption_output .= ' ' . $caption; + } + } + + return $this->template->render('display/results/table_navigation_button', [ + 'db' => $this->__get('db'), + 'table' => $this->__get('table'), + 'sql_query' => $html_sql_query, + 'pos' => $pos, + 'is_browse_distinct' => $this->__get('is_browse_distinct'), + 'goto' => $this->__get('goto'), + 'input_for_real_end' => $input_for_real_end, + 'caption_output' => $caption_output, + 'title' => $title, + 'onsubmit' => $onsubmit, + 'onclick' => $onclick, + ]); + } + + /** + * Possibly return a page selector for table navigation + * + * @return array ($output, $nbTotalPage) + * + * @access private + */ + private function _getHtmlPageSelector(): array + { + $pageNow = @floor( + $_SESSION['tmpval']['pos'] + / $_SESSION['tmpval']['max_rows'] + ) + 1; + + $nbTotalPage = @ceil( + $this->__get('unlim_num_rows') + / $_SESSION['tmpval']['max_rows'] + ); + + $output = ''; + if ($nbTotalPage > 1) { + $_url_params = [ + 'db' => $this->__get('db'), + 'table' => $this->__get('table'), + 'sql_query' => $this->__get('sql_query'), + 'goto' => $this->__get('goto'), + 'is_browse_distinct' => $this->__get('is_browse_distinct'), + ]; + + $output = $this->template->render('display/results/page_selector', [ + 'url_params' => $_url_params, + 'page_selector' => Util::pageselector( + 'pos', + $_SESSION['tmpval']['max_rows'], + $pageNow, + $nbTotalPage, + 200, + 5, + 5, + 20, + 10 + ), + ]); + } + return [ + $output, + $nbTotalPage, + ]; + } + + /** + * Get a navigation bar to browse among the results of a SQL query + * + * @param integer $posNext the offset for the "next" page + * @param integer $posPrevious the offset for the "previous" page + * @param boolean $isInnodb whether its InnoDB or not + * @param string $sortByKeyHtml the sort by key dialog + * + * @return string html content + * + * @access private + * + * @see getTable() + */ + private function _getTableNavigation( + $posNext, + $posPrevious, + $isInnodb, + $sortByKeyHtml + ): string { + $isShowingAll = $_SESSION['tmpval']['max_rows'] === self::ALL_ROWS; + + // Move to the beginning or to the previous page + $moveBackwardButtons = ''; + if ($_SESSION['tmpval']['pos'] && ! $isShowingAll) { + $moveBackwardButtons = $this->_getMoveBackwardButtonsForTableNavigation( + htmlspecialchars($this->__get('sql_query')), + $posPrevious + ); + } + + $pageSelector = ''; + $numberTotalPage = 1; + if (! $isShowingAll) { + list( + $pageSelector, + $numberTotalPage + ) = $this->_getHtmlPageSelector(); + } + + // Move to the next page or to the last one + $moveForwardButtons = ''; + if ($this->__get('unlim_num_rows') === false // view with unknown number of rows + || (! $isShowingAll + && $_SESSION['tmpval']['pos'] + $_SESSION['tmpval']['max_rows'] < $this->__get('unlim_num_rows') + && $this->__get('num_rows') >= $_SESSION['tmpval']['max_rows']) + ) { + $moveForwardButtons = $this->_getMoveForwardButtonsForTableNavigation( + htmlspecialchars($this->__get('sql_query')), + $posNext, + $isInnodb + ); + } + + $hiddenFields = [ + 'db' => $this->__get('db'), + 'table' => $this->__get('table'), + 'server' => $this->__get('server'), + 'sql_query' => $this->__get('sql_query'), + 'is_browse_distinct' => $this->__get('is_browse_distinct'), + 'goto' => $this->__get('goto'), + ]; + + return $this->template->render('display/results/table_navigation', [ + 'move_backward_buttons' => $moveBackwardButtons, + 'page_selector' => $pageSelector, + 'move_forward_buttons' => $moveForwardButtons, + 'number_total_page' => $numberTotalPage, + 'has_show_all' => $GLOBALS['cfg']['ShowAll'] || ($this->__get('unlim_num_rows') <= 500), + 'hidden_fields' => $hiddenFields, + 'session_max_rows' => $isShowingAll ? $GLOBALS['cfg']['MaxRows'] : 'all', + 'unique_id' => $this->__get('unique_id'), + 'is_showing_all' => $isShowingAll, + 'unlim_num_rows' => $this->__get('unlim_num_rows'), + 'max_rows' => $_SESSION['tmpval']['max_rows'], + 'pos' => $_SESSION['tmpval']['pos'], + 'sort_by_key' => $sortByKeyHtml, + ]); + } + + /** + * Prepare move backward buttons - previous and first + * + * @param string $html_sql_query the sql encoded by html special characters + * @param integer $pos_prev the offset for the "previous" page + * + * @return string html content + * + * @access private + * + * @see _getTableNavigation() + */ + private function _getMoveBackwardButtonsForTableNavigation( + $html_sql_query, + $pos_prev + ) { + return $this->_getTableNavigationButton( + '<<', + _pgettext('First page', 'Begin'), + 0, + $html_sql_query, + true + ) + . $this->_getTableNavigationButton( + '<', + _pgettext('Previous page', 'Previous'), + $pos_prev, + $html_sql_query, + true + ); + } + + /** + * Prepare move forward buttons - next and last + * + * @param string $html_sql_query the sql encoded by htmlspecialchars() + * @param integer $pos_next the offset for the "next" page + * @param boolean $is_innodb whether it's InnoDB or not + * + * @return string html content + * + * @access private + * + * @see _getTableNavigation() + */ + private function _getMoveForwardButtonsForTableNavigation( + $html_sql_query, + $pos_next, + $is_innodb + ) { + // display the Next button + $buttons_html = $this->_getTableNavigationButton( + '>', + _pgettext('Next page', 'Next'), + $pos_next, + $html_sql_query, + false + ); + + // prepare some options for the End button + if ($is_innodb + && $this->__get('unlim_num_rows') > $GLOBALS['cfg']['MaxExactCount'] + ) { + $input_for_real_end = ''; + // no backquote around this message + $onclick = ''; + } else { + $input_for_real_end = $onclick = ''; + } + + $maxRows = $_SESSION['tmpval']['max_rows']; + $onsubmit = 'onsubmit="return ' + . ($_SESSION['tmpval']['pos'] + + $maxRows + < $this->__get('unlim_num_rows') + && $this->__get('num_rows') >= $maxRows + ? 'true' + : 'false') . '"'; + + // display the End button + $buttons_html .= $this->_getTableNavigationButton( + '>>', + _pgettext('Last page', 'End'), + @((ceil( + $this->__get('unlim_num_rows') + / $_SESSION['tmpval']['max_rows'] + ) - 1) * $maxRows), + $html_sql_query, + false, + $onsubmit, + $input_for_real_end, + $onclick + ); + + return $buttons_html; + } + + /** + * Get the headers of the results table, for all of the columns + * + * @param array $displayParts which elements to display + * @param array $analyzed_sql_results analyzed sql results + * @param array $sort_expression sort expression + * @param array $sort_expression_nodirection sort expression + * without direction + * @param array $sort_direction sort direction + * @param boolean $is_limited_display with limited operations + * or not + * @param string $unsorted_sql_query query without the sort part + * + * @return string html content + * + * @access private + * + * @see getTableHeaders() + */ + private function _getTableHeadersForColumns( + array $displayParts, + array $analyzed_sql_results, + array $sort_expression, + array $sort_expression_nodirection, + array $sort_direction, + $is_limited_display, + $unsorted_sql_query + ) { + $html = ''; + + // required to generate sort links that will remember whether the + // "Show all" button has been clicked + $sql_md5 = md5($this->__get('sql_query')); + $session_max_rows = $is_limited_display + ? 0 + : $_SESSION['tmpval']['query'][$sql_md5]['max_rows']; + + // Following variable are needed for use in isset/empty or + // use with array indexes/safe use in the for loop + $highlight_columns = $this->__get('highlight_columns'); + $fields_meta = $this->__get('fields_meta'); + + // Prepare Display column comments if enabled + // ($GLOBALS['cfg']['ShowBrowseComments']). + $comments_map = $this->_getTableCommentsArray($analyzed_sql_results); + + list($col_order, $col_visib) = $this->_getColumnParams( + $analyzed_sql_results + ); + + // optimize: avoid calling a method on each iteration + $number_of_columns = $this->__get('fields_cnt'); + + for ($j = 0; $j < $number_of_columns; $j++) { + // PHP 7.4 fix for accessing array offset on bool + $col_visib_current = is_array($col_visib) && isset($col_visib[$j]) ? $col_visib[$j] : null; + + // assign $i with the appropriate column order + $i = $col_order ? $col_order[$j] : $j; + + // See if this column should get highlight because it's used in the + // where-query. + $name = $fields_meta[$i]->name; + $condition_field = isset($highlight_columns[$name]) + || isset($highlight_columns[Util::backquote($name)]) + ? true + : false; + + // Prepare comment-HTML-wrappers for each row, if defined/enabled. + $comments = $this->_getCommentForRow($comments_map, $fields_meta[$i]); + $display_params = $this->__get('display_params'); + + if (($displayParts['sort_lnk'] == '1') && ! $is_limited_display) { + list($order_link, $sorted_header_html) + = $this->_getOrderLinkAndSortedHeaderHtml( + $fields_meta[$i], + $sort_expression, + $sort_expression_nodirection, + $i, + $unsorted_sql_query, + $session_max_rows, + $comments, + $sort_direction, + $col_visib, + $col_visib_current + ); + + $html .= $sorted_header_html; + + $display_params['desc'][] = ' ' . "\n" . $order_link . $comments . ' ' . "\n"; + } else { + // Results can't be sorted + $html + .= $this->_getDraggableClassForNonSortableColumns( + $col_visib, + $col_visib_current, + $condition_field, + $fields_meta[$i], + $comments + ); + + $display_params['desc'][] = ' ' + . htmlspecialchars((string) $fields_meta[$i]->name) + . $comments . ' '; + } // end else + + $this->__set('display_params', $display_params); + } // end for + return $html; + } + + /** + * Get the headers of the results table + * + * @param array $displayParts which elements to display + * @param array $analyzedSqlResults analyzed sql results + * @param string $unsortedSqlQuery the unsorted sql query + * @param array $sortExpression sort expression + * @param array|string $sortExpressionNoDirection sort expression without direction + * @param array $sortDirection sort direction + * @param boolean $isLimitedDisplay with limited operations or not + * + * @return string html content + * + * @access private + * + * @see getTable() + */ + private function _getTableHeaders( + array &$displayParts, + array $analyzedSqlResults, + $unsortedSqlQuery, + array $sortExpression = [], + $sortExpressionNoDirection = '', + array $sortDirection = [], + $isLimitedDisplay = false + ): string { + // Needed for use in isset/empty or + // use with array indexes/safe use in foreach + $printView = $this->__get('printview'); + $displayParams = $this->__get('display_params'); + + // Output data needed for column reordering and show/hide column + $dataForResettingColumnOrder = $this->_getDataForResettingColumnOrder($analyzedSqlResults); + + $displayParams['emptypre'] = 0; + $displayParams['emptyafter'] = 0; + $displayParams['textbtn'] = ''; + $fullOrPartialTextLink = ''; + + $this->__set('display_params', $displayParams); + + // Display options (if we are not in print view) + $optionsBlock = ''; + if (! (isset($printView) && ($printView == '1')) && ! $isLimitedDisplay) { + $optionsBlock = $this->_getOptionsBlock(); + + // prepare full/partial text button or link + $fullOrPartialTextLink = $this->_getFullOrPartialTextButtonOrLink(); + } + + // 1. Set $colspan and generate html with full/partial + // text button or link + list($colspan, $buttonHtml) = $this->_getFieldVisibilityParams( + $displayParts, + $fullOrPartialTextLink + ); + + // 2. Displays the fields' name + // 2.0 If sorting links should be used, checks if the query is a "JOIN" + // statement (see 2.1.3) + + // See if we have to highlight any header fields of a WHERE query. + // Uses SQL-Parser results. + $this->_setHighlightedColumnGlobalField($analyzedSqlResults); + + // Get the headers for all of the columns + $tableHeadersForColumns = $this->_getTableHeadersForColumns( + $displayParts, + $analyzedSqlResults, + $sortExpression, + $sortExpressionNoDirection, + $sortDirection, + $isLimitedDisplay, + $unsortedSqlQuery + ); + + // Display column at rightside - checkboxes or empty column + $columnAtRightSide = ''; + if (! $printView) { + $columnAtRightSide = $this->_getColumnAtRightSide( + $displayParts, + $fullOrPartialTextLink, + $colspan + ); + } + + return $this->template->render('display/results/table_headers', [ + 'db' => $this->__get('db'), + 'table' => $this->__get('table'), + 'unique_id' => $this->__get('unique_id'), + 'save_cells_at_once' => $GLOBALS['cfg']['SaveCellsAtOnce'], + 'data_for_resetting_column_order' => $dataForResettingColumnOrder, + 'options_block' => $optionsBlock, + 'delete_link' => $displayParts['del_lnk'], + 'delete_row' => self::DELETE_ROW, + 'kill_process' => self::KILL_PROCESS, + 'button' => $buttonHtml, + 'table_headers_for_columns' => $tableHeadersForColumns, + 'column_at_right_side' => $columnAtRightSide, + ]); + } + + /** + * Prepare unsorted sql query and sort by key drop down + * + * @param array $analyzed_sql_results analyzed sql results + * @param array|null $sort_expression sort expression + * + * @return array two element array - $unsorted_sql_query, $drop_down_html + * + * @access private + * + * @see _getTableHeaders() + */ + private function _getUnsortedSqlAndSortByKeyDropDown( + array $analyzed_sql_results, + ?array $sort_expression + ) { + $drop_down_html = ''; + + $unsorted_sql_query = Query::replaceClause( + $analyzed_sql_results['statement'], + $analyzed_sql_results['parser']->list, + 'ORDER BY', + '' + ); + + // Data is sorted by indexes only if it there is only one table. + if ($this->_isSelect($analyzed_sql_results)) { + // grab indexes data: + $indexes = Index::getFromTable( + $this->__get('table'), + $this->__get('db') + ); + + // do we have any index? + if (! empty($indexes)) { + $drop_down_html = $this->_getSortByKeyDropDown( + $indexes, + $sort_expression, + $unsorted_sql_query + ); + } + } + + return [ + $unsorted_sql_query, + $drop_down_html, + ]; + } + + /** + * Prepare sort by key dropdown - html code segment + * + * @param Index[] $indexes the indexes of the table for sort criteria + * @param array|null $sortExpression the sort expression + * @param string $unsortedSqlQuery the unsorted sql query + * + * @return string html content + * + * @access private + * + * @see _getTableHeaders() + */ + private function _getSortByKeyDropDown( + $indexes, + ?array $sortExpression, + $unsortedSqlQuery + ): string { + $hiddenFields = [ + 'db' => $this->__get('db'), + 'table' => $this->__get('table'), + 'server' => $this->__get('server'), + 'sort_by_key' => '1', + ]; + + $isIndexUsed = false; + $localOrder = is_array($sortExpression) ? implode(', ', $sortExpression) : ''; + + $options = []; + foreach ($indexes as $index) { + $ascSort = '`' + . implode('` ASC, `', array_keys($index->getColumns())) + . '` ASC'; + + $descSort = '`' + . implode('` DESC, `', array_keys($index->getColumns())) + . '` DESC'; + + $isIndexUsed = $isIndexUsed + || $localOrder === $ascSort + || $localOrder === $descSort; + + $unsortedSqlQueryFirstPart = $unsortedSqlQuery; + $unsortedSqlQuerySecondPart = ''; + if (preg_match( + '@(.*)([[:space:]](LIMIT (.*)|PROCEDURE (.*)|' + . 'FOR UPDATE|LOCK IN SHARE MODE))@is', + $unsortedSqlQuery, + $myReg + )) { + $unsortedSqlQueryFirstPart = $myReg[1]; + $unsortedSqlQuerySecondPart = $myReg[2]; + } + + $options[] = [ + 'value' => $unsortedSqlQueryFirstPart . ' ORDER BY ' + . $ascSort . $unsortedSqlQuerySecondPart, + 'content' => $index->getName() . ' (ASC)', + 'is_selected' => $localOrder === $ascSort, + ]; + $options[] = [ + 'value' => $unsortedSqlQueryFirstPart . ' ORDER BY ' + . $descSort . $unsortedSqlQuerySecondPart, + 'content' => $index->getName() . ' (DESC)', + 'is_selected' => $localOrder === $descSort, + ]; + } + $options[] = [ + 'value' => $unsortedSqlQuery, + 'content' => __('None'), + 'is_selected' => ! $isIndexUsed, + ]; + + return $this->template->render('display/results/sort_by_key', [ + 'hidden_fields' => $hiddenFields, + 'options' => $options, + ]); + } + + /** + * Set column span, row span and prepare html with full/partial + * text button or link + * + * @param array $displayParts which elements to display + * @param string $full_or_partial_text_link full/partial link or text button + * + * @return array 2 element array - $colspan, $button_html + * + * @access private + * + * @see _getTableHeaders() + */ + private function _getFieldVisibilityParams( + array &$displayParts, + $full_or_partial_text_link + ) { + + $button_html = ''; + $display_params = $this->__get('display_params'); + + // 1. Displays the full/partial text button (part 1)... + $button_html .= '' . "\n"; + + $emptyPreCondition = $displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE + && $displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE; + + $colspan = $emptyPreCondition ? ' colspan="4"' + : ''; + + $leftOrBoth = $GLOBALS['cfg']['RowActionLinks'] === self::POSITION_LEFT + || $GLOBALS['cfg']['RowActionLinks'] === self::POSITION_BOTH; + + // ... before the result table + if (($displayParts['edit_lnk'] == self::NO_EDIT_OR_DELETE) + && ($displayParts['del_lnk'] == self::NO_EDIT_OR_DELETE) + && ($displayParts['text_btn'] == '1') + ) { + $display_params['emptypre'] = $emptyPreCondition ? 4 : 0; + } elseif ($leftOrBoth && ($displayParts['text_btn'] == '1') + ) { + // ... at the left column of the result table header if possible + // and required + + $display_params['emptypre'] = $emptyPreCondition ? 4 : 0; + + $button_html .= '' . $full_or_partial_text_link . ''; + } elseif ($leftOrBoth + && (($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE) + || ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE)) + ) { + // ... elseif no button, displays empty(ies) col(s) if required + + $display_params['emptypre'] = $emptyPreCondition ? 4 : 0; + + $button_html .= ''; + } elseif ($GLOBALS['cfg']['RowActionLinks'] == self::POSITION_NONE) { + // ... elseif display an empty column if the actions links are + // disabled to match the rest of the table + $button_html .= ''; + } + + $this->__set('display_params', $display_params); + + return [ + $colspan, + $button_html, + ]; + } + + /** + * Get table comments as array + * + * @param array $analyzed_sql_results analyzed sql results + * + * @return array table comments + * + * @access private + * + * @see _getTableHeaders() + */ + private function _getTableCommentsArray(array $analyzed_sql_results) + { + if (! $GLOBALS['cfg']['ShowBrowseComments'] + || empty($analyzed_sql_results['statement']->from) + ) { + return []; + } + + $ret = []; + foreach ($analyzed_sql_results['statement']->from as $field) { + if (empty($field->table)) { + continue; + } + $ret[$field->table] = $this->relation->getComments( + empty($field->database) ? $this->__get('db') : $field->database, + $field->table + ); + } + + return $ret; + } + + /** + * Set global array for store highlighted header fields + * + * @param array $analyzed_sql_results analyzed sql results + * + * @return void + * + * @access private + * + * @see _getTableHeaders() + */ + private function _setHighlightedColumnGlobalField(array $analyzed_sql_results) + { + $highlight_columns = []; + + if (! empty($analyzed_sql_results['statement']->where)) { + foreach ($analyzed_sql_results['statement']->where as $expr) { + foreach ($expr->identifiers as $identifier) { + $highlight_columns[$identifier] = 'true'; + } + } + } + + $this->__set('highlight_columns', $highlight_columns); + } + + /** + * Prepare data for column restoring and show/hide + * + * @param array $analyzedSqlResults analyzed sql results + * + * @return string html content + * + * @access private + * + * @see _getTableHeaders() + */ + private function _getDataForResettingColumnOrder(array $analyzedSqlResults): string + { + if (! $this->_isSelect($analyzedSqlResults)) { + return ''; + } + + list($columnOrder, $columnVisibility) = $this->_getColumnParams( + $analyzedSqlResults + ); + + $tableCreateTime = ''; + $table = new Table($this->__get('table'), $this->__get('db')); + if (! $table->isView()) { + $tableCreateTime = $GLOBALS['dbi']->getTable( + $this->__get('db'), + $this->__get('table') + )->getStatusInfo('Create_time'); + } + + return $this->template->render('display/results/data_for_resetting_column_order', [ + 'column_order' => $columnOrder, + 'column_visibility' => $columnVisibility, + 'is_view' => $table->isView(), + 'table_create_time' => $tableCreateTime, + ]); + } + + /** + * Prepare option fields block + * + * @return string html content + * + * @access private + * + * @see _getTableHeaders() + */ + private function _getOptionsBlock() + { + if (isset($_SESSION['tmpval']['possible_as_geometry']) && $_SESSION['tmpval']['possible_as_geometry'] == false) { + if ($_SESSION['tmpval']['geoOption'] == self::GEOMETRY_DISP_GEOM) { + $_SESSION['tmpval']['geoOption'] = self::GEOMETRY_DISP_WKT; + } + } + return $this->template->render('display/results/options_block', [ + 'unique_id' => $this->__get('unique_id'), + 'geo_option' => $_SESSION['tmpval']['geoOption'], + 'hide_transformation' => $_SESSION['tmpval']['hide_transformation'], + 'display_blob' => $_SESSION['tmpval']['display_blob'], + 'display_binary' => $_SESSION['tmpval']['display_binary'], + 'relational_display' => $_SESSION['tmpval']['relational_display'], + 'displaywork' => $GLOBALS['cfgRelation']['displaywork'], + 'relwork' => $GLOBALS['cfgRelation']['relwork'], + 'possible_as_geometry' => $_SESSION['tmpval']['possible_as_geometry'], + 'pftext' => $_SESSION['tmpval']['pftext'], + 'db' => $this->__get('db'), + 'table' => $this->__get('table'), + 'sql_query' => $this->__get('sql_query'), + 'goto' => $this->__get('goto'), + 'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'], + ]); + } + + /** + * Get full/partial text button or link + * + * @return string html content + * + * @access private + * + * @see _getTableHeaders() + */ + private function _getFullOrPartialTextButtonOrLink() + { + + $url_params_full_text = [ + 'db' => $this->__get('db'), + 'table' => $this->__get('table'), + 'sql_query' => $this->__get('sql_query'), + 'goto' => $this->__get('goto'), + 'full_text_button' => 1, + ]; + + if ($_SESSION['tmpval']['pftext'] == self::DISPLAY_FULL_TEXT) { + // currently in fulltext mode so show the opposite link + $tmp_image_file = $this->__get('pma_theme_image') . 's_partialtext.png'; + $tmp_txt = __('Partial texts'); + $url_params_full_text['pftext'] = self::DISPLAY_PARTIAL_TEXT; + } else { + $tmp_image_file = $this->__get('pma_theme_image') . 's_fulltext.png'; + $tmp_txt = __('Full texts'); + $url_params_full_text['pftext'] = self::DISPLAY_FULL_TEXT; + } + + $tmp_image = ''
+                     . $tmp_txt . ''; + $tmp_url = 'sql.php' . Url::getCommon($url_params_full_text); + + return Util::linkOrButton($tmp_url, $tmp_image); + } + + /** + * Get comment for row + * + * @param array $commentsMap comments array + * @param array $fieldsMeta set of field properties + * + * @return string html content + * + * @access private + * + * @see _getTableHeaders() + */ + private function _getCommentForRow(array $commentsMap, $fieldsMeta) + { + return $this->template->render('display/results/comment_for_row', [ + 'comments_map' => $commentsMap, + 'fields_meta' => $fieldsMeta, + 'limit_chars' => $GLOBALS['cfg']['LimitChars'], + ]); + } + + /** + * Prepare parameters and html for sorted table header fields + * + * @param stdClass $fields_meta set of field properties + * @param array $sort_expression sort expression + * @param array $sort_expression_nodirection sort expression without direction + * @param integer $column_index the index of the column + * @param string $unsorted_sql_query the unsorted sql query + * @param integer $session_max_rows maximum rows resulted by sql + * @param string $comments comment for row + * @param array $sort_direction sort direction + * @param boolean $col_visib column is visible(false) + * array column isn't visible(string array) + * @param string $col_visib_j element of $col_visib array + * + * @return array 2 element array - $order_link, $sorted_header_html + * + * @access private + * + * @see _getTableHeaders() + */ + private function _getOrderLinkAndSortedHeaderHtml( + $fields_meta, + array $sort_expression, + array $sort_expression_nodirection, + $column_index, + $unsorted_sql_query, + $session_max_rows, + $comments, + array $sort_direction, + $col_visib, + $col_visib_j + ) { + + $sorted_header_html = ''; + + // Checks if the table name is required; it's the case + // for a query with a "JOIN" statement and if the column + // isn't aliased, or in queries like + // SELECT `1`.`master_field` , `2`.`master_field` + // FROM `PMA_relation` AS `1` , `PMA_relation` AS `2` + + $sort_tbl = isset($fields_meta->table) + && strlen($fields_meta->table) > 0 + && $fields_meta->orgname == $fields_meta->name + ? Util::backquote( + $fields_meta->table + ) . '.' + : ''; + + $name_to_use_in_sort = $fields_meta->name; + + // Generates the orderby clause part of the query which is part + // of URL + list($single_sort_order, $multi_sort_order, $order_img) + = $this->_getSingleAndMultiSortUrls( + $sort_expression, + $sort_expression_nodirection, + $sort_tbl, + $name_to_use_in_sort, + $sort_direction, + $fields_meta + ); + + if (preg_match( + '@(.*)([[:space:]](LIMIT (.*)|PROCEDURE (.*)|FOR UPDATE|' + . 'LOCK IN SHARE MODE))@is', + $unsorted_sql_query, + $regs3 + )) { + $single_sorted_sql_query = $regs3[1] . $single_sort_order . $regs3[2]; + $multi_sorted_sql_query = $regs3[1] . $multi_sort_order . $regs3[2]; + } else { + $single_sorted_sql_query = $unsorted_sql_query . $single_sort_order; + $multi_sorted_sql_query = $unsorted_sql_query . $multi_sort_order; + } + + $_single_url_params = [ + 'db' => $this->__get('db'), + 'table' => $this->__get('table'), + 'sql_query' => $single_sorted_sql_query, + 'sql_signature' => Core::signSqlQuery($single_sorted_sql_query), + 'session_max_rows' => $session_max_rows, + 'is_browse_distinct' => $this->__get('is_browse_distinct'), + ]; + + $_multi_url_params = [ + 'db' => $this->__get('db'), + 'table' => $this->__get('table'), + 'sql_query' => $multi_sorted_sql_query, + 'sql_signature' => Core::signSqlQuery($multi_sorted_sql_query), + 'session_max_rows' => $session_max_rows, + 'is_browse_distinct' => $this->__get('is_browse_distinct'), + ]; + $single_order_url = 'sql.php' . Url::getCommon($_single_url_params); + $multi_order_url = 'sql.php' . Url::getCommon($_multi_url_params); + + // Displays the sorting URL + // enable sort order swapping for image + $order_link = $this->_getSortOrderLink( + $order_img, + $fields_meta, + $single_order_url, + $multi_order_url + ); + + $sorted_header_html .= $this->_getDraggableClassForSortableColumns( + $col_visib, + $col_visib_j, + $fields_meta, + $order_link, + $comments + ); + + return [ + $order_link, + $sorted_header_html, + ]; + } + + /** + * Prepare parameters and html for sorted table header fields + * + * @param array $sort_expression sort expression + * @param array $sort_expression_nodirection sort expression without direction + * @param string $sort_tbl The name of the table to which + * the current column belongs to + * @param string $name_to_use_in_sort The current column under + * consideration + * @param array $sort_direction sort direction + * @param stdClass $fields_meta set of field properties + * + * @return array 3 element array - $single_sort_order, $sort_order, $order_img + * + * @access private + * + * @see _getOrderLinkAndSortedHeaderHtml() + */ + private function _getSingleAndMultiSortUrls( + array $sort_expression, + array $sort_expression_nodirection, + $sort_tbl, + $name_to_use_in_sort, + array $sort_direction, + $fields_meta + ) { + $sort_order = ""; + // Check if the current column is in the order by clause + $is_in_sort = $this->_isInSorted( + $sort_expression, + $sort_expression_nodirection, + $sort_tbl, + $name_to_use_in_sort + ); + $current_name = $name_to_use_in_sort; + if ($sort_expression_nodirection[0] == '' || ! $is_in_sort) { + $special_index = $sort_expression_nodirection[0] == '' + ? 0 + : count($sort_expression_nodirection); + $sort_expression_nodirection[$special_index] + = Util::backquote( + $current_name + ); + $sort_direction[$special_index] = preg_match( + '@time|date@i', + $fields_meta->type + ) ? self::DESCENDING_SORT_DIR : self::ASCENDING_SORT_DIR; + } + + $sort_expression_nodirection = array_filter($sort_expression_nodirection); + $single_sort_order = null; + foreach ($sort_expression_nodirection as $index => $expression) { + // check if this is the first clause, + // if it is then we have to add "order by" + $is_first_clause = ($index == 0); + $name_to_use_in_sort = $expression; + $sort_tbl_new = $sort_tbl; + // Test to detect if the column name is a standard name + // Standard name has the table name prefixed to the column name + if (mb_strpos($name_to_use_in_sort, '.') !== false) { + $matches = explode('.', $name_to_use_in_sort); + // Matches[0] has the table name + // Matches[1] has the column name + $name_to_use_in_sort = $matches[1]; + $sort_tbl_new = $matches[0]; + } + + // $name_to_use_in_sort might contain a space due to + // formatting of function expressions like "COUNT(name )" + // so we remove the space in this situation + $name_to_use_in_sort = str_replace([' )', '``'], [')', '`'], $name_to_use_in_sort); + $name_to_use_in_sort = trim($name_to_use_in_sort, '`'); + + // If this the first column name in the order by clause add + // order by clause to the column name + $query_head = $is_first_clause ? "\nORDER BY " : ""; + // Again a check to see if the given column is a aggregate column + if (mb_strpos($name_to_use_in_sort, '(') !== false) { + $sort_order .= $query_head . $name_to_use_in_sort . ' ' ; + } else { + if (strlen($sort_tbl_new) > 0) { + $sort_tbl_new .= "."; + } + $sort_order .= $query_head . $sort_tbl_new + . Util::backquote( + $name_to_use_in_sort + ) . ' ' ; + } + + // For a special case where the code generates two dots between + // column name and table name. + $sort_order = preg_replace("/\.\./", ".", $sort_order); + // Incase this is the current column save $single_sort_order + if ($current_name == $name_to_use_in_sort) { + if (mb_strpos($current_name, '(') !== false) { + $single_sort_order = "\n" . 'ORDER BY ' . Util::backquote($current_name) . ' '; + } else { + $single_sort_order = "\n" . 'ORDER BY ' . $sort_tbl + . Util::backquote( + $current_name + ) . ' '; + } + if ($is_in_sort) { + list($single_sort_order, $order_img) + = $this->_getSortingUrlParams( + $sort_direction, + $single_sort_order, + $index + ); + } else { + $single_sort_order .= strtoupper($sort_direction[$index]); + } + } + if ($current_name == $name_to_use_in_sort && $is_in_sort) { + // We need to generate the arrow button and related html + list($sort_order, $order_img) = $this->_getSortingUrlParams( + $sort_direction, + $sort_order, + $index + ); + $order_img .= " " . ($index + 1) . ""; + } else { + $sort_order .= strtoupper($sort_direction[$index]); + } + // Separate columns by a comma + $sort_order .= ", "; + } + // remove the comma from the last column name in the newly + // constructed clause + $sort_order = mb_substr( + $sort_order, + 0, + mb_strlen($sort_order) - 2 + ); + if (empty($order_img)) { + $order_img = ''; + } + return [ + $single_sort_order, + $sort_order, + $order_img, + ]; + } + + /** + * Check whether the column is sorted + * + * @param array $sort_expression sort expression + * @param array $sort_expression_nodirection sort expression without direction + * @param string $sort_tbl the table name + * @param string $name_to_use_in_sort the sorting column name + * + * @return boolean the column sorted or not + * + * @access private + * + * @see _getTableHeaders() + */ + private function _isInSorted( + array $sort_expression, + array $sort_expression_nodirection, + $sort_tbl, + $name_to_use_in_sort + ) { + + $index_in_expression = 0; + + foreach ($sort_expression_nodirection as $index => $clause) { + if (mb_strpos($clause, '.') !== false) { + $fragments = explode('.', $clause); + $clause2 = $fragments[0] . "." . str_replace('`', '', $fragments[1]); + } else { + $clause2 = $sort_tbl . str_replace('`', '', $clause); + } + if ($clause2 === $sort_tbl . $name_to_use_in_sort) { + $index_in_expression = $index; + break; + } + } + if (empty($sort_expression[$index_in_expression])) { + $is_in_sort = false; + } else { + // Field name may be preceded by a space, or any number + // of characters followed by a dot (tablename.fieldname) + // so do a direct comparison for the sort expression; + // this avoids problems with queries like + // "SELECT id, count(id)..." and clicking to sort + // on id or on count(id). + // Another query to test this: + // SELECT p.*, FROM_UNIXTIME(p.temps) FROM mytable AS p + // (and try clicking on each column's header twice) + $noSortTable = empty($sort_tbl) || mb_strpos( + $sort_expression_nodirection[$index_in_expression], + $sort_tbl + ) === false; + $noOpenParenthesis = mb_strpos( + $sort_expression_nodirection[$index_in_expression], + '(' + ) === false; + if (! empty($sort_tbl) && $noSortTable && $noOpenParenthesis) { + $new_sort_expression_nodirection = $sort_tbl + . $sort_expression_nodirection[$index_in_expression]; + } else { + $new_sort_expression_nodirection + = $sort_expression_nodirection[$index_in_expression]; + } + + //Back quotes are removed in next comparison, so remove them from value + //to compare. + $name_to_use_in_sort = str_replace('`', '', $name_to_use_in_sort); + + $is_in_sort = false; + $sort_name = str_replace('`', '', $sort_tbl) . $name_to_use_in_sort; + + if ($sort_name == str_replace('`', '', $new_sort_expression_nodirection) + || $sort_name == str_replace('`', '', $sort_expression_nodirection[$index_in_expression]) + ) { + $is_in_sort = true; + } + } + + return $is_in_sort; + } + + /** + * Get sort url parameters - sort order and order image + * + * @param array $sort_direction the sort direction + * @param string $sort_order the sorting order + * @param integer $index the index of sort direction array. + * + * @return array 2 element array - $sort_order, $order_img + * + * @access private + * + * @see _getSingleAndMultiSortUrls() + */ + private function _getSortingUrlParams(array $sort_direction, $sort_order, $index) + { + if (strtoupper(trim($sort_direction[$index])) == self::DESCENDING_SORT_DIR) { + $sort_order .= ' ASC'; + $order_img = ' ' . Util::getImage( + 's_desc', + __('Descending'), + [ + 'class' => "soimg", + 'title' => '', + ] + ); + $order_img .= ' ' . Util::getImage( + 's_asc', + __('Ascending'), + [ + 'class' => "soimg hide", + 'title' => '', + ] + ); + } else { + $sort_order .= ' DESC'; + $order_img = ' ' . Util::getImage( + 's_asc', + __('Ascending'), + [ + 'class' => "soimg", + 'title' => '', + ] + ); + $order_img .= ' ' . Util::getImage( + 's_desc', + __('Descending'), + [ + 'class' => "soimg hide", + 'title' => '', + ] + ); + } + return [ + $sort_order, + $order_img, + ]; + } + + /** + * Get sort order link + * + * @param string $order_img the sort order image + * @param stdClass $fields_meta set of field properties + * @param string $order_url the url for sort + * @param string $multi_order_url the url for sort + * + * @return string the sort order link + * + * @access private + * + * @see _getTableHeaders() + */ + private function _getSortOrderLink( + $order_img, + $fields_meta, + $order_url, + $multi_order_url + ) { + $order_link_params = [ + 'class' => 'sortlink', + ]; + + $order_link_content = htmlspecialchars($fields_meta->name); + $inner_link_content = $order_link_content . $order_img + . ''; + + return Util::linkOrButton( + $order_url, + $inner_link_content, + $order_link_params + ); + } + + /** + * Check if the column contains numeric data. If yes, then set the + * column header's alignment right + * + * @param stdClass $fields_meta set of field properties + * @param array $th_class array containing classes + * + * @return void + * + * @see _getDraggableClassForSortableColumns() + */ + private function _getClassForNumericColumnType($fields_meta, array &$th_class) + { + if (preg_match( + '@int|decimal|float|double|real|bit|boolean|serial@i', + (string) $fields_meta->type + )) { + $th_class[] = 'right'; + } + } + + /** + * Prepare columns to draggable effect for sortable columns + * + * @param boolean $col_visib the column is visible (false) + * array the column is not visible (string array) + * @param string $col_visib_j element of $col_visib array + * @param stdClass $fields_meta set of field properties + * @param string $order_link the order link + * @param string $comments the comment for the column + * + * @return string html content + * + * @access private + * + * @see _getTableHeaders() + */ + private function _getDraggableClassForSortableColumns( + $col_visib, + $col_visib_j, + $fields_meta, + $order_link, + $comments + ) { + + $draggable_html = '_getClassForNumericColumnType($fields_meta, $th_class); + if ($col_visib && ! $col_visib_j) { + $th_class[] = 'hide'; + } + + $th_class[] = 'column_heading'; + if ($GLOBALS['cfg']['BrowsePointerEnable'] == true) { + $th_class[] = 'pointer'; + } + + if ($GLOBALS['cfg']['BrowseMarkerEnable'] == true) { + $th_class[] = 'marker'; + } + + $draggable_html .= ' class="' . implode(' ', $th_class) . '"'; + + $draggable_html .= ' data-column="' . htmlspecialchars($fields_meta->name) + . '">' . $order_link . $comments . ''; + + return $draggable_html; + } + + /** + * Prepare columns to draggable effect for non sortable columns + * + * @param boolean $col_visib the column is visible (false) + * array the column is not visible (string array) + * @param string $col_visib_j element of $col_visib array + * @param boolean $condition_field whether to add CSS class condition + * @param stdClass $fields_meta set of field properties + * @param string $comments the comment for the column + * + * @return string html content + * + * @access private + * + * @see _getTableHeaders() + */ + private function _getDraggableClassForNonSortableColumns( + $col_visib, + $col_visib_j, + $condition_field, + $fields_meta, + $comments + ) { + + $draggable_html = '_getClassForNumericColumnType($fields_meta, $th_class); + if ($col_visib && ! $col_visib_j) { + $th_class[] = 'hide'; + } + + if ($condition_field) { + $th_class[] = 'condition'; + } + + $draggable_html .= ' class="' . implode(' ', $th_class) . '"'; + + $draggable_html .= ' data-column="' + . htmlspecialchars((string) $fields_meta->name) . '">'; + + $draggable_html .= htmlspecialchars((string) $fields_meta->name); + + $draggable_html .= "\n" . $comments . ''; + + return $draggable_html; + } + + /** + * Prepare column to show at right side - check boxes or empty column + * + * @param array $displayParts which elements to display + * @param string $full_or_partial_text_link full/partial link or text button + * @param string $colspan column span of table header + * + * @return string html content + * + * @access private + * + * @see _getTableHeaders() + */ + private function _getColumnAtRightSide( + array &$displayParts, + $full_or_partial_text_link, + $colspan + ) { + + $right_column_html = ''; + $display_params = $this->__get('display_params'); + + // Displays the needed checkboxes at the right + // column of the result table header if possible and required... + if (($GLOBALS['cfg']['RowActionLinks'] == self::POSITION_RIGHT) + || ($GLOBALS['cfg']['RowActionLinks'] == self::POSITION_BOTH) + && (($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE) + || ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE)) + && ($displayParts['text_btn'] == '1') + ) { + $display_params['emptyafter'] + = ($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE) + && ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE) ? 4 : 1; + + $right_column_html .= "\n" + . '' + . $full_or_partial_text_link + . ''; + } elseif (($GLOBALS['cfg']['RowActionLinks'] == self::POSITION_LEFT) + || ($GLOBALS['cfg']['RowActionLinks'] == self::POSITION_BOTH) + && (($displayParts['edit_lnk'] == self::NO_EDIT_OR_DELETE) + && ($displayParts['del_lnk'] == self::NO_EDIT_OR_DELETE)) + && (! isset($GLOBALS['is_header_sent']) || ! $GLOBALS['is_header_sent']) + ) { + // ... elseif no button, displays empty columns if required + // (unless coming from Browse mode print view) + + $display_params['emptyafter'] + = ($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE) + && ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE) ? 4 : 1; + + $right_column_html .= "\n" . ''; + } + + $this->__set('display_params', $display_params); + + return $right_column_html; + } + + /** + * Prepares the display for a value + * + * @param string $class class of table cell + * @param bool $conditionField whether to add CSS class condition + * @param string $value value to display + * + * @return string the td + * + * @access private + * + * @see _getDataCellForGeometryColumns(), + * _getDataCellForNonNumericColumns() + */ + private function _buildValueDisplay($class, $conditionField, $value) + { + return $this->template->render('display/results/value_display', [ + 'class' => $class, + 'condition_field' => $conditionField, + 'value' => $value, + ]); + } + + /** + * Prepares the display for a null value + * + * @param string $class class of table cell + * @param bool $conditionField whether to add CSS class condition + * @param stdClass $meta the meta-information about this field + * @param string $align cell alignment + * + * @return string the td + * + * @access private + * + * @see _getDataCellForNumericColumns(), + * _getDataCellForGeometryColumns(), + * _getDataCellForNonNumericColumns() + */ + private function _buildNullDisplay($class, $conditionField, $meta, $align = '') + { + $classes = $this->_addClass($class, $conditionField, $meta, ''); + + return $this->template->render('display/results/null_display', [ + 'align' => $align, + 'meta' => $meta, + 'classes' => $classes, + ]); + } + + /** + * Prepares the display for an empty value + * + * @param string $class class of table cell + * @param bool $conditionField whether to add CSS class condition + * @param stdClass $meta the meta-information about this field + * @param string $align cell alignment + * + * @return string the td + * + * @access private + * + * @see _getDataCellForNumericColumns(), + * _getDataCellForGeometryColumns(), + * _getDataCellForNonNumericColumns() + */ + private function _buildEmptyDisplay($class, $conditionField, $meta, $align = '') + { + $classes = $this->_addClass($class, $conditionField, $meta, 'nowrap'); + + return $this->template->render('display/results/empty_display', [ + 'align' => $align, + 'classes' => $classes, + ]); + } + + /** + * Adds the relevant classes. + * + * @param string $class class of table cell + * @param bool $condition_field whether to add CSS class + * condition + * @param stdClass $meta the meta-information about the + * field + * @param string $nowrap avoid wrapping + * @param bool $is_field_truncated is field truncated (display ...) + * @param TransformationsPlugin|string $transformation_plugin transformation plugin. + * Can also be the default function: + * Core::mimeDefaultFunction + * @param string $default_function default transformation function + * + * @return string the list of classes + * + * @access private + * + * @see _buildNullDisplay(), _getRowData() + */ + private function _addClass( + $class, + $condition_field, + $meta, + $nowrap, + $is_field_truncated = false, + $transformation_plugin = '', + $default_function = '' + ) { + $classes = [ + $class, + $nowrap, + ]; + + if (isset($meta->mimetype)) { + $classes[] = preg_replace('/\//', '_', $meta->mimetype); + } + + if ($condition_field) { + $classes[] = 'condition'; + } + + if ($is_field_truncated) { + $classes[] = 'truncated'; + } + + $mime_map = $this->__get('mime_map'); + $orgFullColName = $this->__get('db') . '.' . $meta->orgtable + . '.' . $meta->orgname; + if ($transformation_plugin != $default_function + || ! empty($mime_map[$orgFullColName]['input_transformation']) + ) { + $classes[] = 'transformed'; + } + + // Define classes to be added to this data field based on the type of data + $matches = [ + 'enum' => 'enum', + 'set' => 'set', + 'binary' => 'hex', + ]; + + foreach ($matches as $key => $value) { + if (mb_strpos($meta->flags, $key) !== false) { + $classes[] = $value; + } + } + + if (mb_strpos($meta->type, 'bit') !== false) { + $classes[] = 'bit'; + } + + return implode(' ', $classes); + } + + /** + * Prepare the body of the results table + * + * @param integer $dt_result the link id associated to the query + * which results have to be displayed + * @param array $displayParts which elements to display + * @param array $map the list of relations + * @param array $analyzed_sql_results analyzed sql results + * @param boolean $is_limited_display with limited operations or not + * + * @return string html content + * + * @global array $row current row data + * + * @access private + * + * @see getTable() + */ + private function _getTableBody( + &$dt_result, + array &$displayParts, + array $map, + array $analyzed_sql_results, + $is_limited_display = false + ) { + global $row; // mostly because of browser transformations, + // to make the row-data accessible in a plugin + + $table_body_html = ''; + + // query without conditions to shorten URLs when needed, 200 is just + // guess, it should depend on remaining URL length + $url_sql_query = $this->_getUrlSqlQuery($analyzed_sql_results); + + $display_params = $this->__get('display_params'); + + if (! is_array($map)) { + $map = []; + } + + $row_no = 0; + $display_params['edit'] = []; + $display_params['copy'] = []; + $display_params['delete'] = []; + $display_params['data'] = []; + $display_params['row_delete'] = []; + $this->__set('display_params', $display_params); + + // name of the class added to all grid editable elements; + // if we don't have all the columns of a unique key in the result set, + // do not permit grid editing + if ($is_limited_display || ! $this->__get('editable')) { + $grid_edit_class = ''; + } else { + switch ($GLOBALS['cfg']['GridEditing']) { + case 'double-click': + // trying to reduce generated HTML by using shorter + // classes like click1 and click2 + $grid_edit_class = 'grid_edit click2'; + break; + case 'click': + $grid_edit_class = 'grid_edit click1'; + break; + default: // 'disabled' + $grid_edit_class = ''; + break; + } + } + + // prepare to get the column order, if available + list($col_order, $col_visib) = $this->_getColumnParams( + $analyzed_sql_results + ); + + // Correction University of Virginia 19991216 in the while below + // Previous code assumed that all tables have keys, specifically that + // the phpMyAdmin GUI should support row delete/edit only for such + // tables. + // Although always using keys is arguably the prescribed way of + // defining a relational table, it is not required. This will in + // particular be violated by the novice. + // We want to encourage phpMyAdmin usage by such novices. So the code + // below has been changed to conditionally work as before when the + // table being displayed has one or more keys; but to display + // delete/edit options correctly for tables without keys. + + $whereClauseMap = $this->__get('whereClauseMap'); + while ($row = $GLOBALS['dbi']->fetchRow($dt_result)) { + // add repeating headers + if (($row_no != 0) && ($_SESSION['tmpval']['repeat_cells'] != 0) + && ! ($row_no % $_SESSION['tmpval']['repeat_cells']) + ) { + $table_body_html .= $this->_getRepeatingHeaders( + $display_params + ); + } + + $tr_class = []; + if ($GLOBALS['cfg']['BrowsePointerEnable'] != true) { + $tr_class[] = 'nopointer'; + } + if ($GLOBALS['cfg']['BrowseMarkerEnable'] != true) { + $tr_class[] = 'nomarker'; + } + + // pointer code part + $classes = (empty($tr_class) ? ' ' : 'class="' . implode(' ', $tr_class) . '"'); + $table_body_html .= ''; + + // 1. Prepares the row + + // In print view these variable needs to be initialized + $del_url = $del_str = $edit_anchor_class + = $edit_str = $js_conf = $copy_url = $copy_str = $edit_url = null; + + // 1.2 Defines the URLs for the modify/delete link(s) + + if (($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE) + || ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE) + ) { + // Results from a "SELECT" statement -> builds the + // WHERE clause to use in links (a unique key if possible) + /** + * @todo $where_clause could be empty, for example a table + * with only one field and it's a BLOB; in this case, + * avoid to display the delete and edit links + */ + list($where_clause, $clause_is_unique, $condition_array) + = Util::getUniqueCondition( + $dt_result, // handle + $this->__get('fields_cnt'), // fields_cnt + $this->__get('fields_meta'), // fields_meta + $row, // row + false, // force_unique + $this->__get('table'), // restrict_to_table + $analyzed_sql_results // analyzed_sql_results + ); + $whereClauseMap[$row_no][$this->__get('table')] = $where_clause; + $this->__set('whereClauseMap', $whereClauseMap); + + $where_clause_html = htmlspecialchars($where_clause); + + // 1.2.1 Modify link(s) - update row case + if ($displayParts['edit_lnk'] == self::UPDATE_ROW) { + list($edit_url, $copy_url, $edit_str, $copy_str, + $edit_anchor_class) + = $this->_getModifiedLinks( + $where_clause, + $clause_is_unique, + $url_sql_query + ); + } // end if (1.2.1) + + // 1.2.2 Delete/Kill link(s) + list($del_url, $del_str, $js_conf) + = $this->_getDeleteAndKillLinks( + $where_clause, + $clause_is_unique, + $url_sql_query, + $displayParts['del_lnk'], + $row + ); + + // 1.3 Displays the links at left if required + if (($GLOBALS['cfg']['RowActionLinks'] == self::POSITION_LEFT) + || ($GLOBALS['cfg']['RowActionLinks'] == self::POSITION_BOTH) + ) { + $table_body_html .= $this->_getPlacedLinks( + self::POSITION_LEFT, + $del_url, + $displayParts, + $row_no, + $where_clause, + $where_clause_html, + $condition_array, + $edit_url, + $copy_url, + $edit_anchor_class, + $edit_str, + $copy_str, + $del_str, + $js_conf + ); + } elseif ($GLOBALS['cfg']['RowActionLinks'] == self::POSITION_NONE) { + $table_body_html .= $this->_getPlacedLinks( + self::POSITION_NONE, + $del_url, + $displayParts, + $row_no, + $where_clause, + $where_clause_html, + $condition_array, + $edit_url, + $copy_url, + $edit_anchor_class, + $edit_str, + $copy_str, + $del_str, + $js_conf + ); + } // end if (1.3) + } // end if (1) + + // 2. Displays the rows' values + if ($this->__get('mime_map') === null) { + $this->_setMimeMap(); + } + $table_body_html .= $this->_getRowValues( + $dt_result, + $row, + $row_no, + $col_order, + $map, + $grid_edit_class, + $col_visib, + $url_sql_query, + $analyzed_sql_results + ); + + // 3. Displays the modify/delete links on the right if required + if (($displayParts['edit_lnk'] != self::NO_EDIT_OR_DELETE) + || ($displayParts['del_lnk'] != self::NO_EDIT_OR_DELETE) + ) { + if (($GLOBALS['cfg']['RowActionLinks'] == self::POSITION_RIGHT) + || ($GLOBALS['cfg']['RowActionLinks'] == self::POSITION_BOTH) + ) { + $table_body_html .= $this->_getPlacedLinks( + self::POSITION_RIGHT, + $del_url, + $displayParts, + $row_no, + $where_clause, + $where_clause_html, + $condition_array, + $edit_url, + $copy_url, + $edit_anchor_class, + $edit_str, + $copy_str, + $del_str, + $js_conf + ); + } + } // end if (3) + + $table_body_html .= ''; + $table_body_html .= "\n"; + $row_no++; + } // end while + + return $table_body_html; + } + + /** + * Sets the MIME details of the columns in the results set + * + * @return void + */ + private function _setMimeMap() + { + $fields_meta = $this->__get('fields_meta'); + $mimeMap = []; + $added = []; + + for ($currentColumn = 0; $currentColumn < $this->__get('fields_cnt'); ++$currentColumn) { + $meta = $fields_meta[$currentColumn]; + $orgFullTableName = $this->__get('db') . '.' . $meta->orgtable; + + if ($GLOBALS['cfgRelation']['commwork'] + && $GLOBALS['cfgRelation']['mimework'] + && $GLOBALS['cfg']['BrowseMIME'] + && ! $_SESSION['tmpval']['hide_transformation'] + && empty($added[$orgFullTableName]) + ) { + $mimeMap = array_merge( + $mimeMap, + $this->transformations->getMime($this->__get('db'), $meta->orgtable, false, true) + ); + $added[$orgFullTableName] = true; + } + } + + // special browser transformation for some SHOW statements + if ($this->__get('is_show') + && ! $_SESSION['tmpval']['hide_transformation'] + ) { + preg_match( + '@^SHOW[[:space:]]+(VARIABLES|(FULL[[:space:]]+)?' + . 'PROCESSLIST|STATUS|TABLE|GRANTS|CREATE|LOGS|DATABASES|FIELDS' + . ')@i', + $this->__get('sql_query'), + $which + ); + + if (isset($which[1])) { + $str = ' ' . strtoupper($which[1]); + $isShowProcessList = strpos($str, 'PROCESSLIST') > 0; + if ($isShowProcessList) { + $mimeMap['..Info'] = [ + 'mimetype' => 'Text_Plain', + 'transformation' => 'output/Text_Plain_Sql.php', + ]; + } + + $isShowCreateTable = preg_match( + '@CREATE[[:space:]]+TABLE@i', + $this->__get('sql_query') + ); + if ($isShowCreateTable) { + $mimeMap['..Create Table'] = [ + 'mimetype' => 'Text_Plain', + 'transformation' => 'output/Text_Plain_Sql.php', + ]; + } + } + } + + $this->__set('mime_map', $mimeMap); + } + + /** + * Get the values for one data row + * + * @param integer $dt_result the link id associated to + * the query which results + * have to be displayed + * @param array $row current row data + * @param integer $row_no the index of current row + * @param array|boolean $col_order the column order false when + * a property not found false + * when a property not found + * @param array $map the list of relations + * @param string $grid_edit_class the class for all editable + * columns + * @param boolean|array|string $col_visib column is visible(false); + * column isn't visible(string + * array) + * @param string $url_sql_query the analyzed sql query + * @param array $analyzed_sql_results analyzed sql results + * + * @return string html content + * + * @access private + * + * @see _getTableBody() + */ + private function _getRowValues( + &$dt_result, + array $row, + $row_no, + $col_order, + array $map, + $grid_edit_class, + $col_visib, + $url_sql_query, + array $analyzed_sql_results + ) { + $row_values_html = ''; + + // Following variable are needed for use in isset/empty or + // use with array indexes/safe use in foreach + $sql_query = $this->__get('sql_query'); + $fields_meta = $this->__get('fields_meta'); + $highlight_columns = $this->__get('highlight_columns'); + $mime_map = $this->__get('mime_map'); + + $row_info = $this->_getRowInfoForSpecialLinks($row, $col_order); + + $whereClauseMap = $this->__get('whereClauseMap'); + + $columnCount = $this->__get('fields_cnt'); + for ($currentColumn = 0; $currentColumn < $columnCount; ++$currentColumn) { + // assign $i with appropriate column order + $i = $col_order ? $col_order[$currentColumn] : $currentColumn; + + $meta = $fields_meta[$i]; + $orgFullColName + = $this->__get('db') . '.' . $meta->orgtable . '.' . $meta->orgname; + + $not_null_class = $meta->not_null ? 'not_null' : ''; + $relation_class = isset($map[$meta->name]) ? 'relation' : ''; + $hide_class = $col_visib && ! $col_visib[$currentColumn] + ? 'hide' + : ''; + $grid_edit = $meta->orgtable != '' ? $grid_edit_class : ''; + + // handle datetime-related class, for grid editing + $field_type_class + = $this->_getClassForDateTimeRelatedFields($meta->type); + + $is_field_truncated = false; + // combine all the classes applicable to this column's value + $class = $this->_getClassesForColumn( + $grid_edit, + $not_null_class, + $relation_class, + $hide_class, + $field_type_class + ); + + // See if this column should get highlight because it's used in the + // where-query. + $condition_field = isset($highlight_columns) + && (isset($highlight_columns[$meta->name]) + || isset($highlight_columns[Util::backquote($meta->name)])) + ? true + : false; + + // Wrap MIME-transformations. [MIME] + $default_function = [ + Core::class, + 'mimeDefaultFunction', + ]; // default_function + $transformation_plugin = $default_function; + $transform_options = []; + + if ($GLOBALS['cfgRelation']['mimework'] + && $GLOBALS['cfg']['BrowseMIME'] + ) { + if (isset($mime_map[$orgFullColName]['mimetype']) + && ! empty($mime_map[$orgFullColName]['transformation']) + ) { + $file = $mime_map[$orgFullColName]['transformation']; + $include_file = 'libraries/classes/Plugins/Transformations/' . $file; + + if (@file_exists($include_file)) { + $class_name = $this->transformations->getClassName($include_file); + if (class_exists($class_name)) { + // todo add $plugin_manager + $plugin_manager = null; + $transformation_plugin = new $class_name( + $plugin_manager + ); + + $transform_options = $this->transformations->getOptions( + isset( + $mime_map[$orgFullColName]['transformation_options'] + ) + ? $mime_map[$orgFullColName]['transformation_options'] + : '' + ); + + $meta->mimetype = str_replace( + '_', + '/', + $mime_map[$orgFullColName]['mimetype'] + ); + } + } // end if file_exists + } // end if transformation is set + } // end if mime/transformation works. + + // Check whether the field needs to display with syntax highlighting + + $dbLower = mb_strtolower($this->__get('db')); + $tblLower = mb_strtolower($meta->orgtable); + $nameLower = mb_strtolower($meta->orgname); + if (! empty($this->transformation_info[$dbLower][$tblLower][$nameLower]) + && (trim($row[$i]) != '') + && ! $_SESSION['tmpval']['hide_transformation'] + ) { + include_once $this->transformation_info[$dbLower][$tblLower][$nameLower][0]; + $transformation_plugin = new $this->transformation_info[$dbLower][$tblLower][$nameLower][1](null); + + $transform_options = $this->transformations->getOptions( + isset($mime_map[$orgFullColName]['transformation_options']) + ? $mime_map[$orgFullColName]['transformation_options'] + : '' + ); + + $meta->mimetype = str_replace( + '_', + '/', + $this->transformation_info[$dbLower][mb_strtolower($meta->orgtable)][mb_strtolower($meta->orgname)][2] + ); + } + + // Check for the predefined fields need to show as link in schemas + $specialSchemaLinks = SpecialSchemaLinks::get(); + + if (! empty($specialSchemaLinks[$dbLower][$tblLower][$nameLower])) { + $linking_url = $this->_getSpecialLinkUrl( + $specialSchemaLinks, + $row[$i], + $row_info, + mb_strtolower($meta->orgname) + ); + $transformation_plugin = new Text_Plain_Link(); + + $transform_options = [ + 0 => $linking_url, + 2 => true, + ]; + + $meta->mimetype = str_replace( + '_', + '/', + 'Text/Plain' + ); + } + + /* + * The result set can have columns from more than one table, + * this is why we have to check for the unique conditions + * related to this table; however getUniqueCondition() is + * costly and does not need to be called if we already know + * the conditions for the current table. + */ + if (! isset($whereClauseMap[$row_no][$meta->orgtable])) { + $unique_conditions = Util::getUniqueCondition( + $dt_result, // handle + $this->__get('fields_cnt'), // fields_cnt + $this->__get('fields_meta'), // fields_meta + $row, // row + false, // force_unique + $meta->orgtable, // restrict_to_table + $analyzed_sql_results // analyzed_sql_results + ); + $whereClauseMap[$row_no][$meta->orgtable] = $unique_conditions[0]; + } + + $_url_params = [ + 'db' => $this->__get('db'), + 'table' => $meta->orgtable, + 'where_clause' => $whereClauseMap[$row_no][$meta->orgtable], + 'transform_key' => $meta->orgname, + ]; + + if (! empty($sql_query)) { + $_url_params['sql_query'] = $url_sql_query; + } + + $transform_options['wrapper_link'] = Url::getCommon($_url_params); + + $display_params = $this->__get('display_params'); + + // in some situations (issue 11406), numeric returns 1 + // even for a string type + // for decimal numeric is returning 1 + // have to improve logic + if (($meta->numeric == 1 && $meta->type != 'string') || $meta->type == 'real') { + // n u m e r i c + + $display_params['data'][$row_no][$i] + = $this->_getDataCellForNumericColumns( + $row[$i] === null ? null : (string) $row[$i], + $class, + $condition_field, + $meta, + $map, + $is_field_truncated, + $analyzed_sql_results, + $transformation_plugin, + $default_function, + $transform_options + ); + } elseif ($meta->type == self::GEOMETRY_FIELD) { + // g e o m e t r y + + // Remove 'grid_edit' from $class as we do not allow to + // inline-edit geometry data. + $class = str_replace('grid_edit', '', $class); + + $display_params['data'][$row_no][$i] + = $this->_getDataCellForGeometryColumns( + $row[$i], + $class, + $meta, + $map, + $_url_params, + $condition_field, + $transformation_plugin, + $default_function, + $transform_options, + $analyzed_sql_results + ); + } else { + // n o t n u m e r i c + + $display_params['data'][$row_no][$i] + = $this->_getDataCellForNonNumericColumns( + $row[$i], + $class, + $meta, + $map, + $_url_params, + $condition_field, + $transformation_plugin, + $default_function, + $transform_options, + $is_field_truncated, + $analyzed_sql_results, + $dt_result, + $i + ); + } + + // output stored cell + $row_values_html .= $display_params['data'][$row_no][$i]; + + if (isset($display_params['rowdata'][$i][$row_no])) { + $display_params['rowdata'][$i][$row_no] + .= $display_params['data'][$row_no][$i]; + } else { + $display_params['rowdata'][$i][$row_no] + = $display_params['data'][$row_no][$i]; + } + + $this->__set('display_params', $display_params); + } // end for + + return $row_values_html; + } + + /** + * Get link for display special schema links + * + * @param array $specialSchemaLinks special schema links + * @param string $column_value column value + * @param array $row_info information about row + * @param string $field_name column name + * + * @return string generated link + */ + private function _getSpecialLinkUrl( + array $specialSchemaLinks, + $column_value, + array $row_info, + $field_name + ) { + $linking_url_params = []; + $link_relations = $specialSchemaLinks[mb_strtolower($this->__get('db'))][mb_strtolower($this->__get('table'))][$field_name]; + + if (! is_array($link_relations['link_param'])) { + $linking_url_params[$link_relations['link_param']] = $column_value; + } else { + // Consider only the case of creating link for column field + // sql query that needs to be passed as url param + $sql = 'SELECT `' . $column_value . '` FROM `' + . $row_info[$link_relations['link_param'][1]] . '`.`' + . $row_info[$link_relations['link_param'][2]] . '`'; + $linking_url_params[$link_relations['link_param'][0]] = $sql; + } + + $divider = strpos($link_relations['default_page'], '?') ? '&' : '?'; + if (empty($link_relations['link_dependancy_params'])) { + return $link_relations['default_page'] + . Url::getCommonRaw($linking_url_params, $divider); + } + + foreach ($link_relations['link_dependancy_params'] as $new_param) { + // If param_info is an array, set the key and value + // from that array + if (is_array($new_param['param_info'])) { + $linking_url_params[$new_param['param_info'][0]] + = $new_param['param_info'][1]; + continue; + } + + $linking_url_params[$new_param['param_info']] + = $row_info[mb_strtolower($new_param['column_name'])]; + + // Special case 1 - when executing routines, according + // to the type of the routine, url param changes + if (empty($row_info['routine_type'])) { + continue; + } + } + + return $link_relations['default_page'] + . Url::getCommonRaw($linking_url_params, $divider); + } + + /** + * Prepare row information for display special links + * + * @param array $row current row data + * @param array|boolean $col_order the column order + * + * @return array associative array with column nama -> value + */ + private function _getRowInfoForSpecialLinks(array $row, $col_order) + { + + $row_info = []; + $fields_meta = $this->__get('fields_meta'); + + for ($n = 0; $n < $this->__get('fields_cnt'); ++$n) { + $m = $col_order ? $col_order[$n] : $n; + $row_info[mb_strtolower($fields_meta[$m]->orgname)] + = $row[$m]; + } + + return $row_info; + } + + /** + * Get url sql query without conditions to shorten URLs + * + * @param array $analyzed_sql_results analyzed sql results + * + * @return string analyzed sql query + * + * @access private + * + * @see _getTableBody() + */ + private function _getUrlSqlQuery(array $analyzed_sql_results) + { + if (($analyzed_sql_results['querytype'] != 'SELECT') + || (mb_strlen($this->__get('sql_query')) < 200) + ) { + return $this->__get('sql_query'); + } + + $query = 'SELECT ' . Query::getClause( + $analyzed_sql_results['statement'], + $analyzed_sql_results['parser']->list, + 'SELECT' + ); + + $from_clause = Query::getClause( + $analyzed_sql_results['statement'], + $analyzed_sql_results['parser']->list, + 'FROM' + ); + + if (! empty($from_clause)) { + $query .= ' FROM ' . $from_clause; + } + + return $query; + } + + /** + * Get column order and column visibility + * + * @param array $analyzed_sql_results analyzed sql results + * + * @return array 2 element array - $col_order, $col_visib + * + * @access private + * + * @see _getTableBody() + */ + private function _getColumnParams(array $analyzed_sql_results) + { + if ($this->_isSelect($analyzed_sql_results)) { + $pmatable = new Table($this->__get('table'), $this->__get('db')); + $col_order = $pmatable->getUiProp(Table::PROP_COLUMN_ORDER); + /* Validate the value */ + if ($col_order !== false) { + $fields_cnt = $this->__get('fields_cnt'); + foreach ($col_order as $value) { + if ($value >= $fields_cnt) { + $pmatable->removeUiProp(Table::PROP_COLUMN_ORDER); + $fields_cnt = false; + } + } + } + $col_visib = $pmatable->getUiProp(Table::PROP_COLUMN_VISIB); + } else { + $col_order = false; + $col_visib = false; + } + + return [ + $col_order, + $col_visib, + ]; + } + + /** + * Get HTML for repeating headers + * + * @param array $display_params holds various display info + * + * @return string html content + * + * @access private + * + * @see _getTableBody() + */ + private function _getRepeatingHeaders( + array $display_params + ) { + $header_html = '' . "\n"; + + if ($display_params['emptypre'] > 0) { + $header_html .= ' ' + . "\n" . '  ' . "\n"; + } elseif ($GLOBALS['cfg']['RowActionLinks'] == self::POSITION_NONE) { + $header_html .= ' ' . "\n"; + } + + foreach ($display_params['desc'] as $val) { + $header_html .= $val; + } + + if ($display_params['emptyafter'] > 0) { + $header_html + .= ' ' + . "\n" . '  ' . "\n"; + } + $header_html .= '' . "\n"; + + return $header_html; + } + + /** + * Get modified links + * + * @param string $where_clause the where clause of the sql + * @param boolean $clause_is_unique the unique condition of clause + * @param string $url_sql_query the analyzed sql query + * + * @return array 5 element array - $edit_url, $copy_url, + * $edit_str, $copy_str, $edit_anchor_class + * + * @access private + * + * @see _getTableBody() + */ + private function _getModifiedLinks( + $where_clause, + $clause_is_unique, + $url_sql_query + ) { + + $_url_params = [ + 'db' => $this->__get('db'), + 'table' => $this->__get('table'), + 'where_clause' => $where_clause, + 'clause_is_unique' => $clause_is_unique, + 'sql_query' => $url_sql_query, + 'goto' => 'sql.php', + ]; + + $edit_url = 'tbl_change.php' + . Url::getCommon( + $_url_params + ['default_action' => 'update'] + ); + + $copy_url = 'tbl_change.php' + . Url::getCommon( + $_url_params + ['default_action' => 'insert'] + ); + + $edit_str = $this->_getActionLinkContent( + 'b_edit', + __('Edit') + ); + $copy_str = $this->_getActionLinkContent( + 'b_insrow', + __('Copy') + ); + + // Class definitions required for grid editing jQuery scripts + $edit_anchor_class = "edit_row_anchor"; + if ($clause_is_unique == 0) { + $edit_anchor_class .= ' nonunique'; + } + + return [ + $edit_url, + $copy_url, + $edit_str, + $copy_str, + $edit_anchor_class, + ]; + } + + /** + * Get delete and kill links + * + * @param string $where_clause the where clause of the sql + * @param boolean $clause_is_unique the unique condition of clause + * @param string $url_sql_query the analyzed sql query + * @param string $del_lnk the delete link of current row + * @param array $row the current row + * + * @return array 3 element array + * $del_url, $del_str, $js_conf + * + * @access private + * + * @see _getTableBody() + */ + private function _getDeleteAndKillLinks( + $where_clause, + $clause_is_unique, + $url_sql_query, + $del_lnk, + array $row + ) { + + $goto = $this->__get('goto'); + + if ($del_lnk == self::DELETE_ROW) { // delete row case + $_url_params = [ + 'db' => $this->__get('db'), + 'table' => $this->__get('table'), + 'sql_query' => $url_sql_query, + 'message_to_show' => __('The row has been deleted.'), + 'goto' => empty($goto) ? 'tbl_sql.php' : $goto, + ]; + + $lnk_goto = 'sql.php' . Url::getCommonRaw($_url_params); + + $del_query = 'DELETE FROM ' + . Util::backquote($this->__get('table')) + . ' WHERE ' . $where_clause . + ($clause_is_unique ? '' : ' LIMIT 1'); + + $_url_params = [ + 'db' => $this->__get('db'), + 'table' => $this->__get('table'), + 'sql_query' => $del_query, + 'message_to_show' => __('The row has been deleted.'), + 'goto' => $lnk_goto, + ]; + $del_url = 'sql.php' . Url::getCommon($_url_params); + + $js_conf = 'DELETE FROM ' . Sanitize::jsFormat($this->__get('table')) + . ' WHERE ' . Sanitize::jsFormat($where_clause, false) + . ($clause_is_unique ? '' : ' LIMIT 1'); + + $del_str = $this->_getActionLinkContent('b_drop', __('Delete')); + } elseif ($del_lnk == self::KILL_PROCESS) { // kill process case + $_url_params = [ + 'db' => $this->__get('db'), + 'table' => $this->__get('table'), + 'sql_query' => $url_sql_query, + 'goto' => 'index.php', + ]; + + $lnk_goto = 'sql.php' . Url::getCommonRaw($_url_params); + + $kill = $GLOBALS['dbi']->getKillQuery((int) $row[0]); + + $_url_params = [ + 'db' => 'mysql', + 'sql_query' => $kill, + 'goto' => $lnk_goto, + ]; + + $del_url = 'sql.php' . Url::getCommon($_url_params); + $js_conf = $kill; + $del_str = Util::getIcon( + 'b_drop', + __('Kill') + ); + } else { + $del_url = $del_str = $js_conf = null; + } + + return [ + $del_url, + $del_str, + $js_conf, + ]; + } + + /** + * Get content inside the table row action links (Edit/Copy/Delete) + * + * @param string $icon The name of the file to get + * @param string $display_text The text displaying after the image icon + * + * @return string + * + * @access private + * + * @see _getModifiedLinks(), _getDeleteAndKillLinks() + */ + private function _getActionLinkContent($icon, $display_text) + { + + $linkContent = ''; + + if (isset($GLOBALS['cfg']['RowActionType']) + && $GLOBALS['cfg']['RowActionType'] == self::ACTION_LINK_CONTENT_ICONS + ) { + $linkContent .= '' + . Util::getImage( + $icon, + $display_text + ) + . ''; + } elseif (isset($GLOBALS['cfg']['RowActionType']) + && $GLOBALS['cfg']['RowActionType'] == self::ACTION_LINK_CONTENT_TEXT + ) { + $linkContent .= '' . $display_text . ''; + } else { + $linkContent .= Util::getIcon( + $icon, + $display_text + ); + } + + return $linkContent; + } + + /** + * Prepare placed links + * + * @param string $dir the direction of links should place + * @param string $del_url the url for delete row + * @param array $displayParts which elements to display + * @param integer $row_no the index of current row + * @param string $where_clause the where clause of the sql + * @param string $where_clause_html the html encoded where clause + * @param array $condition_array array of keys (primary, unique, condition) + * @param string $edit_url the url for edit row + * @param string $copy_url the url for copy row + * @param string $edit_anchor_class the class for html element for edit + * @param string $edit_str the label for edit row + * @param string $copy_str the label for copy row + * @param string $del_str the label for delete row + * @param string|null $js_conf text for the JS confirmation + * + * @return string html content + * + * @access private + * + * @see _getTableBody() + */ + private function _getPlacedLinks( + $dir, + $del_url, + array $displayParts, + $row_no, + $where_clause, + $where_clause_html, + array $condition_array, + $edit_url, + $copy_url, + $edit_anchor_class, + $edit_str, + $copy_str, + $del_str, + ?string $js_conf + ) { + + if (! isset($js_conf)) { + $js_conf = ''; + } + + return $this->_getCheckboxAndLinks( + $dir, + $del_url, + $displayParts, + $row_no, + $where_clause, + $where_clause_html, + $condition_array, + $edit_url, + $copy_url, + $edit_anchor_class, + $edit_str, + $copy_str, + $del_str, + $js_conf + ); + } + + /** + * Get the combined classes for a column + * + * @param string $grid_edit_class the class for all editable columns + * @param string $not_null_class the class for not null columns + * @param string $relation_class the class for relations in a column + * @param string $hide_class the class for visibility of a column + * @param string $field_type_class the class related to type of the field + * + * @return string the combined classes + * + * @access private + * + * @see _getTableBody() + */ + private function _getClassesForColumn( + $grid_edit_class, + $not_null_class, + $relation_class, + $hide_class, + $field_type_class + ) { + return 'data ' . $grid_edit_class . ' ' . $not_null_class . ' ' + . $relation_class . ' ' . $hide_class . ' ' . $field_type_class; + } + + /** + * Get class for datetime related fields + * + * @param string $type the type of the column field + * + * @return string the class for the column + * + * @access private + * + * @see _getTableBody() + */ + private function _getClassForDateTimeRelatedFields($type) + { + if ((substr($type, 0, 9) == self::TIMESTAMP_FIELD) + || ($type == self::DATETIME_FIELD) + ) { + $field_type_class = 'datetimefield'; + } elseif ($type == self::DATE_FIELD) { + $field_type_class = 'datefield'; + } elseif ($type == self::TIME_FIELD) { + $field_type_class = 'timefield'; + } elseif ($type == self::STRING_FIELD) { + $field_type_class = 'text'; + } else { + $field_type_class = ''; + } + return $field_type_class; + } + + /** + * Prepare data cell for numeric type fields + * + * @param string|null $column the column's value + * @param string $class the html class for column + * @param boolean $condition_field the column should highlighted + * or not + * @param stdClass $meta the meta-information about this + * field + * @param array $map the list of relations + * @param boolean $is_field_truncated the condition for blob data + * replacements + * @param array $analyzed_sql_results the analyzed query + * @param TransformationsPlugin $transformation_plugin the name of transformation plugin + * @param string $default_function the default transformation + * function + * @param array $transform_options the transformation parameters + * + * @return string the prepared cell, html content + * + * @access private + * + * @see _getTableBody() + */ + private function _getDataCellForNumericColumns( + ?string $column, + $class, + $condition_field, + $meta, + array $map, + $is_field_truncated, + array $analyzed_sql_results, + $transformation_plugin, + $default_function, + array $transform_options + ) { + + if (! isset($column) || $column === null) { + $cell = $this->_buildNullDisplay( + 'right ' . $class, + $condition_field, + $meta, + '' + ); + } elseif ($column != '') { + $nowrap = ' nowrap'; + $where_comparison = ' = ' . $column; + + $cell = $this->_getRowData( + 'right ' . $class, + $condition_field, + $analyzed_sql_results, + $meta, + $map, + $column, + $column, + $transformation_plugin, + $default_function, + $nowrap, + $where_comparison, + $transform_options, + $is_field_truncated, + '' + ); + } else { + $cell = $this->_buildEmptyDisplay( + 'right ' . $class, + $condition_field, + $meta, + '' + ); + } + + return $cell; + } + + /** + * Get data cell for geometry type fields + * + * @param string|null $column the relevant column in data row + * @param string $class the html class for column + * @param stdClass $meta the meta-information about + * this field + * @param array $map the list of relations + * @param array $_url_params the parameters for generate url + * @param boolean $condition_field the column should highlighted + * or not + * @param TransformationsPlugin $transformation_plugin the name of transformation + * function + * @param string $default_function the default transformation + * function + * @param string $transform_options the transformation parameters + * @param array $analyzed_sql_results the analyzed query + * + * @return string the prepared data cell, html content + * + * @access private + * + * @see _getTableBody() + */ + private function _getDataCellForGeometryColumns( + ?string $column, + $class, + $meta, + array $map, + array $_url_params, + $condition_field, + $transformation_plugin, + $default_function, + $transform_options, + array $analyzed_sql_results + ) { + if (! isset($column) || $column === null) { + $cell = $this->_buildNullDisplay($class, $condition_field, $meta); + return $cell; + } + + if ($column == '') { + $cell = $this->_buildEmptyDisplay($class, $condition_field, $meta); + return $cell; + } + + // Display as [GEOMETRY - (size)] + if ($_SESSION['tmpval']['geoOption'] == self::GEOMETRY_DISP_GEOM) { + $geometry_text = $this->_handleNonPrintableContents( + strtoupper(self::GEOMETRY_FIELD), + $column, + $transformation_plugin, + $transform_options, + $default_function, + $meta, + $_url_params + ); + + $cell = $this->_buildValueDisplay( + $class, + $condition_field, + $geometry_text + ); + return $cell; + } + + if ($_SESSION['tmpval']['geoOption'] == self::GEOMETRY_DISP_WKT) { + // Prepare in Well Known Text(WKT) format. + $where_comparison = ' = ' . $column; + + // Convert to WKT format + $wktval = Util::asWKT($column); + list( + $is_field_truncated, + $displayedColumn, + // skip 3rd param + ) = $this->_getPartialText($wktval); + + $cell = $this->_getRowData( + $class, + $condition_field, + $analyzed_sql_results, + $meta, + $map, + $wktval, + $displayedColumn, + $transformation_plugin, + $default_function, + '', + $where_comparison, + $transform_options, + $is_field_truncated, + '' + ); + return $cell; + } + + // Prepare in Well Known Binary (WKB) format. + + if ($_SESSION['tmpval']['display_binary']) { + $where_comparison = ' = ' . $column; + + $wkbval = substr(bin2hex($column), 8); + list( + $is_field_truncated, + $displayedColumn, + // skip 3rd param + ) = $this->_getPartialText($wkbval); + + $cell = $this->_getRowData( + $class, + $condition_field, + $analyzed_sql_results, + $meta, + $map, + $wkbval, + $displayedColumn, + $transformation_plugin, + $default_function, + '', + $where_comparison, + $transform_options, + $is_field_truncated, + '' + ); + return $cell; + } + + $wkbval = $this->_handleNonPrintableContents( + self::BINARY_FIELD, + $column, + $transformation_plugin, + $transform_options, + $default_function, + $meta, + $_url_params + ); + + $cell = $this->_buildValueDisplay( + $class, + $condition_field, + $wkbval + ); + + return $cell; + } + + /** + * Get data cell for non numeric type fields + * + * @param string|null $column the relevant column in data row + * @param string $class the html class for column + * @param stdClass $meta the meta-information about + * the field + * @param array $map the list of relations + * @param array $_url_params the parameters for generate + * url + * @param boolean $condition_field the column should highlighted + * or not + * @param TransformationsPlugin $transformation_plugin the name of transformation + * function + * @param string $default_function the default transformation + * function + * @param string $transform_options the transformation parameters + * @param boolean $is_field_truncated is data truncated due to + * LimitChars + * @param array $analyzed_sql_results the analyzed query + * @param integer $dt_result the link id associated to + * the query which results + * have to be displayed + * @param integer $col_index the column index + * + * @return string the prepared data cell, html content + * + * @access private + * + * @see _getTableBody() + */ + private function _getDataCellForNonNumericColumns( + ?string $column, + $class, + $meta, + array $map, + array $_url_params, + $condition_field, + $transformation_plugin, + $default_function, + $transform_options, + $is_field_truncated, + array $analyzed_sql_results, + &$dt_result, + $col_index + ) { + $original_length = 0; + + $is_analyse = $this->__get('is_analyse'); + $field_flags = $GLOBALS['dbi']->fieldFlags($dt_result, $col_index); + + $bIsText = gettype($transformation_plugin) === 'object' + && strpos($transformation_plugin->getMIMEType(), 'Text') + === false; + + // disable inline grid editing + // if binary fields are protected + // or transformation plugin is of non text type + // such as image + if ((false !== stripos($field_flags, self::BINARY_FIELD) + && ($GLOBALS['cfg']['ProtectBinary'] === 'all' + || ($GLOBALS['cfg']['ProtectBinary'] === 'noblob' + && false === stripos($meta->type, self::BLOB_FIELD)) + || ($GLOBALS['cfg']['ProtectBinary'] === 'blob' + && false !== stripos($meta->type, self::BLOB_FIELD)))) + || $bIsText + ) { + $class = str_replace('grid_edit', '', $class); + } + + if (! isset($column) || $column === null) { + $cell = $this->_buildNullDisplay($class, $condition_field, $meta); + return $cell; + } + + if ($column == '') { + $cell = $this->_buildEmptyDisplay($class, $condition_field, $meta); + return $cell; + } + + // Cut all fields to $GLOBALS['cfg']['LimitChars'] + // (unless it's a link-type transformation or binary) + $displayedColumn = $column; + if (! (gettype($transformation_plugin) === "object" + && strpos($transformation_plugin->getName(), 'Link') !== false) + && false === stripos($field_flags, self::BINARY_FIELD) + ) { + list( + $is_field_truncated, + $column, + $original_length + ) = $this->_getPartialText($column); + } + + $formatted = false; + if (isset($meta->_type) && $meta->_type === MYSQLI_TYPE_BIT) { + $displayedColumn = Util::printableBitValue( + (int) $displayedColumn, + (int) $meta->length + ); + + // some results of PROCEDURE ANALYSE() are reported as + // being BINARY but they are quite readable, + // so don't treat them as BINARY + } elseif (false !== stripos($field_flags, self::BINARY_FIELD) + && ! (isset($is_analyse) && $is_analyse) + ) { + // we show the BINARY or BLOB message and field's size + // (or maybe use a transformation) + $binary_or_blob = self::BLOB_FIELD; + if ($meta->type === self::STRING_FIELD) { + $binary_or_blob = self::BINARY_FIELD; + } + $displayedColumn = $this->_handleNonPrintableContents( + $binary_or_blob, + $displayedColumn, + $transformation_plugin, + $transform_options, + $default_function, + $meta, + $_url_params, + $is_field_truncated + ); + $class = $this->_addClass( + $class, + $condition_field, + $meta, + '', + $is_field_truncated, + $transformation_plugin, + $default_function + ); + $result = strip_tags($column); + // disable inline grid editing + // if binary or blob data is not shown + if (false !== stripos($result, $binary_or_blob)) { + $class = str_replace('grid_edit', '', $class); + } + $formatted = true; + } + + if ($formatted) { + $cell = $this->_buildValueDisplay( + $class, + $condition_field, + $displayedColumn + ); + return $cell; + } + + // transform functions may enable no-wrapping: + $function_nowrap = 'applyTransformationNoWrap'; + + $bool_nowrap = ($default_function != $transformation_plugin) + && function_exists((string) $transformation_plugin->$function_nowrap()) + ? $transformation_plugin->$function_nowrap($transform_options) + : false; + + // do not wrap if date field type + $nowrap = preg_match('@DATE|TIME@i', $meta->type) + || $bool_nowrap ? ' nowrap' : ''; + + $where_comparison = ' = \'' + . $GLOBALS['dbi']->escapeString($column) + . '\''; + + $cell = $this->_getRowData( + $class, + $condition_field, + $analyzed_sql_results, + $meta, + $map, + $column, + $displayedColumn, + $transformation_plugin, + $default_function, + $nowrap, + $where_comparison, + $transform_options, + $is_field_truncated, + $original_length + ); + + return $cell; + } + + /** + * Checks the posted options for viewing query results + * and sets appropriate values in the session. + * + * @todo make maximum remembered queries configurable + * @todo move/split into SQL class!? + * @todo currently this is called twice unnecessary + * @todo ignore LIMIT and ORDER in query!? + * + * @return void + * + * @access public + * + * @see sql.php file + */ + public function setConfigParamsForDisplayTable() + { + + $sql_md5 = md5($this->__get('sql_query')); + $query = []; + if (isset($_SESSION['tmpval']['query'][$sql_md5])) { + $query = $_SESSION['tmpval']['query'][$sql_md5]; + } + + $query['sql'] = $this->__get('sql_query'); + + if (empty($query['repeat_cells'])) { + $query['repeat_cells'] = $GLOBALS['cfg']['RepeatCells']; + } + + // as this is a form value, the type is always string so we cannot + // use Core::isValid($_POST['session_max_rows'], 'integer') + if (Core::isValid($_POST['session_max_rows'], 'numeric')) { + $query['max_rows'] = (int) $_POST['session_max_rows']; + unset($_POST['session_max_rows']); + } elseif ($_POST['session_max_rows'] == self::ALL_ROWS) { + $query['max_rows'] = self::ALL_ROWS; + unset($_POST['session_max_rows']); + } elseif (empty($query['max_rows'])) { + $query['max_rows'] = intval($GLOBALS['cfg']['MaxRows']); + } + + if (Core::isValid($_REQUEST['pos'], 'numeric')) { + $query['pos'] = $_REQUEST['pos']; + unset($_REQUEST['pos']); + } elseif (empty($query['pos'])) { + $query['pos'] = 0; + } + + if (Core::isValid( + $_REQUEST['pftext'], + [ + self::DISPLAY_PARTIAL_TEXT, + self::DISPLAY_FULL_TEXT, + ] + ) + ) { + $query['pftext'] = $_REQUEST['pftext']; + unset($_REQUEST['pftext']); + } elseif (empty($query['pftext'])) { + $query['pftext'] = self::DISPLAY_PARTIAL_TEXT; + } + + if (Core::isValid( + $_REQUEST['relational_display'], + [ + self::RELATIONAL_KEY, + self::RELATIONAL_DISPLAY_COLUMN, + ] + ) + ) { + $query['relational_display'] = $_REQUEST['relational_display']; + unset($_REQUEST['relational_display']); + } elseif (empty($query['relational_display'])) { + // The current session value has priority over a + // change via Settings; this change will be apparent + // starting from the next session + $query['relational_display'] = $GLOBALS['cfg']['RelationalDisplay']; + } + + if (Core::isValid( + $_REQUEST['geoOption'], + [ + self::GEOMETRY_DISP_WKT, + self::GEOMETRY_DISP_WKB, + self::GEOMETRY_DISP_GEOM, + ] + ) + ) { + $query['geoOption'] = $_REQUEST['geoOption']; + unset($_REQUEST['geoOption']); + } elseif (empty($query['geoOption'])) { + $query['geoOption'] = self::GEOMETRY_DISP_GEOM; + } + + if (isset($_REQUEST['display_binary'])) { + $query['display_binary'] = true; + unset($_REQUEST['display_binary']); + } elseif (isset($_REQUEST['display_options_form'])) { + // we know that the checkbox was unchecked + unset($query['display_binary']); + } elseif (! isset($_REQUEST['full_text_button'])) { + // selected by default because some operations like OPTIMIZE TABLE + // and all queries involving functions return "binary" contents, + // according to low-level field flags + $query['display_binary'] = true; + } + + if (isset($_REQUEST['display_blob'])) { + $query['display_blob'] = true; + unset($_REQUEST['display_blob']); + } elseif (isset($_REQUEST['display_options_form'])) { + // we know that the checkbox was unchecked + unset($query['display_blob']); + } + + if (isset($_REQUEST['hide_transformation'])) { + $query['hide_transformation'] = true; + unset($_REQUEST['hide_transformation']); + } elseif (isset($_REQUEST['display_options_form'])) { + // we know that the checkbox was unchecked + unset($query['hide_transformation']); + } + + // move current query to the last position, to be removed last + // so only least executed query will be removed if maximum remembered + // queries limit is reached + unset($_SESSION['tmpval']['query'][$sql_md5]); + $_SESSION['tmpval']['query'][$sql_md5] = $query; + + // do not exceed a maximum number of queries to remember + if (count($_SESSION['tmpval']['query']) > 10) { + array_shift($_SESSION['tmpval']['query']); + //echo 'deleting one element ...'; + } + + // populate query configuration + $_SESSION['tmpval']['pftext'] + = $query['pftext']; + $_SESSION['tmpval']['relational_display'] + = $query['relational_display']; + $_SESSION['tmpval']['geoOption'] + = $query['geoOption']; + $_SESSION['tmpval']['display_binary'] = isset( + $query['display_binary'] + ); + $_SESSION['tmpval']['display_blob'] = isset( + $query['display_blob'] + ); + $_SESSION['tmpval']['hide_transformation'] = isset( + $query['hide_transformation'] + ); + $_SESSION['tmpval']['pos'] + = $query['pos']; + $_SESSION['tmpval']['max_rows'] + = $query['max_rows']; + $_SESSION['tmpval']['repeat_cells'] + = $query['repeat_cells']; + } + + /** + * Prepare a table of results returned by a SQL query. + * + * @param integer $dt_result the link id associated to the query + * which results have to be displayed + * @param array $displayParts the parts to display + * @param array $analyzed_sql_results analyzed sql results + * @param boolean $is_limited_display With limited operations or not + * + * @return string Generated HTML content for resulted table + * + * @access public + * + * @see sql.php file + */ + public function getTable( + &$dt_result, + array &$displayParts, + array $analyzed_sql_results, + $is_limited_display = false + ) { + /** + * The statement this table is built for. + * @var SelectStatement + */ + if (isset($analyzed_sql_results['statement'])) { + $statement = $analyzed_sql_results['statement']; + } else { + $statement = null; + } + + // Following variable are needed for use in isset/empty or + // use with array indexes/safe use in foreach + $fields_meta = $this->__get('fields_meta'); + $showtable = $this->__get('showtable'); + $printview = $this->__get('printview'); + + /** + * @todo move this to a central place + * @todo for other future table types + */ + $is_innodb = (isset($showtable['Type']) + && $showtable['Type'] == self::TABLE_TYPE_INNO_DB); + + $sql = new Sql(); + if ($is_innodb && $sql->isJustBrowsing($analyzed_sql_results, true)) { + $pre_count = '~'; + $after_count = Util::showHint( + Sanitize::sanitizeMessage( + __('May be approximate. See [doc@faq3-11]FAQ 3.11[/doc].') + ) + ); + } else { + $pre_count = ''; + $after_count = ''; + } + + // 1. ----- Prepares the work ----- + + // 1.1 Gets the information about which functionalities should be + // displayed + + list( + $displayParts, + $total + ) = $this->_setDisplayPartsAndTotal($displayParts); + + // 1.2 Defines offsets for the next and previous pages + if ($displayParts['nav_bar'] == '1') { + list($pos_next, $pos_prev) = $this->_getOffsets(); + } // end if + + // 1.3 Extract sorting expressions. + // we need $sort_expression and $sort_expression_nodirection + // even if there are many table references + $sort_expression = []; + $sort_expression_nodirection = []; + $sort_direction = []; + + if ($statement !== null && ! empty($statement->order)) { + foreach ($statement->order as $o) { + $sort_expression[] = $o->expr->expr . ' ' . $o->type; + $sort_expression_nodirection[] = $o->expr->expr; + $sort_direction[] = $o->type; + } + } else { + $sort_expression[] = ''; + $sort_expression_nodirection[] = ''; + $sort_direction[] = ''; + } + + $number_of_columns = count($sort_expression_nodirection); + + // 1.4 Prepares display of first and last value of the sorted column + $sorted_column_message = ''; + for ($i = 0; $i < $number_of_columns; $i++) { + $sorted_column_message .= $this->_getSortedColumnMessage( + $dt_result, + $sort_expression_nodirection[$i] + ); + } + + // 2. ----- Prepare to display the top of the page ----- + + // 2.1 Prepares a messages with position information + $sqlQueryMessage = ''; + if (($displayParts['nav_bar'] == '1') && isset($pos_next)) { + $message = $this->_setMessageInformation( + $sorted_column_message, + $analyzed_sql_results, + $total, + $pos_next, + $pre_count, + $after_count + ); + + $sqlQueryMessage = Util::getMessage( + $message, + $this->__get('sql_query'), + 'success' + ); + } elseif ((! isset($printview) || ($printview != '1')) && ! $is_limited_display) { + $sqlQueryMessage = Util::getMessage( + __('Your SQL query has been executed successfully.'), + $this->__get('sql_query'), + 'success' + ); + } + + // 2.3 Prepare the navigation bars + if (strlen($this->__get('table')) === 0) { + if ($analyzed_sql_results['querytype'] == 'SELECT') { + // table does not always contain a real table name, + // for example in MySQL 5.0.x, the query SHOW STATUS + // returns STATUS as a table name + $this->__set('table', $fields_meta[0]->table); + } else { + $this->__set('table', ''); + } + } + + // can the result be sorted? + if ($displayParts['sort_lnk'] == '1' && $analyzed_sql_results['statement'] !== null) { + // At this point, $sort_expression is an array + list($unsorted_sql_query, $sort_by_key_html) + = $this->_getUnsortedSqlAndSortByKeyDropDown( + $analyzed_sql_results, + $sort_expression + ); + } else { + $sort_by_key_html = $unsorted_sql_query = ''; + } + + $navigation = ''; + if ($displayParts['nav_bar'] == '1' && $statement !== null && empty($statement->limit)) { + $navigation = $this->_getTableNavigation( + $pos_next, + $pos_prev, + $is_innodb, + $sort_by_key_html + ); + } + + // 2b ----- Get field references from Database ----- + // (see the 'relation' configuration variable) + + // initialize map + $map = []; + + if (strlen($this->__get('table')) > 0) { + // This method set the values for $map array + $this->_setParamForLinkForeignKeyRelatedTables($map); + + // Coming from 'Distinct values' action of structure page + // We manipulate relations mechanism to show a link to related rows. + if ($this->__get('is_browse_distinct')) { + $map[$fields_meta[1]->name] = [ + $this->__get('table'), + $fields_meta[1]->name, + '', + $this->__get('db'), + ]; + } + } // end if + // end 2b + + // 3. ----- Prepare the results table ----- + $headers = $this->_getTableHeaders( + $displayParts, + $analyzed_sql_results, + $unsorted_sql_query, + $sort_expression, + $sort_expression_nodirection, + $sort_direction, + $is_limited_display + ); + + $body = $this->_getTableBody( + $dt_result, + $displayParts, + $map, + $analyzed_sql_results, + $is_limited_display + ); + + $this->__set('display_params', null); + + // 4. ----- Prepares the link for multi-fields edit and delete + $multiRowOperationLinks = ''; + if ($displayParts['del_lnk'] == self::DELETE_ROW + && $displayParts['del_lnk'] != self::KILL_PROCESS + ) { + $multiRowOperationLinks = $this->_getMultiRowOperationLinks( + $dt_result, + $analyzed_sql_results, + $displayParts['del_lnk'] + ); + } + + // 5. ----- Prepare "Query results operations" + $operations = ''; + if ((! isset($printview) || ($printview != '1')) && ! $is_limited_display) { + $operations = $this->_getResultsOperations( + $displayParts, + $analyzed_sql_results + ); + } + + return $this->template->render('display/results/table', [ + 'sql_query_message' => $sqlQueryMessage, + 'navigation' => $navigation, + 'headers' => $headers, + 'body' => $body, + 'multi_row_operation_links' => $multiRowOperationLinks, + 'operations' => $operations, + ]); + } + + /** + * Get offsets for next page and previous page + * + * @return array array with two elements - $pos_next, $pos_prev + * + * @access private + * + * @see getTable() + */ + private function _getOffsets() + { + + if ($_SESSION['tmpval']['max_rows'] == self::ALL_ROWS) { + $pos_next = 0; + $pos_prev = 0; + } else { + $pos_next = $_SESSION['tmpval']['pos'] + + $_SESSION['tmpval']['max_rows']; + + $pos_prev = $_SESSION['tmpval']['pos'] + - $_SESSION['tmpval']['max_rows']; + + if ($pos_prev < 0) { + $pos_prev = 0; + } + } + + return [ + $pos_next, + $pos_prev, + ]; + } + + /** + * Prepare sorted column message + * + * @param integer $dt_result the link id associated to the + * query which results have to + * be displayed + * @param string $sort_expression_nodirection sort expression without direction + * + * @return string|null html content, null if not found sorted column + * + * @access private + * + * @see getTable() + */ + private function _getSortedColumnMessage( + &$dt_result, + $sort_expression_nodirection + ) { + $fields_meta = $this->__get('fields_meta'); // To use array indexes + + if (empty($sort_expression_nodirection)) { + return null; + } + + if (mb_strpos($sort_expression_nodirection, '.') === false) { + $sort_table = $this->__get('table'); + $sort_column = $sort_expression_nodirection; + } else { + list($sort_table, $sort_column) + = explode('.', $sort_expression_nodirection); + } + + $sort_table = Util::unQuote($sort_table); + $sort_column = Util::unQuote($sort_column); + + // find the sorted column index in row result + // (this might be a multi-table query) + $sorted_column_index = false; + + foreach ($fields_meta as $key => $meta) { + if (($meta->table == $sort_table) && ($meta->name == $sort_column)) { + $sorted_column_index = $key; + break; + } + } + + if ($sorted_column_index === false) { + return null; + } + + // fetch first row of the result set + $row = $GLOBALS['dbi']->fetchRow($dt_result); + + // initializing default arguments + $default_function = [ + Core::class, + 'mimeDefaultFunction', + ]; + $transformation_plugin = $default_function; + $transform_options = []; + + // check for non printable sorted row data + $meta = $fields_meta[$sorted_column_index]; + + if (false !== stripos($meta->type, self::BLOB_FIELD) + || ($meta->type == self::GEOMETRY_FIELD) + ) { + $column_for_first_row = $this->_handleNonPrintableContents( + $meta->type, + $row[$sorted_column_index], + $transformation_plugin, + $transform_options, + $default_function, + $meta + ); + } else { + $column_for_first_row = $row[$sorted_column_index]; + } + + $column_for_first_row = mb_strtoupper( + mb_substr( + (string) $column_for_first_row, + 0, + (int) $GLOBALS['cfg']['LimitChars'] + ) . '...' + ); + + // fetch last row of the result set + $GLOBALS['dbi']->dataSeek($dt_result, $this->__get('num_rows') - 1); + $row = $GLOBALS['dbi']->fetchRow($dt_result); + + // check for non printable sorted row data + $meta = $fields_meta[$sorted_column_index]; + if (false !== stripos($meta->type, self::BLOB_FIELD) + || ($meta->type == self::GEOMETRY_FIELD) + ) { + $column_for_last_row = $this->_handleNonPrintableContents( + $meta->type, + $row[$sorted_column_index], + $transformation_plugin, + $transform_options, + $default_function, + $meta + ); + } else { + $column_for_last_row = $row[$sorted_column_index]; + } + + $column_for_last_row = mb_strtoupper( + mb_substr( + (string) $column_for_last_row, + 0, + (int) $GLOBALS['cfg']['LimitChars'] + ) . '...' + ); + + // reset to first row for the loop in _getTableBody() + $GLOBALS['dbi']->dataSeek($dt_result, 0); + + // we could also use here $sort_expression_nodirection + return ' [' . htmlspecialchars($sort_column) + . ': ' . htmlspecialchars($column_for_first_row) . ' - ' + . htmlspecialchars($column_for_last_row) . ']'; + } + + /** + * Set the content that needs to be shown in message + * + * @param string $sorted_column_message the message for sorted column + * @param array $analyzed_sql_results the analyzed query + * @param integer $total the total number of rows returned by + * the SQL query without any + * programmatically appended LIMIT clause + * @param integer $pos_next the offset for next page + * @param string $pre_count the string renders before row count + * @param string $after_count the string renders after row count + * + * @return Message an object of Message + * + * @access private + * + * @see getTable() + */ + private function _setMessageInformation( + $sorted_column_message, + array $analyzed_sql_results, + $total, + $pos_next, + $pre_count, + $after_count + ) { + + $unlim_num_rows = $this->__get('unlim_num_rows'); // To use in isset() + + if (! empty($analyzed_sql_results['statement']->limit)) { + $first_shown_rec = $analyzed_sql_results['statement']->limit->offset; + $row_count = $analyzed_sql_results['statement']->limit->rowCount; + + if ($row_count < $total) { + $last_shown_rec = $first_shown_rec + $row_count - 1; + } else { + $last_shown_rec = $first_shown_rec + $total - 1; + } + } elseif (($_SESSION['tmpval']['max_rows'] == self::ALL_ROWS) + || ($pos_next > $total) + ) { + $first_shown_rec = $_SESSION['tmpval']['pos']; + $last_shown_rec = $total - 1; + } else { + $first_shown_rec = $_SESSION['tmpval']['pos']; + $last_shown_rec = $pos_next - 1; + } + + $table = new Table($this->__get('table'), $this->__get('db')); + if ($table->isView() + && ($total == $GLOBALS['cfg']['MaxExactCountViews']) + ) { + $message = Message::notice( + __( + 'This view has at least this number of rows. ' + . 'Please refer to %sdocumentation%s.' + ) + ); + + $message->addParam('[doc@cfg_MaxExactCount]'); + $message->addParam('[/doc]'); + $message_view_warning = Util::showHint($message); + } else { + $message_view_warning = false; + } + + $message = Message::success(__('Showing rows %1s - %2s')); + $message->addParam($first_shown_rec); + + if ($message_view_warning !== false) { + $message->addParamHtml('... ' . $message_view_warning); + } else { + $message->addParam($last_shown_rec); + } + + $message->addText('('); + + if ($message_view_warning === false) { + if (isset($unlim_num_rows) && ($unlim_num_rows != $total)) { + $message_total = Message::notice( + $pre_count . __('%1$d total, %2$d in query') + ); + $message_total->addParam($total); + $message_total->addParam($unlim_num_rows); + } else { + $message_total = Message::notice($pre_count . __('%d total')); + $message_total->addParam($total); + } + + if (! empty($after_count)) { + $message_total->addHtml($after_count); + } + $message->addMessage($message_total, ''); + + $message->addText(', ', ''); + } + + $message_qt = Message::notice(__('Query took %01.4f seconds.') . ')'); + $message_qt->addParam($this->__get('querytime')); + + $message->addMessage($message_qt, ''); + if ($sorted_column_message !== null) { + $message->addHtml($sorted_column_message, ''); + } + + return $message; + } + + /** + * Set the value of $map array for linking foreign key related tables + * + * @param array $map the list of relations + * + * @return void + * + * @access private + * + * @see getTable() + */ + private function _setParamForLinkForeignKeyRelatedTables(array &$map) + { + // To be able to later display a link to the related table, + // we verify both types of relations: either those that are + // native foreign keys or those defined in the phpMyAdmin + // configuration storage. If no PMA storage, we won't be able + // to use the "column to display" notion (for example show + // the name related to a numeric id). + $exist_rel = $this->relation->getForeigners( + $this->__get('db'), + $this->__get('table'), + '', + self::POSITION_BOTH + ); + + if (! empty($exist_rel)) { + foreach ($exist_rel as $master_field => $rel) { + if ($master_field != 'foreign_keys_data') { + $display_field = $this->relation->getDisplayField( + $rel['foreign_db'], + $rel['foreign_table'] + ); + $map[$master_field] = [ + $rel['foreign_table'], + $rel['foreign_field'], + $display_field, + $rel['foreign_db'], + ]; + } else { + foreach ($rel as $key => $one_key) { + foreach ($one_key['index_list'] as $index => $one_field) { + $display_field = $this->relation->getDisplayField( + isset($one_key['ref_db_name']) + ? $one_key['ref_db_name'] + : $GLOBALS['db'], + $one_key['ref_table_name'] + ); + + $map[$one_field] = [ + $one_key['ref_table_name'], + $one_key['ref_index_list'][$index], + $display_field, + isset($one_key['ref_db_name']) + ? $one_key['ref_db_name'] + : $GLOBALS['db'], + ]; + } + } + } + } + } + } + + /** + * Prepare multi field edit/delete links + * + * @param integer $dt_result the link id associated to the query which + * results have to be displayed + * @param array $analyzed_sql_results analyzed sql results + * @param string $del_link the display element - 'del_link' + * + * @return string html content + * + * @access private + * + * @see getTable() + */ + private function _getMultiRowOperationLinks( + &$dt_result, + array $analyzed_sql_results, + $del_link + ) { + $links_html = '\n"; + + $links_html .= '' + . "\n"; + + if (! empty($url_query)) { + $links_html .= '' . "\n"; + } + + // fetch last row of the result set + $GLOBALS['dbi']->dataSeek($dt_result, $this->__get('num_rows') - 1); + $row = $GLOBALS['dbi']->fetchRow($dt_result); + + // @see DbiMysqi::fetchRow & DatabaseInterface::fetchRow + if (! is_array($row)) { + $row = []; + } + + // $clause_is_unique is needed by getTable() to generate the proper param + // in the multi-edit and multi-delete form + list($where_clause, $clause_is_unique, $condition_array) + = Util::getUniqueCondition( + $dt_result, // handle + $this->__get('fields_cnt'), // fields_cnt + $this->__get('fields_meta'), // fields_meta + $row, // row + false, // force_unique + false, // restrict_to_table + $analyzed_sql_results // analyzed_sql_results + ); + unset($where_clause, $condition_array); + + // reset to first row for the loop in _getTableBody() + $GLOBALS['dbi']->dataSeek($dt_result, 0); + + $links_html .= '' . "\n"; + + $links_html .= '' . "\n"; + + return $links_html; + } + + /** + * Generates HTML to display the Create view in span tag + * + * @param array $analyzed_sql_results analyzed sql results + * @param string $url_query String with URL Parameters + * + * @return string + * + * @access private + * + * @see _getResultsOperations() + */ + private function _getLinkForCreateView(array $analyzed_sql_results, $url_query) + { + $results_operations_html = ''; + if (empty($analyzed_sql_results['procedure'])) { + $results_operations_html .= '' + . Util::linkOrButton( + 'view_create.php' . $url_query, + Util::getIcon( + 'b_view_add', + __('Create view'), + true + ), + ['class' => 'create_view ajax'] + ) + . '' . "\n"; + } + return $results_operations_html; + } + + /** + * Calls the _getResultsOperations with $only_view as true + * + * @param array $analyzed_sql_results analyzed sql results + * + * @return string + * + * @access public + * + */ + public function getCreateViewQueryResultOp(array $analyzed_sql_results) + { + $results_operations_html = ''; + //calling to _getResultOperations with a fake $displayParts + //and setting only_view parameter to be true to generate just view + $results_operations_html .= $this->_getResultsOperations( + [], + $analyzed_sql_results, + true + ); + return $results_operations_html; + } + + /** + * Get copy to clipboard links for results operations + * + * @return string + * + * @access private + */ + private function _getCopytoclipboardLinks() + { + return Util::linkOrButton( + '#', + Util::getIcon( + 'b_insrow', + __('Copy to clipboard'), + true + ), + ['id' => 'copyToClipBoard'] + ); + } + + /** + * Get printview links for results operations + * + * @return string + * + * @access private + */ + private function _getPrintviewLinks() + { + return Util::linkOrButton( + '#', + Util::getIcon( + 'b_print', + __('Print'), + true + ), + ['id' => 'printView'], + 'print_view' + ); + } + + /** + * Get operations that are available on results. + * + * @param array $displayParts the parts to display + * @param array $analyzed_sql_results analyzed sql results + * @param boolean $only_view Whether to show only view + * + * @return string html content + * + * @access private + * + * @see getTable() + */ + private function _getResultsOperations( + array $displayParts, + array $analyzed_sql_results, + $only_view = false + ) { + global $printview; + + $results_operations_html = ''; + $fields_meta = $this->__get('fields_meta'); // To safe use in foreach + $header_shown = false; + $header = '
'; + } + return $results_operations_html; + } + + // Displays "printable view" link if required + if ($displayParts['pview_lnk'] == '1') { + $results_operations_html .= $this->_getPrintviewLinks(); + $results_operations_html .= $this->_getCopytoclipboardLinks(); + } // end displays "printable view" + + // Export link + // (the url_query has extra parameters that won't be used to export) + // (the single_table parameter is used in \PhpMyAdmin\Export->getDisplay() + // to hide the SQL and the structure export dialogs) + // If the parser found a PROCEDURE clause + // (most probably PROCEDURE ANALYSE()) it makes no sense to + // display the Export link). + if (($analyzed_sql_results['querytype'] == self::QUERY_TYPE_SELECT) + && ! isset($printview) + && empty($analyzed_sql_results['procedure']) + ) { + if (count($analyzed_sql_results['select_tables']) === 1) { + $_url_params['single_table'] = 'true'; + } + + if (! $header_shown) { + $results_operations_html .= $header; + $header_shown = true; + } + + $_url_params['unlim_num_rows'] = $this->__get('unlim_num_rows'); + + /** + * At this point we don't know the table name; this can happen + * for example with a query like + * SELECT bike_code FROM (SELECT bike_code FROM bikes) tmp + * As a workaround we set in the table parameter the name of the + * first table of this database, so that tbl_export.php and + * the script it calls do not fail + */ + if (empty($_url_params['table']) && ! empty($_url_params['db'])) { + $_url_params['table'] = $GLOBALS['dbi']->fetchValue("SHOW TABLES"); + /* No result (probably no database selected) */ + if ($_url_params['table'] === false) { + unset($_url_params['table']); + } + } + + $results_operations_html .= Util::linkOrButton( + 'tbl_export.php' . Url::getCommon($_url_params), + Util::getIcon( + 'b_tblexport', + __('Export'), + true + ) + ) + . "\n"; + + // prepare chart + $results_operations_html .= Util::linkOrButton( + 'tbl_chart.php' . Url::getCommon($_url_params), + Util::getIcon( + 'b_chart', + __('Display chart'), + true + ) + ) + . "\n"; + + // prepare GIS chart + $geometry_found = false; + // If at least one geometry field is found + foreach ($fields_meta as $meta) { + if ($meta->type == self::GEOMETRY_FIELD) { + $geometry_found = true; + break; + } + } + + if ($geometry_found) { + $results_operations_html + .= Util::linkOrButton( + 'tbl_gis_visualization.php' + . Url::getCommon($_url_params), + Util::getIcon( + 'b_globe', + __('Visualize GIS data'), + true + ) + ) + . "\n"; + } + } + + // CREATE VIEW + /** + * + * @todo detect privileges to create a view + * (but see 2006-01-19 note in PhpMyAdmin\Display\CreateTable, + * I think we cannot detect db-specific privileges reliably) + * Note: we don't display a Create view link if we found a PROCEDURE clause + */ + if (! $header_shown) { + $results_operations_html .= $header; + $header_shown = true; + } + + $results_operations_html .= $this->_getLinkForCreateView( + $analyzed_sql_results, + $url_query + ); + + if ($header_shown) { + $results_operations_html .= '
'; + } + + return $results_operations_html; + } + + /** + * Verifies what to do with non-printable contents (binary or BLOB) + * in Browse mode. + * + * @param string $category BLOB|BINARY|GEOMETRY + * @param string|null $content the binary content + * @param mixed $transformation_plugin transformation plugin. + * Can also be the + * default function: + * Core::mimeDefaultFunction + * @param string $transform_options transformation parameters + * @param string $default_function default transformation function + * @param stdClass $meta the meta-information about the field + * @param array $url_params parameters that should go to the + * download link + * @param boolean $is_truncated the result is truncated or not + * + * @return mixed string or float + * + * @access private + * + * @see _getDataCellForGeometryColumns(), + * _getDataCellForNonNumericColumns(), + * _getSortedColumnMessage() + */ + private function _handleNonPrintableContents( + $category, + ?string $content, + $transformation_plugin, + $transform_options, + $default_function, + $meta, + array $url_params = [], + &$is_truncated = null + ) { + $is_truncated = false; + $result = '[' . $category; + + if (isset($content)) { + $size = strlen($content); + $display_size = Util::formatByteDown($size, 3, 1); + $result .= ' - ' . $display_size[0] . ' ' . $display_size[1]; + } else { + $result .= ' - NULL'; + $size = 0; + } + + $result .= ']'; + + // if we want to use a text transformation on a BLOB column + if (gettype($transformation_plugin) === "object") { + $posMimeOctetstream = strpos( + $transformation_plugin->getMIMESubtype(), + 'Octetstream' + ); + $posMimeText = strpos($transformation_plugin->getMIMEtype(), 'Text'); + if ($posMimeOctetstream + || $posMimeText !== false + ) { + // Applying Transformations on hex string of binary data + // seems more appropriate + $result = pack("H*", bin2hex($content)); + } + } + + if ($size <= 0) { + return $result; + } + + if ($default_function != $transformation_plugin) { + $result = $transformation_plugin->applyTransformation( + $result, + $transform_options, + $meta + ); + return $result; + } + + $result = $default_function($result, [], $meta); + if (($_SESSION['tmpval']['display_binary'] + && $meta->type === self::STRING_FIELD) + || ($_SESSION['tmpval']['display_blob'] + && false !== stripos($meta->type, self::BLOB_FIELD)) + ) { + // in this case, restart from the original $content + if (mb_check_encoding($content, 'utf-8') + && ! preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]/u', $content) + ) { + // show as text if it's valid utf-8 + $result = htmlspecialchars($content); + } else { + $result = '0x' . bin2hex($content); + } + list( + $is_truncated, + $result, + // skip 3rd param + ) = $this->_getPartialText($result); + } + + /* Create link to download */ + + // in PHP < 5.5, empty() only checks variables + $tmpdb = $this->__get('db'); + if (count($url_params) > 0 + && (! empty($tmpdb) && ! empty($meta->orgtable)) + ) { + $result = '
' + . $result . ''; + } + + return $result; + } + + /** + * Retrieves the associated foreign key info for a data cell + * + * @param array $map the list of relations + * @param stdClass $meta the meta-information about the field + * @param string $where_comparison data for the where clause + * + * @return string formatted data + * + * @access private + * + */ + private function _getFromForeign(array $map, $meta, $where_comparison) + { + $dispsql = 'SELECT ' + . Util::backquote($map[$meta->name][2]) + . ' FROM ' + . Util::backquote($map[$meta->name][3]) + . '.' + . Util::backquote($map[$meta->name][0]) + . ' WHERE ' + . Util::backquote($map[$meta->name][1]) + . $where_comparison; + + $dispresult = $GLOBALS['dbi']->tryQuery( + $dispsql, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + + if ($dispresult && $GLOBALS['dbi']->numRows($dispresult) > 0) { + list($dispval) = $GLOBALS['dbi']->fetchRow($dispresult, 0); + } else { + $dispval = __('Link not found!'); + } + + $GLOBALS['dbi']->freeResult($dispresult); + + return $dispval; + } + + /** + * Prepares the displayable content of a data cell in Browse mode, + * taking into account foreign key description field and transformations + * + * @param string $class css classes for the td element + * @param bool $condition_field whether the column is a part of + * the where clause + * @param array $analyzed_sql_results the analyzed query + * @param stdClass $meta the meta-information about the + * field + * @param array $map the list of relations + * @param string $data data + * @param string $displayedData data that will be displayed (maybe be chunked) + * @param TransformationsPlugin $transformation_plugin transformation plugin. + * Can also be the default function: + * Core::mimeDefaultFunction + * @param string $default_function default function + * @param string $nowrap 'nowrap' if the content should + * not be wrapped + * @param string $where_comparison data for the where clause + * @param array $transform_options options for transformation + * @param bool $is_field_truncated whether the field is truncated + * @param string $original_length of a truncated column, or '' + * + * @return string formatted data + * + * @access private + * + * @see _getDataCellForNumericColumns(), _getDataCellForGeometryColumns(), + * _getDataCellForNonNumericColumns(), + * + */ + private function _getRowData( + $class, + $condition_field, + array $analyzed_sql_results, + $meta, + array $map, + $data, + $displayedData, + $transformation_plugin, + $default_function, + $nowrap, + $where_comparison, + array $transform_options, + $is_field_truncated, + $original_length = '' + ) { + $relational_display = $_SESSION['tmpval']['relational_display']; + $printview = $this->__get('printview'); + $decimals = isset($meta->decimals) ? $meta->decimals : '-1'; + $result = '_addClass( + $class, + $condition_field, + $meta, + $nowrap, + $is_field_truncated, + $transformation_plugin, + $default_function + ) + . '">'; + + if (! empty($analyzed_sql_results['statement']->expr)) { + foreach ($analyzed_sql_results['statement']->expr as $expr) { + if (empty($expr->alias) || empty($expr->column)) { + continue; + } + if (strcasecmp($meta->name, $expr->alias) == 0) { + $meta->name = $expr->column; + } + } + } + + if (isset($map[$meta->name])) { + // Field to display from the foreign table? + if (isset($map[$meta->name][2]) + && strlen((string) $map[$meta->name][2]) > 0 + ) { + $dispval = $this->_getFromForeign( + $map, + $meta, + $where_comparison + ); + } else { + $dispval = ''; + } // end if... else... + + if (isset($printview) && ($printview == '1')) { + $result .= ($transformation_plugin != $default_function + ? $transformation_plugin->applyTransformation( + $data, + $transform_options, + $meta + ) + : $default_function($data) + ) + . ' [->' . $dispval . ']'; + } else { + if ($relational_display == self::RELATIONAL_KEY) { + // user chose "relational key" in the display options, so + // the title contains the display field + $title = ! empty($dispval) + ? htmlspecialchars($dispval) + : ''; + } else { + $title = htmlspecialchars($data); + } + + $sqlQuery = 'SELECT * FROM ' + . Util::backquote($map[$meta->name][3]) . '.' + . Util::backquote($map[$meta->name][0]) + . ' WHERE ' + . Util::backquote($map[$meta->name][1]) + . $where_comparison; + + $_url_params = [ + 'db' => $map[$meta->name][3], + 'table' => $map[$meta->name][0], + 'pos' => '0', + 'sql_signature' => Core::signSqlQuery($sqlQuery), + 'sql_query' => $sqlQuery, + ]; + + if ($transformation_plugin != $default_function) { + // always apply a transformation on the real data, + // not on the display field + $displayedData = $transformation_plugin->applyTransformation( + $data, + $transform_options, + $meta + ); + } else { + if ($relational_display == self::RELATIONAL_DISPLAY_COLUMN + && ! empty($map[$meta->name][2]) + ) { + // user chose "relational display field" in the + // display options, so show display field in the cell + $displayedData = $default_function($dispval); + } else { + // otherwise display data in the cell + $displayedData = $default_function($displayedData); + } + } + + $tag_params = ['title' => $title]; + if (strpos($class, 'grid_edit') !== false) { + $tag_params['class'] = 'ajax'; + } + $result .= Util::linkOrButton( + 'sql.php' . Url::getCommon($_url_params), + $displayedData, + $tag_params + ); + } + } else { + $result .= ($transformation_plugin != $default_function + ? $transformation_plugin->applyTransformation( + $data, + $transform_options, + $meta + ) + : $default_function($data) + ); + } + + $result .= '' . "\n"; + + return $result; + } + + /** + * Prepares a checkbox for multi-row submits + * + * @param string $del_url delete url + * @param array $displayParts array with explicit indexes for all + * the display elements + * @param string $row_no the row number + * @param string $where_clause_html url encoded where clause + * @param array $condition_array array of conditions in the where clause + * @param string $id_suffix suffix for the id + * @param string $class css classes for the td element + * + * @return string the generated HTML + * + * @access private + * + * @see _getTableBody(), _getCheckboxAndLinks() + */ + private function _getCheckboxForMultiRowSubmissions( + $del_url, + array $displayParts, + $row_no, + $where_clause_html, + array $condition_array, + $id_suffix, + $class + ) { + $ret = ''; + + if (! empty($del_url) && $displayParts['del_lnk'] != self::KILL_PROCESS) { + $ret .= '' + . '' + . ' '; + } + + return $ret; + } + + /** + * Prepares an Edit link + * + * @param string $edit_url edit url + * @param string $class css classes for td element + * @param string $edit_str text for the edit link + * @param string $where_clause where clause + * @param string $where_clause_html url encoded where clause + * + * @return string the generated HTML + * + * @access private + * + * @see _getTableBody(), _getCheckboxAndLinks() + */ + private function _getEditLink( + $edit_url, + $class, + $edit_str, + $where_clause, + $where_clause_html + ) { + $ret = ''; + if (! empty($edit_url)) { + $ret .= '' + . '' + . Util::linkOrButton($edit_url, $edit_str); + /* + * Where clause for selecting this row uniquely is provided as + * a hidden input. Used by jQuery scripts for handling grid editing + */ + if (! empty($where_clause)) { + $ret .= ''; + } + $ret .= ''; + } + + return $ret; + } + + /** + * Prepares an Copy link + * + * @param string $copy_url copy url + * @param string $copy_str text for the copy link + * @param string $where_clause where clause + * @param string $where_clause_html url encoded where clause + * @param string $class css classes for the td element + * + * @return string the generated HTML + * + * @access private + * + * @see _getTableBody(), _getCheckboxAndLinks() + */ + private function _getCopyLink( + $copy_url, + $copy_str, + $where_clause, + $where_clause_html, + $class + ) { + $ret = ''; + if (! empty($copy_url)) { + $ret .= '' + . Util::linkOrButton($copy_url, $copy_str); + + /* + * Where clause for selecting this row uniquely is provided as + * a hidden input. Used by jQuery scripts for handling grid editing + */ + if (! empty($where_clause)) { + $ret .= ''; + } + $ret .= ''; + } + + return $ret; + } + + /** + * Prepares a Delete link + * + * @param string $del_url delete url + * @param string $del_str text for the delete link + * @param string $js_conf text for the JS confirmation + * @param string $class css classes for the td element + * + * @return string the generated HTML + * + * @access private + * + * @see _getTableBody(), _getCheckboxAndLinks() + */ + private function _getDeleteLink($del_url, $del_str, $js_conf, $class) + { + + $ret = ''; + if (empty($del_url)) { + return $ret; + } + + $ret .= '' + . Util::linkOrButton( + $del_url, + $del_str, + ['class' => 'delete_row requireConfirm' . $ajax] + ) + . '
' . $js_conf . '
' + . ''; + + return $ret; + } + + /** + * Prepare checkbox and links at some position (left or right) + * (only called for horizontal mode) + * + * @param string $position the position of the checkbox and links + * @param string $del_url delete url + * @param array $displayParts array with explicit indexes for all the + * display elements + * @param string $row_no row number + * @param string $where_clause where clause + * @param string $where_clause_html url encoded where clause + * @param array $condition_array array of conditions in the where clause + * @param string $edit_url edit url + * @param string $copy_url copy url + * @param string $class css classes for the td elements + * @param string $edit_str text for the edit link + * @param string $copy_str text for the copy link + * @param string $del_str text for the delete link + * @param string $js_conf text for the JS confirmation + * + * @return string the generated HTML + * + * @access private + * + * @see _getPlacedLinks() + */ + private function _getCheckboxAndLinks( + $position, + $del_url, + array $displayParts, + $row_no, + $where_clause, + $where_clause_html, + array $condition_array, + $edit_url, + $copy_url, + $class, + $edit_str, + $copy_str, + $del_str, + $js_conf + ) { + $ret = ''; + + if ($position == self::POSITION_LEFT) { + $ret .= $this->_getCheckboxForMultiRowSubmissions( + $del_url, + $displayParts, + $row_no, + $where_clause_html, + $condition_array, + '_left', + '' + ); + + $ret .= $this->_getEditLink( + $edit_url, + $class, + $edit_str, + $where_clause, + $where_clause_html + ); + + $ret .= $this->_getCopyLink( + $copy_url, + $copy_str, + $where_clause, + $where_clause_html, + '' + ); + + $ret .= $this->_getDeleteLink($del_url, $del_str, $js_conf, ''); + } elseif ($position == self::POSITION_RIGHT) { + $ret .= $this->_getDeleteLink($del_url, $del_str, $js_conf, ''); + + $ret .= $this->_getCopyLink( + $copy_url, + $copy_str, + $where_clause, + $where_clause_html, + '' + ); + + $ret .= $this->_getEditLink( + $edit_url, + $class, + $edit_str, + $where_clause, + $where_clause_html + ); + + $ret .= $this->_getCheckboxForMultiRowSubmissions( + $del_url, + $displayParts, + $row_no, + $where_clause_html, + $condition_array, + '_right', + '' + ); + } else { // $position == self::POSITION_NONE + $ret .= $this->_getCheckboxForMultiRowSubmissions( + $del_url, + $displayParts, + $row_no, + $where_clause_html, + $condition_array, + '_left', + '' + ); + } + + return $ret; + } + + /** + * Truncates given string based on LimitChars configuration + * and Session pftext variable + * (string is truncated only if necessary) + * + * @param string $str string to be truncated + * + * @return mixed + * + * @access private + * + * @see _handleNonPrintableContents(), _getDataCellForGeometryColumns(), + * _getDataCellForNonNumericColumns + */ + private function _getPartialText($str) + { + $original_length = mb_strlen($str); + if ($original_length > $GLOBALS['cfg']['LimitChars'] + && $_SESSION['tmpval']['pftext'] === self::DISPLAY_PARTIAL_TEXT + ) { + $str = mb_substr( + $str, + 0, + (int) $GLOBALS['cfg']['LimitChars'] + ) . '...'; + $truncated = true; + } else { + $truncated = false; + } + + return [ + $truncated, + $str, + $original_length, + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Encoding.php b/srcs/phpmyadmin/libraries/classes/Encoding.php new file mode 100644 index 0000000..32715f3 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Encoding.php @@ -0,0 +1,358 @@ + [ + 'iconv', + self::ENGINE_ICONV, + 'iconv', + ], + 'recode' => [ + 'recode_string', + self::ENGINE_RECODE, + 'recode', + ], + 'mb' => [ + 'mb_convert_encoding', + self::ENGINE_MB, + 'mbstring', + ], + 'none' => [ + 'isset', + self::ENGINE_NONE, + '', + ], + ]; + + /** + * Order of automatic detection of engines + * + * @var array + */ + private static $_engineorder = [ + 'iconv', + 'mb', + 'recode', + ]; + + /** + * Kanji encodings list + * + * @var string + */ + private static $_kanji_encodings = 'ASCII,SJIS,EUC-JP,JIS'; + + /** + * Initializes encoding engine detecting available backends. + * + * @return void + */ + public static function initEngine(): void + { + $engine = 'auto'; + if (isset($GLOBALS['cfg']['RecodingEngine'])) { + $engine = $GLOBALS['cfg']['RecodingEngine']; + } + + /* Use user configuration */ + if (isset(self::$_enginemap[$engine])) { + if (function_exists(self::$_enginemap[$engine][0])) { + self::$_engine = self::$_enginemap[$engine][1]; + return; + } else { + Core::warnMissingExtension(self::$_enginemap[$engine][2]); + } + } + + /* Autodetection */ + foreach (self::$_engineorder as $engine) { + if (function_exists(self::$_enginemap[$engine][0])) { + self::$_engine = self::$_enginemap[$engine][1]; + return; + } + } + + /* Fallback to none conversion */ + self::$_engine = self::ENGINE_NONE; + } + + /** + * Setter for engine. Use with caution, mostly useful for testing. + * + * @param int $engine Engine encoding + * + * @return void + */ + public static function setEngine(int $engine): void + { + self::$_engine = $engine; + } + + /** + * Checks whether there is any charset conversion supported + * + * @return bool + */ + public static function isSupported(): bool + { + if (self::$_engine === null) { + self::initEngine(); + } + return self::$_engine != self::ENGINE_NONE; + } + + /** + * Converts encoding of text according to parameters with detected + * conversion function. + * + * @param string $src_charset source charset + * @param string $dest_charset target charset + * @param string $what what to convert + * + * @return string converted text + * + * @access public + */ + public static function convertString( + string $src_charset, + string $dest_charset, + string $what + ): string { + if ($src_charset == $dest_charset) { + return $what; + } + if (self::$_engine === null) { + self::initEngine(); + } + switch (self::$_engine) { + case self::ENGINE_RECODE: + return recode_string( + $src_charset . '..' . $dest_charset, + $what + ); + case self::ENGINE_ICONV: + return iconv( + $src_charset, + $dest_charset . + (isset($GLOBALS['cfg']['IconvExtraParams']) ? $GLOBALS['cfg']['IconvExtraParams'] : ''), + $what + ); + case self::ENGINE_MB: + return mb_convert_encoding( + $what, + $dest_charset, + $src_charset + ); + default: + return $what; + } + } + + /** + * Detects whether Kanji encoding is available + * + * @return bool + */ + public static function canConvertKanji(): bool + { + return $GLOBALS['lang'] == 'ja'; + } + + /** + * Setter for Kanji encodings. Use with caution, mostly useful for testing. + * + * @return string + */ + public static function getKanjiEncodings(): string + { + return self::$_kanji_encodings; + } + + /** + * Setter for Kanji encodings. Use with caution, mostly useful for testing. + * + * @param string $value Kanji encodings list + * + * @return void + */ + public static function setKanjiEncodings(string $value): void + { + self::$_kanji_encodings = $value; + } + + /** + * Reverses SJIS & EUC-JP position in the encoding codes list + * + * @return void + */ + public static function kanjiChangeOrder(): void + { + $parts = explode(',', self::$_kanji_encodings); + if ($parts[1] == 'EUC-JP') { + self::$_kanji_encodings = 'ASCII,SJIS,EUC-JP,JIS'; + } else { + self::$_kanji_encodings = 'ASCII,EUC-JP,SJIS,JIS'; + } + } + + /** + * Kanji string encoding convert + * + * @param string $str the string to convert + * @param string $enc the destination encoding code + * @param string $kana set 'kana' convert to JIS-X208-kana + * + * @return string the converted string + */ + public static function kanjiStrConv(string $str, string $enc, string $kana): string + { + if ($enc == '' && $kana == '') { + return $str; + } + + $string_encoding = mb_detect_encoding($str, self::$_kanji_encodings); + if ($string_encoding === false) { + $string_encoding = 'utf-8'; + } + + if ($kana == 'kana') { + $dist = mb_convert_kana($str, 'KV', $string_encoding); + $str = $dist; + } + if ($string_encoding != $enc && $enc != '') { + $dist = mb_convert_encoding($str, $enc, $string_encoding); + } else { + $dist = $str; + } + return $dist; + } + + + /** + * Kanji file encoding convert + * + * @param string $file the name of the file to convert + * @param string $enc the destination encoding code + * @param string $kana set 'kana' convert to JIS-X208-kana + * + * @return string the name of the converted file + */ + public static function kanjiFileConv(string $file, string $enc, string $kana): string + { + if ($enc == '' && $kana == '') { + return $file; + } + $tmpfname = tempnam($GLOBALS['PMA_Config']->getUploadTempDir(), $enc); + $fpd = fopen($tmpfname, 'wb'); + $fps = fopen($file, 'r'); + self::kanjiChangeOrder(); + while (! feof($fps)) { + $line = fgets($fps, 4096); + $dist = self::kanjiStrConv($line, $enc, $kana); + fwrite($fpd, $dist); + } // end while + self::kanjiChangeOrder(); + fclose($fps); + fclose($fpd); + unlink($file); + + return $tmpfname; + } + + /** + * Defines radio form fields to switch between encoding modes + * + * @return string HTML code for the radio controls + */ + public static function kanjiEncodingForm(): string + { + $template = new Template(); + return $template->render('encoding/kanji_encoding_form'); + } + + /** + * Lists available encodings. + * + * @return array + */ + public static function listEncodings(): array + { + if (self::$_engine === null) { + self::initEngine(); + } + /* Most engines do not support listing */ + if (self::$_engine != self::ENGINE_MB) { + return $GLOBALS['cfg']['AvailableCharsets']; + } + + return array_intersect( + array_map('strtolower', mb_list_encodings()), + $GLOBALS['cfg']['AvailableCharsets'] + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Engines/Bdb.php b/srcs/phpmyadmin/libraries/classes/Engines/Bdb.php new file mode 100644 index 0000000..70919e3 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Engines/Bdb.php @@ -0,0 +1,76 @@ + [ + 'title' => __('Version information'), + ], + 'bdb_cache_size' => [ + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'bdb_home' => [], + 'bdb_log_buffer_size' => [ + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'bdb_logdir' => [], + 'bdb_max_lock' => [ + 'type' => PMA_ENGINE_DETAILS_TYPE_NUMERIC, + ], + 'bdb_shared_data' => [], + 'bdb_tmpdir' => [], + 'bdb_data_direct' => [], + 'bdb_lock_detect' => [], + 'bdb_log_direct' => [], + 'bdb_no_recover' => [], + 'bdb_no_sync' => [], + 'skip_sync_bdb_logs' => [], + 'sync_bdb_logs' => [], + ]; + } + + /** + * Returns the pattern to be used in the query for SQL variables + * related to this storage engine + * + * @return string LIKE pattern + */ + public function getVariablesLikePattern() + { + return '%bdb%'; + } + + /** + * returns string with filename for the MySQL helppage + * about this storage engine + * + * @return string mysql helppage filename + */ + public function getMysqlHelpPage() + { + return 'bdb'; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Engines/Berkeleydb.php b/srcs/phpmyadmin/libraries/classes/Engines/Berkeleydb.php new file mode 100644 index 0000000..0c69d5f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Engines/Berkeleydb.php @@ -0,0 +1,19 @@ + [ + 'title' => __('Data home directory'), + 'desc' => __( + 'The common part of the directory path for all InnoDB data ' + . 'files.' + ), + ], + 'innodb_data_file_path' => [ + 'title' => __('Data files'), + ], + 'innodb_autoextend_increment' => [ + 'title' => __('Autoextend increment'), + 'desc' => __( + 'The increment size for extending the size of an autoextending ' + . 'tablespace when it becomes full.' + ), + 'type' => PMA_ENGINE_DETAILS_TYPE_NUMERIC, + ], + 'innodb_buffer_pool_size' => [ + 'title' => __('Buffer pool size'), + 'desc' => __( + 'The size of the memory buffer InnoDB uses to cache data and ' + . 'indexes of its tables.' + ), + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'innodb_additional_mem_pool_size' => [ + 'title' => 'innodb_additional_mem_pool_size', + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'innodb_buffer_pool_awe_mem_mb' => [ + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'innodb_checksums' => [], + 'innodb_commit_concurrency' => [], + 'innodb_concurrency_tickets' => [ + 'type' => PMA_ENGINE_DETAILS_TYPE_NUMERIC, + ], + 'innodb_doublewrite' => [], + 'innodb_fast_shutdown' => [], + 'innodb_file_io_threads' => [ + 'type' => PMA_ENGINE_DETAILS_TYPE_NUMERIC, + ], + 'innodb_file_per_table' => [], + 'innodb_flush_log_at_trx_commit' => [], + 'innodb_flush_method' => [], + 'innodb_force_recovery' => [], + 'innodb_lock_wait_timeout' => [ + 'type' => PMA_ENGINE_DETAILS_TYPE_NUMERIC, + ], + 'innodb_locks_unsafe_for_binlog' => [], + 'innodb_log_arch_dir' => [], + 'innodb_log_archive' => [], + 'innodb_log_buffer_size' => [ + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'innodb_log_file_size' => [ + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'innodb_log_files_in_group' => [ + 'type' => PMA_ENGINE_DETAILS_TYPE_NUMERIC, + ], + 'innodb_log_group_home_dir' => [], + 'innodb_max_dirty_pages_pct' => [ + 'type' => PMA_ENGINE_DETAILS_TYPE_NUMERIC, + ], + 'innodb_max_purge_lag' => [], + 'innodb_mirrored_log_groups' => [ + 'type' => PMA_ENGINE_DETAILS_TYPE_NUMERIC, + ], + 'innodb_open_files' => [ + 'type' => PMA_ENGINE_DETAILS_TYPE_NUMERIC, + ], + 'innodb_support_xa' => [], + 'innodb_sync_spin_loops' => [ + 'type' => PMA_ENGINE_DETAILS_TYPE_NUMERIC, + ], + 'innodb_table_locks' => [ + 'type' => PMA_ENGINE_DETAILS_TYPE_BOOLEAN, + ], + 'innodb_thread_concurrency' => [ + 'type' => PMA_ENGINE_DETAILS_TYPE_NUMERIC, + ], + 'innodb_thread_sleep_delay' => [ + 'type' => PMA_ENGINE_DETAILS_TYPE_NUMERIC, + ], + ]; + } + + /** + * Returns the pattern to be used in the query for SQL variables + * related to InnoDb storage engine + * + * @return string SQL query LIKE pattern + */ + public function getVariablesLikePattern() + { + return 'innodb\\_%'; + } + + /** + * Get information pages + * + * @return array detail pages + */ + public function getInfoPages() + { + if ($this->support < PMA_ENGINE_SUPPORT_YES) { + return []; + } + $pages = []; + $pages['Bufferpool'] = __('Buffer Pool'); + $pages['Status'] = __('InnoDB Status'); + + return $pages; + } + + /** + * returns html tables with stats over inno db buffer pool + * + * @return string html table with stats + */ + public function getPageBufferpool() + { + // The following query is only possible because we know + // that we are on MySQL 5 here (checked above)! + // side note: I love MySQL 5 for this. :-) + $sql = 'SHOW STATUS' + . ' WHERE Variable_name LIKE \'Innodb\\_buffer\\_pool\\_%\'' + . ' OR Variable_name = \'Innodb_page_size\';'; + $status = $GLOBALS['dbi']->fetchResult($sql, 0, 1); + + $output = '' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' '; + + // not present at least since MySQL 5.1.40 + if (isset($status['Innodb_buffer_pool_pages_latched'])) { + $output .= ' ' + . ' ' + . ' ' + . ' '; + } + + $output .= ' ' . "\n" + . '
' . "\n" + . ' ' . __('Buffer Pool Usage') . "\n" + . '
' . "\n" + . ' ' . __('Total') . "\n" + . ' : ' + . Util::formatNumber( + $status['Innodb_buffer_pool_pages_total'], + 0 + ) + . ' ' . __('pages') + . ' / ' + . implode( + ' ', + Util::formatByteDown( + $status['Innodb_buffer_pool_pages_total'] + * $status['Innodb_page_size'] + ) + ) . "\n" + . '
' . __('Free pages') . '' + . Util::formatNumber( + $status['Innodb_buffer_pool_pages_free'], + 0 + ) + . '
' . __('Dirty pages') . '' + . Util::formatNumber( + $status['Innodb_buffer_pool_pages_dirty'], + 0 + ) + . '
' . __('Pages containing data') . '' + . Util::formatNumber( + $status['Innodb_buffer_pool_pages_data'], + 0 + ) . "\n" + . '
' . __('Pages to be flushed') . '' + . Util::formatNumber( + $status['Innodb_buffer_pool_pages_flushed'], + 0 + ) . "\n" + . '
' . __('Busy pages') . '' + . Util::formatNumber( + $status['Innodb_buffer_pool_pages_misc'], + 0 + ) . "\n" + . '
' . __('Latched pages') . '' + . Util::formatNumber( + $status['Innodb_buffer_pool_pages_latched'], + 0 + ) + . '
' . "\n\n" + . '' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . '
' . "\n" + . ' ' . __('Buffer Pool Activity') . "\n" + . '
' . __('Read requests') . '' + . Util::formatNumber( + $status['Innodb_buffer_pool_read_requests'], + 0 + ) . "\n" + . '
' . __('Write requests') . '' + . Util::formatNumber( + $status['Innodb_buffer_pool_write_requests'], + 0 + ) . "\n" + . '
' . __('Read misses') . '' + . Util::formatNumber( + $status['Innodb_buffer_pool_reads'], + 0 + ) . "\n" + . '
' . __('Write waits') . '' + . Util::formatNumber( + $status['Innodb_buffer_pool_wait_free'], + 0 + ) . "\n" + . '
' . __('Read misses in %') . '' + . ($status['Innodb_buffer_pool_read_requests'] == 0 + ? '---' + : htmlspecialchars( + Util::formatNumber( + $status['Innodb_buffer_pool_reads'] * 100 + / $status['Innodb_buffer_pool_read_requests'], + 3, + 2 + ) + ) . ' %') . "\n" + . '
' . __('Write waits in %') . '' + . ($status['Innodb_buffer_pool_write_requests'] == 0 + ? '---' + : htmlspecialchars( + Util::formatNumber( + $status['Innodb_buffer_pool_wait_free'] * 100 + / $status['Innodb_buffer_pool_write_requests'], + 3, + 2 + ) + ) . ' %') . "\n" + . '
' . "\n"; + + return $output; + } + + /** + * returns InnoDB status + * + * @return string result of SHOW ENGINE INNODB STATUS inside pre tags + */ + public function getPageStatus() + { + return '
' . "\n"
+            . htmlspecialchars((string) $GLOBALS['dbi']->fetchValue(
+                'SHOW ENGINE INNODB STATUS;',
+                0,
+                'Status'
+            )) . "\n" . '
' . "\n"; + } + + /** + * returns string with filename for the MySQL helppage + * about this storage engine + * + * @return string mysql helppage filename + */ + public function getMysqlHelpPage() + { + return 'innodb-storage-engine'; + } + + /** + * Gets the InnoDB plugin version number + * + * @return string the version number, or empty if not running as a plugin + */ + public function getInnodbPluginVersion() + { + return $GLOBALS['dbi']->fetchValue('SELECT @@innodb_version;'); + } + + /** + * Gets the InnoDB file format + * + * (do not confuse this with phpMyAdmin's storage engine plugins!) + * + * @return string the InnoDB file format + */ + public function getInnodbFileFormat() + { + return $GLOBALS['dbi']->fetchValue( + "SHOW GLOBAL VARIABLES LIKE 'innodb_file_format';", + 0, + 1 + ); + } + + /** + * Verifies if this server supports the innodb_file_per_table feature + * + * (do not confuse this with phpMyAdmin's storage engine plugins!) + * + * @return boolean whether this feature is supported or not + */ + public function supportsFilePerTable() + { + return ( + $GLOBALS['dbi']->fetchValue( + "SHOW GLOBAL VARIABLES LIKE 'innodb_file_per_table';", + 0, + 1 + ) == 'ON' + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Engines/Memory.php b/srcs/phpmyadmin/libraries/classes/Engines/Memory.php new file mode 100644 index 0000000..d01a63e --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Engines/Memory.php @@ -0,0 +1,34 @@ + [ + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Engines/Merge.php b/srcs/phpmyadmin/libraries/classes/Engines/Merge.php new file mode 100644 index 0000000..c4bee94 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Engines/Merge.php @@ -0,0 +1,21 @@ + [ + 'title' => __('Data pointer size'), + 'desc' => __( + 'The default pointer size in bytes, to be used by CREATE TABLE ' + . 'for MyISAM tables when no MAX_ROWS option is specified.' + ), + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'myisam_recover_options' => [ + 'title' => __('Automatic recovery mode'), + 'desc' => __( + 'The mode for automatic recovery of crashed MyISAM tables, as ' + . 'set via the --myisam-recover server startup option.' + ), + ], + 'myisam_max_sort_file_size' => [ + 'title' => __('Maximum size for temporary sort files'), + 'desc' => __( + 'The maximum size of the temporary file MySQL is allowed to use ' + . 'while re-creating a MyISAM index (during REPAIR TABLE, ALTER ' + . 'TABLE, or LOAD DATA INFILE).' + ), + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'myisam_max_extra_sort_file_size' => [ + 'title' => __('Maximum size for temporary files on index creation'), + 'desc' => __( + 'If the temporary file used for fast MyISAM index creation ' + . 'would be larger than using the key cache by the amount ' + . 'specified here, prefer the key cache method.' + ), + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'myisam_repair_threads' => [ + 'title' => __('Repair threads'), + 'desc' => __( + 'If this value is greater than 1, MyISAM table indexes are ' + . 'created in parallel (each index in its own thread) during ' + . 'the repair by sorting process.' + ), + 'type' => PMA_ENGINE_DETAILS_TYPE_NUMERIC, + ], + 'myisam_sort_buffer_size' => [ + 'title' => __('Sort buffer size'), + 'desc' => __( + 'The buffer that is allocated when sorting MyISAM indexes ' + . 'during a REPAIR TABLE or when creating indexes with CREATE ' + . 'INDEX or ALTER TABLE.' + ), + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'myisam_stats_method' => [], + 'delay_key_write' => [], + 'bulk_insert_buffer_size' => [ + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'skip_external_locking' => [], + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Engines/Ndbcluster.php b/srcs/phpmyadmin/libraries/classes/Engines/Ndbcluster.php new file mode 100644 index 0000000..bd331bf --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Engines/Ndbcluster.php @@ -0,0 +1,54 @@ + [], + ]; + } + + /** + * Returns the pattern to be used in the query for SQL variables + * related to NDBCLUSTER storage engine + * + * @return string SQL query LIKE pattern + */ + public function getVariablesLikePattern() + { + return 'ndb\\_%'; + } + + /** + * Returns string with filename for the MySQL help page + * about this storage engine + * + * @return string mysql helppage filename + */ + public function getMysqlHelpPage() + { + return 'ndbcluster'; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Engines/Pbxt.php b/srcs/phpmyadmin/libraries/classes/Engines/Pbxt.php new file mode 100644 index 0000000..9aa101a --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Engines/Pbxt.php @@ -0,0 +1,195 @@ + [ + 'title' => __('Index cache size'), + 'desc' => __( + 'This is the amount of memory allocated to the' + . ' index cache. Default value is 32MB. The memory' + . ' allocated here is used only for caching index pages.' + ), + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'pbxt_record_cache_size' => [ + 'title' => __('Record cache size'), + 'desc' => __( + 'This is the amount of memory allocated to the' + . ' record cache used to cache table data. The default' + . ' value is 32MB. This memory is used to cache changes to' + . ' the handle data (.xtd) and row pointer (.xtr) files.' + ), + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'pbxt_log_cache_size' => [ + 'title' => __('Log cache size'), + 'desc' => __( + 'The amount of memory allocated to the' + . ' transaction log cache used to cache on transaction log' + . ' data. The default is 16MB.' + ), + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'pbxt_log_file_threshold' => [ + 'title' => __('Log file threshold'), + 'desc' => __( + 'The size of a transaction log before rollover,' + . ' and a new log is created. The default value is 16MB.' + ), + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'pbxt_transaction_buffer_size' => [ + 'title' => __('Transaction buffer size'), + 'desc' => __( + 'The size of the global transaction log buffer' + . ' (the engine allocates 2 buffers of this size).' + . ' The default is 1MB.' + ), + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'pbxt_checkpoint_frequency' => [ + 'title' => __('Checkpoint frequency'), + 'desc' => __( + 'The amount of data written to the transaction' + . ' log before a checkpoint is performed.' + . ' The default value is 24MB.' + ), + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'pbxt_data_log_threshold' => [ + 'title' => __('Data log threshold'), + 'desc' => __( + 'The maximum size of a data log file. The default' + . ' value is 64MB. PBXT can create a maximum of 32000 data' + . ' logs, which are used by all tables. So the value of' + . ' this variable can be increased to increase the total' + . ' amount of data that can be stored in the database.' + ), + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'pbxt_garbage_threshold' => [ + 'title' => __('Garbage threshold'), + 'desc' => __( + 'The percentage of garbage in a data log file' + . ' before it is compacted. This is a value between 1 and' + . ' 99. The default is 50.' + ), + 'type' => PMA_ENGINE_DETAILS_TYPE_NUMERIC, + ], + 'pbxt_log_buffer_size' => [ + 'title' => __('Log buffer size'), + 'desc' => __( + 'The size of the buffer used when writing a data' + . ' log. The default is 256MB. The engine allocates one' + . ' buffer per thread, but only if the thread is required' + . ' to write a data log.' + ), + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'pbxt_data_file_grow_size' => [ + 'title' => __('Data file grow size'), + 'desc' => __('The grow size of the handle data (.xtd) files.'), + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'pbxt_row_file_grow_size' => [ + 'title' => __('Row file grow size'), + 'desc' => __('The grow size of the row pointer (.xtr) files.'), + 'type' => PMA_ENGINE_DETAILS_TYPE_SIZE, + ], + 'pbxt_log_file_count' => [ + 'title' => __('Log file count'), + 'desc' => __( + 'This is the number of transaction log files' + . ' (pbxt/system/xlog*.xt) the system will maintain. If the' + . ' number of logs exceeds this value then old logs will be' + . ' deleted, otherwise they are renamed and given the next' + . ' highest number.' + ), + 'type' => PMA_ENGINE_DETAILS_TYPE_NUMERIC, + ], + ]; + } + + /** + * returns the pbxt engine specific handling for + * PMA_ENGINE_DETAILS_TYPE_SIZE variables. + * + * @param string $formatted_size the size expression (for example 8MB) + * + * @return array the formatted value and its unit + */ + public function resolveTypeSize($formatted_size) + { + if (preg_match('/^[0-9]+[a-zA-Z]+$/', $formatted_size)) { + $value = Util::extractValueFromFormattedSize( + $formatted_size + ); + } else { + $value = $formatted_size; + } + + return Util::formatByteDown($value); + } + + //-------------------- + /** + * Get information about pages + * + * @return array Information about pages + */ + public function getInfoPages() + { + $pages = []; + $pages['Documentation'] = __('Documentation'); + + return $pages; + } + + //-------------------- + /** + * Get content of documentation page + * + * @return string + */ + public function getPageDocumentation() + { + $output = '

' . sprintf( + __( + 'Documentation and further information about PBXT' + . ' can be found on the %sPrimeBase XT Home Page%s.' + ), + '', + '' + ) + . '

' . "\n"; + + return $output; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Engines/PerformanceSchema.php b/srcs/phpmyadmin/libraries/classes/Engines/PerformanceSchema.php new file mode 100644 index 0000000..01669b4 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Engines/PerformanceSchema.php @@ -0,0 +1,31 @@ + 'Internal error', + E_ERROR => 'Error', + E_WARNING => 'Warning', + E_PARSE => 'Parsing Error', + E_NOTICE => 'Notice', + E_CORE_ERROR => 'Core Error', + E_CORE_WARNING => 'Core Warning', + E_COMPILE_ERROR => 'Compile Error', + E_COMPILE_WARNING => 'Compile Warning', + E_USER_ERROR => 'User Error', + E_USER_WARNING => 'User Warning', + E_USER_NOTICE => 'User Notice', + E_STRICT => 'Runtime Notice', + E_DEPRECATED => 'Deprecation Notice', + E_USER_DEPRECATED => 'Deprecation Notice', + E_RECOVERABLE_ERROR => 'Catchable Fatal Error', + ]; + + /** + * Error levels + * + * @var array + */ + public static $errorlevel = [ + 0 => 'error', + E_ERROR => 'error', + E_WARNING => 'error', + E_PARSE => 'error', + E_NOTICE => 'notice', + E_CORE_ERROR => 'error', + E_CORE_WARNING => 'error', + E_COMPILE_ERROR => 'error', + E_COMPILE_WARNING => 'error', + E_USER_ERROR => 'error', + E_USER_WARNING => 'error', + E_USER_NOTICE => 'notice', + E_STRICT => 'notice', + E_DEPRECATED => 'notice', + E_USER_DEPRECATED => 'notice', + E_RECOVERABLE_ERROR => 'error', + ]; + + /** + * The file in which the error occurred + * + * @var string + */ + protected $file = ''; + + /** + * The line in which the error occurred + * + * @var integer + */ + protected $line = 0; + + /** + * Holds the backtrace for this error + * + * @var array + */ + protected $backtrace = []; + + /** + * Hide location of errors + */ + protected $hide_location = false; + + /** + * Constructor + * + * @param integer $errno error number + * @param string $errstr error message + * @param string $errfile file + * @param integer $errline line + */ + public function __construct(int $errno, string $errstr, string $errfile, int $errline) + { + parent::__construct(); + $this->setNumber($errno); + $this->setMessage($errstr, false); + $this->setFile($errfile); + $this->setLine($errline); + + // This function can be disabled in php.ini + if (function_exists('debug_backtrace')) { + $backtrace = @debug_backtrace(); + // remove last three calls: + // debug_backtrace(), handleError() and addError() + $backtrace = array_slice($backtrace, 3); + } else { + $backtrace = []; + } + + $this->setBacktrace($backtrace); + } + + /** + * Process backtrace to avoid path disclossures, objects and so on + * + * @param array $backtrace backtrace + * + * @return array + */ + public static function processBacktrace(array $backtrace): array + { + $result = []; + + $members = [ + 'line', + 'function', + 'class', + 'type', + ]; + + foreach ($backtrace as $idx => $step) { + /* Create new backtrace entry */ + $result[$idx] = []; + + /* Make path relative */ + if (isset($step['file'])) { + $result[$idx]['file'] = self::relPath($step['file']); + } + + /* Store members we want */ + foreach ($members as $name) { + if (isset($step[$name])) { + $result[$idx][$name] = $step[$name]; + } + } + + /* Store simplified args */ + if (isset($step['args'])) { + foreach ($step['args'] as $key => $arg) { + $result[$idx]['args'][$key] = self::getArg($arg, $step['function']); + } + } + } + + return $result; + } + + /** + * Toggles location hiding + * + * @param boolean $hide Whether to hide + * + * @return void + */ + public function setHideLocation(bool $hide): void + { + $this->hide_location = $hide; + } + + /** + * sets PhpMyAdmin\Error::$_backtrace + * + * We don't store full arguments to avoid wakeup or memory problems. + * + * @param array $backtrace backtrace + * + * @return void + */ + public function setBacktrace(array $backtrace): void + { + $this->backtrace = self::processBacktrace($backtrace); + } + + /** + * sets PhpMyAdmin\Error::$_line + * + * @param integer $line the line + * + * @return void + */ + public function setLine(int $line): void + { + $this->line = $line; + } + + /** + * sets PhpMyAdmin\Error::$_file + * + * @param string $file the file + * + * @return void + */ + public function setFile(string $file): void + { + $this->file = self::relPath($file); + } + + + /** + * returns unique PhpMyAdmin\Error::$hash, if not exists it will be created + * + * @return string PhpMyAdmin\Error::$hash + */ + public function getHash(): string + { + try { + $backtrace = serialize($this->getBacktrace()); + } catch (Exception $e) { + $backtrace = ''; + } + if ($this->hash === null) { + $this->hash = md5( + $this->getNumber() . + $this->getMessage() . + $this->getFile() . + $this->getLine() . + $backtrace + ); + } + + return $this->hash; + } + + /** + * returns PhpMyAdmin\Error::$_backtrace for first $count frames + * pass $count = -1 to get full backtrace. + * The same can be done by not passing $count at all. + * + * @param integer $count Number of stack frames. + * + * @return array PhpMyAdmin\Error::$_backtrace + */ + public function getBacktrace(int $count = -1): array + { + if ($count != -1) { + return array_slice($this->backtrace, 0, $count); + } + return $this->backtrace; + } + + /** + * returns PhpMyAdmin\Error::$file + * + * @return string PhpMyAdmin\Error::$file + */ + public function getFile(): string + { + return $this->file; + } + + /** + * returns PhpMyAdmin\Error::$line + * + * @return integer PhpMyAdmin\Error::$line + */ + public function getLine(): int + { + return $this->line; + } + + /** + * returns type of error + * + * @return string type of error + */ + public function getType(): string + { + return self::$errortype[$this->getNumber()]; + } + + /** + * returns level of error + * + * @return string level of error + */ + public function getLevel(): string + { + return self::$errorlevel[$this->getNumber()]; + } + + /** + * returns title prepared for HTML Title-Tag + * + * @return string HTML escaped and truncated title + */ + public function getHtmlTitle(): string + { + return htmlspecialchars( + mb_substr($this->getTitle(), 0, 100) + ); + } + + /** + * returns title for error + * + * @return string + */ + public function getTitle(): string + { + return $this->getType() . ': ' . $this->getMessage(); + } + + /** + * Get HTML backtrace + * + * @return string + */ + public function getBacktraceDisplay(): string + { + return self::formatBacktrace( + $this->getBacktrace(), + "
\n", + "
\n" + ); + } + + /** + * return formatted backtrace field + * + * @param array $backtrace Backtrace data + * @param string $separator Arguments separator to use + * @param string $lines Lines separator to use + * + * @return string formatted backtrace + */ + public static function formatBacktrace( + array $backtrace, + string $separator, + string $lines + ): string { + $retval = ''; + + foreach ($backtrace as $step) { + if (isset($step['file']) && isset($step['line'])) { + $retval .= self::relPath($step['file']) + . '#' . $step['line'] . ': '; + } + if (isset($step['class'])) { + $retval .= $step['class'] . $step['type']; + } + $retval .= self::getFunctionCall($step, $separator); + $retval .= $lines; + } + + return $retval; + } + + /** + * Formats function call in a backtrace + * + * @param array $step backtrace step + * @param string $separator Arguments separator to use + * + * @return string + */ + public static function getFunctionCall(array $step, string $separator): string + { + $retval = $step['function'] . '('; + if (isset($step['args'])) { + if (count($step['args']) > 1) { + $retval .= $separator; + foreach ($step['args'] as $arg) { + $retval .= "\t"; + $retval .= $arg; + $retval .= ',' . $separator; + } + } elseif (count($step['args']) > 0) { + foreach ($step['args'] as $arg) { + $retval .= $arg; + } + } + } + $retval .= ')'; + return $retval; + } + + /** + * Get a single function argument + * + * if $function is one of include/require + * the $arg is converted to a relative path + * + * @param string $arg argument to process + * @param string $function function name + * + * @return string + */ + public static function getArg($arg, string $function): string + { + $retval = ''; + $include_functions = [ + 'include', + 'include_once', + 'require', + 'require_once', + ]; + $connect_functions = [ + 'mysql_connect', + 'mysql_pconnect', + 'mysqli_connect', + 'mysqli_real_connect', + 'connect', + '_realConnect', + ]; + + if (in_array($function, $include_functions)) { + $retval .= self::relPath($arg); + } elseif (in_array($function, $connect_functions) + && gettype($arg) === 'string' + ) { + $retval .= gettype($arg) . ' ********'; + } elseif (is_scalar($arg)) { + $retval .= gettype($arg) . ' ' + . htmlspecialchars(var_export($arg, true)); + } elseif (is_object($arg)) { + $retval .= ''; + } else { + $retval .= gettype($arg); + } + + return $retval; + } + + /** + * Gets the error as string of HTML + * + * @return string + */ + public function getDisplay(): string + { + $this->isDisplayed(true); + $retval = '
'; + if (! $this->isUserError()) { + $retval .= '' . $this->getType() . ''; + $retval .= ' in ' . $this->getFile() . '#' . $this->getLine(); + $retval .= "
\n"; + } + $retval .= $this->getMessage(); + if (! $this->isUserError()) { + $retval .= "
\n"; + $retval .= "
\n"; + $retval .= "Backtrace
\n"; + $retval .= "
\n"; + $retval .= $this->getBacktraceDisplay(); + } + $retval .= '
'; + + return $retval; + } + + /** + * whether this error is a user error + * + * @return boolean + */ + public function isUserError(): bool + { + return $this->hide_location || + ($this->getNumber() & (E_USER_WARNING | E_USER_ERROR | E_USER_NOTICE)); + } + + /** + * return short relative path to phpMyAdmin basedir + * + * prevent path disclosure in error message, + * and make users feel safe to submit error reports + * + * @param string $path path to be shorten + * + * @return string shortened path + */ + public static function relPath(string $path): string + { + $dest = @realpath($path); + + /* Probably affected by open_basedir */ + if ($dest === false) { + return basename($path); + } + + $Ahere = explode( + DIRECTORY_SEPARATOR, + realpath(__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..') + ); + $Adest = explode(DIRECTORY_SEPARATOR, $dest); + + $result = '.'; + // && count ($Adest)>0 && count($Ahere)>0 ) + while (implode(DIRECTORY_SEPARATOR, $Adest) != implode(DIRECTORY_SEPARATOR, $Ahere)) { + if (count($Ahere) > count($Adest)) { + array_pop($Ahere); + $result .= DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..'; + } else { + array_pop($Adest); + } + } + $path = $result . str_replace(implode(DIRECTORY_SEPARATOR, $Adest), '', $dest); + return str_replace( + DIRECTORY_SEPARATOR . PATH_SEPARATOR, + DIRECTORY_SEPARATOR, + $path + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/ErrorHandler.php b/srcs/phpmyadmin/libraries/classes/ErrorHandler.php new file mode 100644 index 0000000..f5cf124 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/ErrorHandler.php @@ -0,0 +1,604 @@ +error_reporting = error_reporting(); + } + } + + /** + * Destructor + * + * stores errors in session + * + */ + public function __destruct() + { + if (isset($_SESSION)) { + if (! isset($_SESSION['errors'])) { + $_SESSION['errors'] = []; + } + + // remember only not displayed errors + foreach ($this->errors as $key => $error) { + /** + * We don't want to store all errors here as it would + * explode user session. + */ + if (count($_SESSION['errors']) >= 10) { + $error = new Error( + 0, + __('Too many error messages, some are not displayed.'), + __FILE__, + __LINE__ + ); + $_SESSION['errors'][$error->getHash()] = $error; + break; + } elseif (($error instanceof Error) + && ! $error->isDisplayed() + ) { + $_SESSION['errors'][$key] = $error; + } + } + } + } + + /** + * Toggles location hiding + * + * @param boolean $hide Whether to hide + * + * @return void + */ + public function setHideLocation(bool $hide): void + { + $this->hide_location = $hide; + } + + /** + * returns array with all errors + * + * @param bool $check Whether to check for session errors + * + * @return Error[] + */ + public function getErrors(bool $check = true): array + { + if ($check) { + $this->checkSavedErrors(); + } + return $this->errors; + } + + /** + * returns the errors occurred in the current run only. + * Does not include the errors saved in the SESSION + * + * @return Error[] + */ + public function getCurrentErrors(): array + { + return $this->errors; + } + + /** + * Pops recent errors from the storage + * + * @param int $count Old error count + * + * @return Error[] + */ + public function sliceErrors(int $count): array + { + $errors = $this->getErrors(false); + $this->errors = array_splice($errors, 0, $count); + return array_splice($errors, $count); + } + + /** + * Error handler - called when errors are triggered/occurred + * + * This calls the addError() function, escaping the error string + * Ignores the errors wherever Error Control Operator (@) is used. + * + * @param integer $errno error number + * @param string $errstr error string + * @param string $errfile error file + * @param integer $errline error line + * + * @return void + */ + public function handleError( + int $errno, + string $errstr, + string $errfile, + int $errline + ): void { + if (function_exists('error_reporting')) { + /** + * Check if Error Control Operator (@) was used, but still show + * user errors even in this case. + */ + if (error_reporting() == 0 && + $this->error_reporting != 0 && + ($errno & (E_USER_WARNING | E_USER_ERROR | E_USER_NOTICE)) == 0 + ) { + return; + } + } else { + if (($errno & (E_USER_WARNING | E_USER_ERROR | E_USER_NOTICE)) == 0) { + return; + } + } + + $this->addError($errstr, $errno, $errfile, $errline, true); + } + + /** + * Add an error; can also be called directly (with or without escaping) + * + * The following error types cannot be handled with a user defined function: + * E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, + * E_COMPILE_WARNING, + * and most of E_STRICT raised in the file where set_error_handler() is called. + * + * Do not use the context parameter as we want to avoid storing the + * complete $GLOBALS inside $_SESSION['errors'] + * + * @param string $errstr error string + * @param integer $errno error number + * @param string $errfile error file + * @param integer $errline error line + * @param boolean $escape whether to escape the error string + * + * @return void + */ + public function addError( + string $errstr, + int $errno, + string $errfile, + int $errline, + bool $escape = true + ): void { + if ($escape) { + $errstr = htmlspecialchars($errstr); + } + // create error object + $error = new Error( + $errno, + $errstr, + $errfile, + $errline + ); + $error->setHideLocation($this->hide_location); + + // do not repeat errors + $this->errors[$error->getHash()] = $error; + + switch ($error->getNumber()) { + case E_STRICT: + case E_DEPRECATED: + case E_NOTICE: + case E_WARNING: + case E_CORE_WARNING: + case E_COMPILE_WARNING: + case E_RECOVERABLE_ERROR: + /* Avoid rendering BB code in PHP errors */ + $error->setBBCode(false); + break; + case E_USER_NOTICE: + case E_USER_WARNING: + case E_USER_ERROR: + // just collect the error + // display is called from outside + break; + case E_ERROR: + case E_PARSE: + case E_CORE_ERROR: + case E_COMPILE_ERROR: + default: + // FATAL error, display it and exit + $this->dispFatalError($error); + exit; + } + } + + /** + * trigger a custom error + * + * @param string $errorInfo error message + * @param integer $errorNumber error number + * + * @return void + */ + public function triggerError(string $errorInfo, ?int $errorNumber = null): void + { + // we could also extract file and line from backtrace + // and call handleError() directly + trigger_error($errorInfo, $errorNumber); + } + + /** + * display fatal error and exit + * + * @param Error $error the error + * + * @return void + */ + protected function dispFatalError(Error $error): void + { + if (! headers_sent()) { + $this->dispPageStart($error); + } + $error->display(); + $this->dispPageEnd(); + exit; + } + + /** + * Displays user errors not displayed + * + * @return void + */ + public function dispUserErrors(): void + { + echo $this->getDispUserErrors(); + } + + /** + * Renders user errors not displayed + * + * @return string + */ + public function getDispUserErrors(): string + { + $retval = ''; + foreach ($this->getErrors() as $error) { + if ($error->isUserError() && ! $error->isDisplayed()) { + $retval .= $error->getDisplay(); + } + } + return $retval; + } + + /** + * display HTML header + * + * @param Error $error the error + * + * @return void + */ + protected function dispPageStart(?Error $error = null): void + { + Response::getInstance()->disable(); + echo ''; + if ($error) { + echo $error->getTitle(); + } else { + echo 'phpMyAdmin error reporting page'; + } + echo ''; + } + + /** + * display HTML footer + * + * @return void + */ + protected function dispPageEnd(): void + { + echo ''; + } + + /** + * renders errors not displayed + * + * @return string + */ + public function getDispErrors(): string + { + $retval = ''; + // display errors if SendErrorReports is set to 'ask'. + if ($GLOBALS['cfg']['SendErrorReports'] != 'never') { + foreach ($this->getErrors() as $error) { + if (! $error->isDisplayed()) { + $retval .= $error->getDisplay(); + } + } + } else { + $retval .= $this->getDispUserErrors(); + } + // if preference is not 'never' and + // there are 'actual' errors to be reported + if ($GLOBALS['cfg']['SendErrorReports'] != 'never' + && $this->countErrors() != $this->countUserErrors() + ) { + // add report button. + $retval .= '
'php', + 'send_error_report' => '1', + 'server' => $GLOBALS['server'], + ]); + $retval .= '' + . '' + . ''; + + if ($GLOBALS['cfg']['SendErrorReports'] == 'ask') { + // add ignore buttons + $retval .= ''; + } + $retval .= ''; + $retval .= '
'; + } + return $retval; + } + + /** + * displays errors not displayed + * + * @return void + */ + public function dispErrors(): void + { + echo $this->getDispErrors(); + } + + /** + * look in session for saved errors + * + * @return void + */ + protected function checkSavedErrors(): void + { + if (isset($_SESSION['errors'])) { + // restore saved errors + foreach ($_SESSION['errors'] as $hash => $error) { + if ($error instanceof Error && ! isset($this->errors[$hash])) { + $this->errors[$hash] = $error; + } + } + + // delete stored errors + $_SESSION['errors'] = []; + unset($_SESSION['errors']); + } + } + + /** + * return count of errors + * + * @param bool $check Whether to check for session errors + * + * @return integer number of errors occurred + */ + public function countErrors(bool $check = true): int + { + return count($this->getErrors($check)); + } + + /** + * return count of user errors + * + * @return integer number of user errors occurred + */ + public function countUserErrors(): int + { + $count = 0; + if ($this->countErrors()) { + foreach ($this->getErrors() as $error) { + if ($error->isUserError()) { + $count++; + } + } + } + + return $count; + } + + /** + * whether use errors occurred or not + * + * @return boolean + */ + public function hasUserErrors(): bool + { + return (bool) $this->countUserErrors(); + } + + /** + * whether errors occurred or not + * + * @return boolean + */ + public function hasErrors(): bool + { + return (bool) $this->countErrors(); + } + + /** + * number of errors to be displayed + * + * @return integer number of errors to be displayed + */ + public function countDisplayErrors(): int + { + if ($GLOBALS['cfg']['SendErrorReports'] != 'never') { + return $this->countErrors(); + } + + return $this->countUserErrors(); + } + + /** + * whether there are errors to display or not + * + * @return boolean + */ + public function hasDisplayErrors(): bool + { + return (bool) $this->countDisplayErrors(); + } + + /** + * Deletes previously stored errors in SESSION. + * Saves current errors in session as previous errors. + * Required to save current errors in case 'ask' + * + * @return void + */ + public function savePreviousErrors(): void + { + unset($_SESSION['prev_errors']); + $_SESSION['prev_errors'] = $GLOBALS['error_handler']->getCurrentErrors(); + } + + /** + * Function to check if there are any errors to be prompted. + * Needed because user warnings raised are + * also collected by global error handler. + * This distinguishes between the actual errors + * and user errors raised to warn user. + * + * @return boolean true if there are errors to be "prompted", false otherwise + */ + public function hasErrorsForPrompt(): bool + { + return ( + $GLOBALS['cfg']['SendErrorReports'] != 'never' + && $this->countErrors() != $this->countUserErrors() + ); + } + + /** + * Function to report all the collected php errors. + * Must be called at the end of each script + * by the $GLOBALS['error_handler'] only. + * + * @return void + */ + public function reportErrors(): void + { + // if there're no actual errors, + if (! $this->hasErrors() + || $this->countErrors() == $this->countUserErrors() + ) { + // then simply return. + return; + } + // Delete all the prev_errors in session & store new prev_errors in session + $this->savePreviousErrors(); + $response = Response::getInstance(); + $jsCode = ''; + if ($GLOBALS['cfg']['SendErrorReports'] == 'always') { + if ($response->isAjax()) { + // set flag for automatic report submission. + $response->addJSON('sendErrorAlways', '1'); + } else { + // send the error reports asynchronously & without asking user + $jsCode .= '$("#pma_report_errors_form").submit();' + . 'Functions.ajaxShowMessage( + Messages.phpErrorsBeingSubmitted, false + );'; + // js code to appropriate focusing, + $jsCode .= '$("html, body").animate({ + scrollTop:$(document).height() + }, "slow");'; + } + } elseif ($GLOBALS['cfg']['SendErrorReports'] == 'ask') { + //ask user whether to submit errors or not. + if (! $response->isAjax()) { + // js code to show appropriate msgs, event binding & focusing. + $jsCode = 'Functions.ajaxShowMessage(Messages.phpErrorsFound);' + . '$("#pma_ignore_errors_popup").on("click", function() { + Functions.ignorePhpErrors() + });' + . '$("#pma_ignore_all_errors_popup").on("click", + function() { + Functions.ignorePhpErrors(false) + });' + . '$("#pma_ignore_errors_bottom").on("click", function(e) { + e.preventDefault(); + Functions.ignorePhpErrors() + });' + . '$("#pma_ignore_all_errors_bottom").on("click", + function(e) { + e.preventDefault(); + Functions.ignorePhpErrors(false) + });' + . '$("html, body").animate({ + scrollTop:$(document).height() + }, "slow");'; + } + } + // The errors are already sent from the response. + // Just focus on errors division upon load event. + $response->getFooter()->getScripts()->addCode($jsCode); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/ErrorReport.php b/srcs/phpmyadmin/libraries/classes/ErrorReport.php new file mode 100644 index 0000000..b7044f2 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/ErrorReport.php @@ -0,0 +1,294 @@ +httpRequest = $httpRequest; + $this->relation = $relation; + $this->template = $template; + } + + /** + * Set the URL where to submit reports to + * + * @param string $submissionUrl Submission URL + * @return void + */ + public function setSubmissionUrl(string $submissionUrl): void + { + $this->submissionUrl = $submissionUrl; + } + + /** + * Returns the pretty printed error report data collected from the + * current configuration or from the request parameters sent by the + * error reporting js code. + * + * @return string the report + */ + private function getPrettyData(): string + { + $report = $this->getData(); + + return json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } + + /** + * Returns the error report data collected from the current configuration or + * from the request parameters sent by the error reporting js code. + * + * @param string $exceptionType whether exception is 'js' or 'php' + * + * @return array error report if success, Empty Array otherwise + */ + public function getData(string $exceptionType = 'js'): array + { + /** @var Config $PMA_Config */ + global $PMA_Config; + + $relParams = $this->relation->getRelationsParam(); + // common params for both, php & js exceptions + $report = [ + "pma_version" => PMA_VERSION, + "browser_name" => PMA_USR_BROWSER_AGENT, + "browser_version" => PMA_USR_BROWSER_VER, + "user_os" => PMA_USR_OS, + "server_software" => $_SERVER['SERVER_SOFTWARE'], + "user_agent_string" => $_SERVER['HTTP_USER_AGENT'], + "locale" => $PMA_Config->getCookie('pma_lang'), + "configuration_storage" => + $relParams['db'] === null ? "disabled" : "enabled", + "php_version" => PHP_VERSION, + ]; + + if ($exceptionType == 'js') { + if (empty($_POST['exception'])) { + return []; + } + $exception = $_POST['exception']; + $exception["stack"] = $this->translateStacktrace($exception["stack"]); + + if (isset($exception["url"])) { + list($uri, $scriptName) = $this->sanitizeUrl($exception["url"]); + $exception["uri"] = $uri; + $report["script_name"] = $scriptName; + unset($exception["url"]); + } elseif (isset($_POST["url"])) { + list($uri, $scriptName) = $this->sanitizeUrl($_POST["url"]); + $exception["uri"] = $uri; + $report["script_name"] = $scriptName; + unset($_POST["url"]); + } else { + $report["script_name"] = null; + } + + $report["exception_type"] = 'js'; + $report["exception"] = $exception; + if (isset($_POST['microhistory'])) { + $report["microhistory"] = $_POST['microhistory']; + } + + if (! empty($_POST['description'])) { + $report['steps'] = $_POST['description']; + } + } elseif ($exceptionType == 'php') { + $errors = []; + // create php error report + $i = 0; + if (! isset($_SESSION['prev_errors']) + || $_SESSION['prev_errors'] == '' + ) { + return []; + } + foreach ($_SESSION['prev_errors'] as $errorObj) { + /** @var Error $errorObj */ + if ($errorObj->getLine() + && $errorObj->getType() + && $errorObj->getNumber() != E_USER_WARNING + ) { + $errors[$i++] = [ + "lineNum" => $errorObj->getLine(), + "file" => $errorObj->getFile(), + "type" => $errorObj->getType(), + "msg" => $errorObj->getOnlyMessage(), + "stackTrace" => $errorObj->getBacktrace(5), + "stackhash" => $errorObj->getHash(), + ]; + } + } + + // if there were no 'actual' errors to be submitted. + if ($i == 0) { + return []; // then return empty array + } + $report["exception_type"] = 'php'; + $report["errors"] = $errors; + } else { + return []; + } + + return $report; + } + + /** + * Sanitize a url to remove the identifiable host name and extract the + * current script name from the url fragment + * + * It returns two things in an array. The first is the uri without the + * hostname and identifying query params. The second is the name of the + * php script in the url + * + * @param string $url the url to sanitize + * + * @return array the uri and script name + */ + private function sanitizeUrl(string $url): array + { + $components = parse_url($url); + if (isset($components["fragment"]) + && preg_match("", $components["fragment"], $matches) + ) { + $uri = str_replace($matches[0], "", $components["fragment"]); + $url = "https://example.com/" . $uri; + $components = parse_url($url); + } + + // get script name + preg_match("<([a-zA-Z\-_\d\.]*\.php|js\/[a-zA-Z\-_\d\/\.]*\.js)$>", $components["path"], $matches); + if (count($matches) < 2) { + $scriptName = 'index.php'; + } else { + $scriptName = $matches[1]; + } + + // remove deployment specific details to make uri more generic + if (isset($components["query"])) { + parse_str($components["query"], $queryArray); + unset($queryArray["db"]); + unset($queryArray["table"]); + unset($queryArray["token"]); + unset($queryArray["server"]); + $query = http_build_query($queryArray); + } else { + $query = ''; + } + + $uri = $scriptName . "?" . $query; + return [ + $uri, + $scriptName, + ]; + } + + /** + * Sends report data to the error reporting server + * + * @param array $report the report info to be sent + * + * @return string|null|bool the reply of the server + */ + public function send(array $report) + { + return $this->httpRequest->create( + $this->submissionUrl, + "POST", + false, + json_encode($report), + "Content-Type: application/json" + ); + } + + /** + * Translates the cumulative line numbers in the stack trace as well as sanitize + * urls and trim long lines in the context + * + * @param array $stack the stack trace + * + * @return array the modified stack trace + */ + private function translateStacktrace(array $stack): array + { + foreach ($stack as &$level) { + foreach ($level["context"] as &$line) { + if (mb_strlen($line) > 80) { + $line = mb_substr($line, 0, 75) . "//..."; + } + } + list($uri, $scriptName) = $this->sanitizeUrl($level["url"]); + $level["uri"] = $uri; + $level["scriptname"] = $scriptName; + unset($level["url"]); + } + unset($level); + return $stack; + } + + /** + * Generates the error report form to collect user description and preview the + * report before being sent + * + * @return string the form + */ + public function getForm(): string + { + $datas = [ + 'report_data' => $this->getPrettyData(), + 'hidden_inputs' => Url::getHiddenInputs(), + 'hidden_fields' => null, + ]; + + $reportData = $this->getData(); + if (! empty($reportData)) { + $datas['hidden_fields'] = Url::getHiddenFields($reportData, '', true); + } + + return $this->template->render('error/report_form', $datas); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Export.php b/srcs/phpmyadmin/libraries/classes/Export.php new file mode 100644 index 0000000..34a16b4 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Export.php @@ -0,0 +1,1225 @@ +dbi = $dbi; + } + + /** + * Sets a session variable upon a possible fatal error during export + * + * @return void + */ + public function shutdown(): void + { + $error = error_get_last(); + if ($error != null && mb_strpos($error['message'], "execution time")) { + //set session variable to check if there was error while exporting + $_SESSION['pma_export_error'] = $error['message']; + } + } + + /** + * Detect ob_gzhandler + * + * @return bool + */ + public function isGzHandlerEnabled(): bool + { + return in_array('ob_gzhandler', ob_list_handlers()); + } + + /** + * Detect whether gzencode is needed; it might not be needed if + * the server is already compressing by itself + * + * @return bool Whether gzencode is needed + */ + public function gzencodeNeeded(): bool + { + /* + * We should gzencode only if the function exists + * but we don't want to compress twice, therefore + * gzencode only if transparent compression is not enabled + * and gz compression was not asked via $cfg['OBGzip'] + * but transparent compression does not apply when saving to server + */ + $chromeAndGreaterThan43 = PMA_USR_BROWSER_AGENT == 'CHROME' + && PMA_USR_BROWSER_VER >= 43; // see bug #4942 + + return function_exists('gzencode') + && ((! ini_get('zlib.output_compression') + && ! $this->isGzHandlerEnabled()) + || $GLOBALS['save_on_server'] + || $chromeAndGreaterThan43); + } + + /** + * Output handler for all exports, if needed buffering, it stores data into + * $dump_buffer, otherwise it prints them out. + * + * @param string $line the insert statement + * + * @return bool Whether output succeeded + */ + public function outputHandler(?string $line): bool + { + global $time_start, $dump_buffer, $dump_buffer_len, $save_filename; + + // Kanji encoding convert feature + if ($GLOBALS['output_kanji_conversion']) { + $line = Encoding::kanjiStrConv( + $line, + $GLOBALS['knjenc'], + isset($GLOBALS['xkana']) ? $GLOBALS['xkana'] : '' + ); + } + + // If we have to buffer data, we will perform everything at once at the end + if ($GLOBALS['buffer_needed']) { + $dump_buffer .= $line; + if ($GLOBALS['onfly_compression']) { + $dump_buffer_len += strlen($line); + + if ($dump_buffer_len > $GLOBALS['memory_limit']) { + if ($GLOBALS['output_charset_conversion']) { + $dump_buffer = Encoding::convertString( + 'utf-8', + $GLOBALS['charset'], + $dump_buffer + ); + } + if ($GLOBALS['compression'] == 'gzip' + && $this->gzencodeNeeded() + ) { + // as a gzipped file + // without the optional parameter level because it bugs + $dump_buffer = gzencode($dump_buffer); + } + if ($GLOBALS['save_on_server']) { + $write_result = @fwrite($GLOBALS['file_handle'], $dump_buffer); + // Here, use strlen rather than mb_strlen to get the length + // in bytes to compare against the number of bytes written. + if ($write_result != strlen($dump_buffer)) { + $GLOBALS['message'] = Message::error( + __('Insufficient space to save the file %s.') + ); + $GLOBALS['message']->addParam($save_filename); + return false; + } + } else { + echo $dump_buffer; + } + $dump_buffer = ''; + $dump_buffer_len = 0; + } + } else { + $time_now = time(); + if ($time_start >= $time_now + 30) { + $time_start = $time_now; + header('X-pmaPing: Pong'); + } // end if + } + } elseif ($GLOBALS['asfile']) { + if ($GLOBALS['output_charset_conversion']) { + $line = Encoding::convertString( + 'utf-8', + $GLOBALS['charset'], + $line + ); + } + if ($GLOBALS['save_on_server'] && mb_strlen($line) > 0) { + if ($GLOBALS['file_handle'] !== null) { + $write_result = @fwrite($GLOBALS['file_handle'], $line); + } else { + $write_result = false; + } + // Here, use strlen rather than mb_strlen to get the length + // in bytes to compare against the number of bytes written. + if (! $write_result + || $write_result != strlen($line) + ) { + $GLOBALS['message'] = Message::error( + __('Insufficient space to save the file %s.') + ); + $GLOBALS['message']->addParam($save_filename); + return false; + } + $time_now = time(); + if ($time_start >= $time_now + 30) { + $time_start = $time_now; + header('X-pmaPing: Pong'); + } // end if + } else { + // We export as file - output normally + echo $line; + } + } else { + // We export as html - replace special chars + echo htmlspecialchars($line); + } + return true; + } + + /** + * Returns HTML containing the footer for a displayed export + * + * @param string $back_button the link for going Back + * @param string $refreshButton the link for refreshing page + * + * @return string the HTML output + */ + public function getHtmlForDisplayedExportFooter( + string $back_button, + string $refreshButton + ): string { + /** + * Close the html tags and add the footers for on-screen export + */ + return '' + . ' ' + . '
' + // bottom back button + . $back_button + . $refreshButton + . '' + . '' . "\n"; + } + + /** + * Computes the memory limit for export + * + * @return int the memory limit + */ + public function getMemoryLimit(): int + { + $memory_limit = trim(ini_get('memory_limit')); + $memory_limit_num = (int) substr($memory_limit, 0, -1); + $lowerLastChar = strtolower(substr($memory_limit, -1)); + // 2 MB as default + if (empty($memory_limit) || '-1' == $memory_limit) { + $memory_limit = 2 * 1024 * 1024; + } elseif ($lowerLastChar == 'm') { + $memory_limit = $memory_limit_num * 1024 * 1024; + } elseif ($lowerLastChar == 'k') { + $memory_limit = $memory_limit_num * 1024; + } elseif ($lowerLastChar == 'g') { + $memory_limit = $memory_limit_num * 1024 * 1024 * 1024; + } else { + $memory_limit = (int) $memory_limit; + } + + // Some of memory is needed for other things and as threshold. + // During export I had allocated (see memory_get_usage function) + // approx 1.2MB so this comes from that. + if ($memory_limit > 1500000) { + $memory_limit -= 1500000; + } + + // Some memory is needed for compression, assume 1/3 + $memory_limit /= 8; + return $memory_limit; + } + + /** + * Return the filename and MIME type for export file + * + * @param string $export_type type of export + * @param string $remember_template whether to remember template + * @param ExportPlugin $export_plugin the export plugin + * @param string $compression compression asked + * @param string $filename_template the filename template + * + * @return string[] the filename template and mime type + */ + public function getFilenameAndMimetype( + string $export_type, + string $remember_template, + ExportPlugin $export_plugin, + string $compression, + string $filename_template + ): array { + if ($export_type == 'server') { + if (! empty($remember_template)) { + $GLOBALS['PMA_Config']->setUserValue( + 'pma_server_filename_template', + 'Export/file_template_server', + $filename_template + ); + } + } elseif ($export_type == 'database') { + if (! empty($remember_template)) { + $GLOBALS['PMA_Config']->setUserValue( + 'pma_db_filename_template', + 'Export/file_template_database', + $filename_template + ); + } + } else { + if (! empty($remember_template)) { + $GLOBALS['PMA_Config']->setUserValue( + 'pma_table_filename_template', + 'Export/file_template_table', + $filename_template + ); + } + } + $filename = Util::expandUserString($filename_template); + // remove dots in filename (coming from either the template or already + // part of the filename) to avoid a remote code execution vulnerability + $filename = Sanitize::sanitizeFilename($filename, $replaceDots = true); + + // Grab basic dump extension and mime type + // Check if the user already added extension; + // get the substring where the extension would be if it was included + $extension_start_pos = mb_strlen($filename) - mb_strlen( + $export_plugin->getProperties()->getExtension() + ) - 1; + $user_extension = mb_substr( + $filename, + $extension_start_pos, + mb_strlen($filename) + ); + $required_extension = "." . $export_plugin->getProperties()->getExtension(); + if (mb_strtolower($user_extension) != $required_extension) { + $filename .= $required_extension; + } + $mime_type = $export_plugin->getProperties()->getMimeType(); + + // If dump is going to be compressed, set correct mime_type and add + // compression to extension + if ($compression == 'gzip') { + $filename .= '.gz'; + $mime_type = 'application/x-gzip'; + } elseif ($compression == 'zip') { + $filename .= '.zip'; + $mime_type = 'application/zip'; + } + return [ + $filename, + $mime_type, + ]; + } + + /** + * Open the export file + * + * @param string $filename the export filename + * @param boolean $quick_export whether it's a quick export or not + * + * @return array the full save filename, possible message and the file handle + */ + public function openFile(string $filename, bool $quick_export): array + { + $file_handle = null; + $message = ''; + $doNotSaveItOver = true; + + if (isset($_POST['quick_export_onserver_overwrite'])) { + $doNotSaveItOver = $_POST['quick_export_onserver_overwrite'] != 'saveitover'; + } + + $save_filename = Util::userDir($GLOBALS['cfg']['SaveDir']) + . preg_replace('@[/\\\\]@', '_', $filename); + + if (@file_exists($save_filename) + && ((! $quick_export && empty($_POST['onserver_overwrite'])) + || ($quick_export + && $doNotSaveItOver)) + ) { + $message = Message::error( + __( + 'File %s already exists on server, ' + . 'change filename or check overwrite option.' + ) + ); + $message->addParam($save_filename); + } elseif (@is_file($save_filename) && ! @is_writable($save_filename)) { + $message = Message::error( + __( + 'The web server does not have permission ' + . 'to save the file %s.' + ) + ); + $message->addParam($save_filename); + } elseif (! $file_handle = @fopen($save_filename, 'w')) { + $message = Message::error( + __( + 'The web server does not have permission ' + . 'to save the file %s.' + ) + ); + $message->addParam($save_filename); + } + return [ + $save_filename, + $message, + $file_handle, + ]; + } + + /** + * Close the export file + * + * @param resource $file_handle the export file handle + * @param string $dump_buffer the current dump buffer + * @param string $save_filename the export filename + * + * @return Message a message object (or empty string) + */ + public function closeFile( + $file_handle, + string $dump_buffer, + string $save_filename + ): Message { + $write_result = @fwrite($file_handle, $dump_buffer); + fclose($file_handle); + // Here, use strlen rather than mb_strlen to get the length + // in bytes to compare against the number of bytes written. + if (strlen($dump_buffer) > 0 + && (! $write_result || $write_result != strlen($dump_buffer)) + ) { + $message = new Message( + __('Insufficient space to save the file %s.'), + Message::ERROR, + [$save_filename] + ); + } else { + $message = new Message( + __('Dump has been saved to file %s.'), + Message::SUCCESS, + [$save_filename] + ); + } + return $message; + } + + /** + * Compress the export buffer + * + * @param array|string $dump_buffer the current dump buffer + * @param string $compression the compression mode + * @param string $filename the filename + * + * @return array|string|bool + */ + public function compress($dump_buffer, string $compression, string $filename) + { + if ($compression == 'zip' && function_exists('gzcompress')) { + $zipExtension = new ZipExtension(); + $filename = substr($filename, 0, -4); // remove extension (.zip) + $dump_buffer = $zipExtension->createFile($dump_buffer, $filename); + } elseif ($compression == 'gzip' && $this->gzencodeNeeded()) { + // without the optional parameter level because it bugs + $dump_buffer = gzencode($dump_buffer); + } + return $dump_buffer; + } + + /** + * Saves the dump_buffer for a particular table in an array + * Used in separate files export + * + * @param string $object_name the name of current object to be stored + * @param boolean $append optional boolean to append to an existing index or not + * + * @return void + */ + public function saveObjectInBuffer(string $object_name, bool $append = false): void + { + global $dump_buffer_objects, $dump_buffer, $dump_buffer_len; + + if (! empty($dump_buffer)) { + if ($append && isset($dump_buffer_objects[$object_name])) { + $dump_buffer_objects[$object_name] .= $dump_buffer; + } else { + $dump_buffer_objects[$object_name] = $dump_buffer; + } + } + + // Re - initialize + $dump_buffer = ''; + $dump_buffer_len = 0; + } + + /** + * Returns HTML containing the header for a displayed export + * + * @param string $export_type the export type + * @param string $db the database name + * @param string $table the table name + * + * @return string[] the generated HTML and back button + */ + public function getHtmlForDisplayedExportHeader( + string $export_type, + string $db, + string $table + ): array { + $html = '
'; + + /** + * Displays a back button with all the $_POST data in the URL + * (store in a variable to also display after the textarea) + */ + $back_button = '

[ ' . __('Back') . ' ]

'; + $html .= '
'; + $html .= $back_button; + $refreshButton = '
'; + $refreshButton .= '[ ' . __('Refresh') . ' ]'; + foreach ($_POST as $name => $value) { + if (is_array($value)) { + foreach ($value as $val) { + $refreshButton .= ''; + } + } else { + $refreshButton .= ''; + } + } + $refreshButton .= '
'; + $html .= $refreshButton + . '
' + . '
' + . ''; + + return $html_output; + } + + /** + * Get HTML for enum type + * + * @param array $column description of column in given table + * @param string $backup_field hidden input field + * @param string $column_name_appendix the name attribute + * @param array $extracted_columnspec associative array containing type, + * spec_in_brackets and possibly + * enum_set_values (another array) + * @param string $onChangeClause onchange clause for fields + * @param integer $tabindex tab index + * @param integer $tabindex_for_value offset for the values tabindex + * @param integer $idindex id index + * @param mixed $data data to edit + * @param boolean $readOnly is column read only or not + * + * @return string an html snippet + */ + private function getPmaTypeEnum( + array $column, + $backup_field, + $column_name_appendix, + array $extracted_columnspec, + $onChangeClause, + $tabindex, + $tabindex_for_value, + $idindex, + $data, + $readOnly + ) { + $html_output = ''; + if (! isset($column['values'])) { + $column['values'] = $this->getColumnEnumValues( + $column, + $extracted_columnspec + ); + } + $column_enum_values = $column['values']; + $html_output .= ''; + $html_output .= "\n" . ' ' . $backup_field . "\n"; + if (mb_strlen($column['Type']) > 20) { + $html_output .= $this->getDropDownDependingOnLength( + $column, + $column_name_appendix, + $onChangeClause, + $tabindex, + $tabindex_for_value, + $idindex, + $data, + $column_enum_values, + $readOnly + ); + } else { + $html_output .= $this->getRadioButtonDependingOnLength( + $column_name_appendix, + $onChangeClause, + $tabindex, + $column, + $tabindex_for_value, + $idindex, + $data, + $column_enum_values, + $readOnly + ); + } + return $html_output; + } + + /** + * Get column values + * + * @param array $column description of column in given table + * @param array $extracted_columnspec associative array containing type, + * spec_in_brackets and possibly enum_set_values + * (another array) + * + * @return array column values as an associative array + */ + private function getColumnEnumValues(array $column, array $extracted_columnspec) + { + $column['values'] = []; + foreach ($extracted_columnspec['enum_set_values'] as $val) { + $column['values'][] = [ + 'plain' => $val, + 'html' => htmlspecialchars($val), + ]; + } + return $column['values']; + } + + /** + * Get HTML drop down for more than 20 string length + * + * @param array $column description of column in given table + * @param string $column_name_appendix the name attribute + * @param string $onChangeClause onchange clause for fields + * @param integer $tabindex tab index + * @param integer $tabindex_for_value offset for the values tabindex + * @param integer $idindex id index + * @param string $data data to edit + * @param array $column_enum_values $column['values'] + * @param boolean $readOnly is column read only or not + * + * @return string an html snippet + */ + private function getDropDownDependingOnLength( + array $column, + $column_name_appendix, + $onChangeClause, + $tabindex, + $tabindex_for_value, + $idindex, + $data, + array $column_enum_values, + $readOnly + ) { + $html_output = ''; + + //Add hidden input, as disabled '; + } + return $html_output; + } + + /** + * Get HTML radio button for less than 20 string length + * + * @param string $column_name_appendix the name attribute + * @param string $onChangeClause onchange clause for fields + * @param integer $tabindex tab index + * @param array $column description of column in given table + * @param integer $tabindex_for_value offset for the values tabindex + * @param integer $idindex id index + * @param string $data data to edit + * @param array $column_enum_values $column['values'] + * @param boolean $readOnly is column read only or not + * + * @return string an html snippet + */ + private function getRadioButtonDependingOnLength( + $column_name_appendix, + $onChangeClause, + $tabindex, + array $column, + $tabindex_for_value, + $idindex, + $data, + array $column_enum_values, + $readOnly + ) { + $j = 0; + $html_output = ''; + foreach ($column_enum_values as $enum_value) { + $html_output .= ' ' + . ''; + $html_output .= '' . "\n"; + $j++; + } + return $html_output; + } + + /** + * Get the HTML for 'set' pma type + * + * @param array $column description of column in given table + * @param array $extracted_columnspec associative array containing type, + * spec_in_brackets and possibly + * enum_set_values (another array) + * @param string $backup_field hidden input field + * @param string $column_name_appendix the name attribute + * @param string $onChangeClause onchange clause for fields + * @param integer $tabindex tab index + * @param integer $tabindex_for_value offset for the values tabindex + * @param integer $idindex id index + * @param string $data description of the column field + * @param boolean $readOnly is column read only or not + * + * @return string an html snippet + */ + private function getPmaTypeSet( + array $column, + array $extracted_columnspec, + $backup_field, + $column_name_appendix, + $onChangeClause, + $tabindex, + $tabindex_for_value, + $idindex, + $data, + $readOnly + ) { + list($column_set_values, $select_size) = $this->getColumnSetValueAndSelectSize( + $column, + $extracted_columnspec + ); + $vset = array_flip(explode(',', $data)); + $html_output = $backup_field . "\n"; + $html_output .= ''; + $html_output .= ''; + + //Add hidden input, as disabled '; + } + return $html_output; + } + + /** + * Retrieve column 'set' value and select size + * + * @param array $column description of column in given table + * @param array $extracted_columnspec associative array containing type, + * spec_in_brackets and possibly enum_set_values + * (another array) + * + * @return array $column['values'], $column['select_size'] + */ + private function getColumnSetValueAndSelectSize( + array $column, + array $extracted_columnspec + ) { + if (! isset($column['values'])) { + $column['values'] = []; + foreach ($extracted_columnspec['enum_set_values'] as $val) { + $column['values'][] = [ + 'plain' => $val, + 'html' => htmlspecialchars($val), + ]; + } + $column['select_size'] = min(4, count($column['values'])); + } + return [ + $column['values'], + $column['select_size'], + ]; + } + + /** + * Get HTML for binary and blob column + * + * @param array $column description of column in given table + * @param string|null $data data to edit + * @param string $special_chars special characters + * @param integer $biggest_max_file_size biggest max file size for uploading + * @param string $backup_field hidden input field + * @param string $column_name_appendix the name attribute + * @param string $onChangeClause onchange clause for fields + * @param integer $tabindex tab index + * @param integer $tabindex_for_value offset for the values tabindex + * @param integer $idindex id index + * @param string $text_dir text direction + * @param string $special_chars_encoded replaced char if the string starts + * with a \r\n pair (0x0d0a) add an + * extra \n + * @param string $vkey [multi_edit]['row_id'] + * @param boolean $is_upload is upload or not + * @param boolean $readOnly is column read only or not + * + * @return string an html snippet + */ + private function getBinaryAndBlobColumn( + array $column, + ?string $data, + $special_chars, + $biggest_max_file_size, + $backup_field, + $column_name_appendix, + $onChangeClause, + $tabindex, + $tabindex_for_value, + $idindex, + $text_dir, + $special_chars_encoded, + $vkey, + $is_upload, + $readOnly + ) { + $html_output = ''; + // Add field type : Protected or Hexadecimal + $fields_type_html = ''; + // Default value : hex + $fields_type_val = 'hex'; + if (($GLOBALS['cfg']['ProtectBinary'] === 'blob' && $column['is_blob']) + || ($GLOBALS['cfg']['ProtectBinary'] === 'all') + || ($GLOBALS['cfg']['ProtectBinary'] === 'noblob' && ! $column['is_blob']) + ) { + $html_output .= __('Binary - do not edit'); + if (isset($data)) { + $data_size = Util::formatByteDown( + mb_strlen(stripslashes($data)), + 3, + 1 + ); + $html_output .= ' (' . $data_size[0] . ' ' . $data_size[1] . ')'; + unset($data_size); + } + $fields_type_val = 'protected'; + $html_output .= ''; + } elseif ($column['is_blob'] + || ($column['len'] > $GLOBALS['cfg']['LimitChars']) + ) { + $html_output .= "\n" . $this->getTextarea( + $column, + $backup_field, + $column_name_appendix, + $onChangeClause, + $tabindex, + $tabindex_for_value, + $idindex, + $text_dir, + $special_chars_encoded, + 'HEX', + $readOnly + ); + } else { + // field size should be at least 4 and max $GLOBALS['cfg']['LimitChars'] + $fieldsize = min(max($column['len'], 4), $GLOBALS['cfg']['LimitChars']); + $html_output .= "\n" . $backup_field . "\n" . $this->getHtmlInput( + $column, + $column_name_appendix, + $special_chars, + $fieldsize, + $onChangeClause, + $tabindex, + $tabindex_for_value, + $idindex, + 'HEX', + $readOnly + ); + } + $html_output .= sprintf($fields_type_html, $fields_type_val); + + if ($is_upload && $column['is_blob'] && ! $readOnly) { + // We don't want to prevent users from using + // browser's default drag-drop feature on some page(s), + // so we add noDragDrop class to the input + $html_output .= '
' + . ' '; + list($html_out,) = $this->getMaxUploadSize( + $column, + $biggest_max_file_size + ); + $html_output .= $html_out; + } + + if (! empty($GLOBALS['cfg']['UploadDir']) && ! $readOnly) { + $html_output .= $this->getSelectOptionForUpload($vkey, $column); + } + + return $html_output; + } + + /** + * Get HTML input type + * + * @param array $column description of column in given table + * @param string $column_name_appendix the name attribute + * @param string $special_chars special characters + * @param integer $fieldsize html field size + * @param string $onChangeClause onchange clause for fields + * @param integer $tabindex tab index + * @param integer $tabindex_for_value offset for the values tabindex + * @param integer $idindex id index + * @param string $data_type the html5 data-* attribute type + * @param boolean $readOnly is column read only or not + * + * @return string an html snippet + */ + private function getHtmlInput( + array $column, + $column_name_appendix, + $special_chars, + $fieldsize, + $onChangeClause, + $tabindex, + $tabindex_for_value, + $idindex, + $data_type, + $readOnly + ) { + $input_type = 'text'; + // do not use the 'date' or 'time' types here; they have no effect on some + // browsers and create side effects (see bug #4218) + + $the_class = 'textfield'; + // verify True_Type which does not contain the parentheses and length + if (! $readOnly) { + if ($column['True_Type'] === 'date') { + $the_class .= ' datefield'; + } elseif ($column['True_Type'] === 'time') { + $the_class .= ' timefield'; + } elseif ($column['True_Type'] === 'datetime' + || $column['True_Type'] === 'timestamp' + ) { + $the_class .= ' datetimefield'; + } + } + $input_min_max = false; + if (in_array($column['True_Type'], $this->dbi->types->getIntegerTypes())) { + $extracted_columnspec = Util::extractColumnSpec( + $column['Type'] + ); + $is_unsigned = $extracted_columnspec['unsigned']; + $min_max_values = $this->dbi->types->getIntegerRange( + $column['True_Type'], + ! $is_unsigned + ); + $input_min_max = 'min="' . $min_max_values[0] . '" ' + . 'max="' . $min_max_values[1] . '"'; + $data_type = 'INT'; + } + return ''; + } + + /** + * Get HTML select option for upload + * + * @param string $vkey [multi_edit]['row_id'] + * @param array $column description of column in given table + * + * @return string|null an html snippet + */ + private function getSelectOptionForUpload($vkey, array $column) + { + $files = $this->fileListing->getFileSelectOptions( + Util::userDir($GLOBALS['cfg']['UploadDir']) + ); + + if ($files === false) { + return '' . __('Error') . '
' . "\n" + . __('The directory you set for upload work cannot be reached.') . "\n"; + } elseif (! empty($files)) { + return "
\n" + . '' . __('Or') . ' ' + . __('web server upload directory:') . '
' . "\n" + . '' . "\n"; + } + + return null; + } + + /** + * Retrieve the maximum upload file size + * + * @param array $column description of column in given table + * @param integer $biggest_max_file_size biggest max file size for uploading + * + * @return array an html snippet and $biggest_max_file_size + */ + private function getMaxUploadSize(array $column, $biggest_max_file_size) + { + // find maximum upload size, based on field type + /** + * @todo with functions this is not so easy, as you can basically + * process any data with function like MD5 + */ + global $max_upload_size; + $max_field_sizes = [ + 'tinyblob' => '256', + 'blob' => '65536', + 'mediumblob' => '16777216', + 'longblob' => '4294967296',// yeah, really + ]; + + $this_field_max_size = $max_upload_size; // from PHP max + if ($this_field_max_size > $max_field_sizes[$column['pma_type']]) { + $this_field_max_size = $max_field_sizes[$column['pma_type']]; + } + $html_output + = Util::getFormattedMaximumUploadSize( + $this_field_max_size + ) . "\n"; + // do not generate here the MAX_FILE_SIZE, because we should + // put only one in the form to accommodate the biggest field + if ($this_field_max_size > $biggest_max_file_size) { + $biggest_max_file_size = $this_field_max_size; + } + return [ + $html_output, + $biggest_max_file_size, + ]; + } + + /** + * Get HTML for the Value column of other datatypes + * (here, "column" is used in the sense of HTML column in HTML table) + * + * @param array $column description of column in given table + * @param string $default_char_editing default char editing mode which is stored + * in the config.inc.php script + * @param string $backup_field hidden input field + * @param string $column_name_appendix the name attribute + * @param string $onChangeClause onchange clause for fields + * @param integer $tabindex tab index + * @param string $special_chars special characters + * @param integer $tabindex_for_value offset for the values tabindex + * @param integer $idindex id index + * @param string $text_dir text direction + * @param string $special_chars_encoded replaced char if the string starts + * with a \r\n pair (0x0d0a) add an extra \n + * @param string $data data to edit + * @param array $extracted_columnspec associative array containing type, + * spec_in_brackets and possibly + * enum_set_values (another array) + * @param boolean $readOnly is column read only or not + * + * @return string an html snippet + */ + private function getValueColumnForOtherDatatypes( + array $column, + $default_char_editing, + $backup_field, + $column_name_appendix, + $onChangeClause, + $tabindex, + $special_chars, + $tabindex_for_value, + $idindex, + $text_dir, + $special_chars_encoded, + $data, + array $extracted_columnspec, + $readOnly + ) { + // HTML5 data-* attribute data-type + $data_type = $this->dbi->types->getTypeClass($column['True_Type']); + $fieldsize = $this->getColumnSize($column, $extracted_columnspec); + $html_output = $backup_field . "\n"; + if ($column['is_char'] + && ($GLOBALS['cfg']['CharEditing'] == 'textarea' + || mb_strpos($data, "\n") !== false) + ) { + $html_output .= "\n"; + $GLOBALS['cfg']['CharEditing'] = $default_char_editing; + $html_output .= $this->getTextarea( + $column, + $backup_field, + $column_name_appendix, + $onChangeClause, + $tabindex, + $tabindex_for_value, + $idindex, + $text_dir, + $special_chars_encoded, + $data_type, + $readOnly + ); + } else { + $html_output .= $this->getHtmlInput( + $column, + $column_name_appendix, + $special_chars, + $fieldsize, + $onChangeClause, + $tabindex, + $tabindex_for_value, + $idindex, + $data_type, + $readOnly + ); + + if (preg_match('/(VIRTUAL|PERSISTENT|GENERATED)/', $column['Extra']) && $column['Extra'] !== 'DEFAULT_GENERATED') { + $html_output .= ''; + } + if ($column['Extra'] == 'auto_increment') { + $html_output .= ''; + } + if (substr($column['pma_type'], 0, 9) == 'timestamp') { + $html_output .= ''; + } + if (substr($column['pma_type'], 0, 8) == 'datetime') { + $html_output .= ''; + } + if ($column['True_Type'] == 'bit') { + $html_output .= ''; + } + } + return $html_output; + } + + /** + * Get the field size + * + * @param array $column description of column in given table + * @param array $extracted_columnspec associative array containing type, + * spec_in_brackets and possibly enum_set_values + * (another array) + * + * @return integer field size + */ + private function getColumnSize(array $column, array $extracted_columnspec) + { + if ($column['is_char']) { + $fieldsize = $extracted_columnspec['spec_in_brackets']; + if ($fieldsize > $GLOBALS['cfg']['MaxSizeForInputField']) { + /** + * This case happens for CHAR or VARCHAR columns which have + * a size larger than the maximum size for input field. + */ + $GLOBALS['cfg']['CharEditing'] = 'textarea'; + } + } else { + /** + * This case happens for example for INT or DATE columns; + * in these situations, the value returned in $column['len'] + * seems appropriate. + */ + $fieldsize = $column['len']; + } + return min( + max($fieldsize, $GLOBALS['cfg']['MinSizeForInputField']), + $GLOBALS['cfg']['MaxSizeForInputField'] + ); + } + + /** + * Get HTML for gis data types + * + * @return string an html snippet + */ + private function getHtmlForGisDataTypes() + { + $edit_str = Util::getIcon('b_edit', __('Edit/Insert')); + return '' + . Util::linkOrButton( + '#', + $edit_str, + [], + '_blank' + ) + . ''; + } + + /** + * get html for continue insertion form + * + * @param string $table name of the table + * @param string $db name of the database + * @param array $where_clause_array array of where clauses + * @param string $err_url error url + * + * @return string an html snippet + */ + public function getContinueInsertionForm( + $table, + $db, + array $where_clause_array, + $err_url + ) { + return $this->template->render('table/insert/continue_insertion_form', [ + 'db' => $db, + 'table' => $table, + 'where_clause_array' => $where_clause_array, + 'err_url' => $err_url, + 'goto' => $GLOBALS['goto'], + 'sql_query' => isset($_POST['sql_query']) ? $_POST['sql_query'] : null, + 'has_where_clause' => isset($_POST['where_clause']), + 'insert_rows_default' => $GLOBALS['cfg']['InsertRows'], + ]); + } + + /** + * Get action panel + * + * @param array|null $where_clause where clause + * @param string $after_insert insert mode, e.g. new_insert, same_insert + * @param integer $tabindex tab index + * @param integer $tabindex_for_value offset for the values tabindex + * @param boolean $found_unique_key boolean variable for unique key + * + * @return string an html snippet + */ + public function getActionsPanel( + $where_clause, + $after_insert, + $tabindex, + $tabindex_for_value, + $found_unique_key + ) { + $html_output = '
' + . '' + . '' + . '' + . '' + . '' + . ''; + $html_output .= '' + . $this->getSubmitAndResetButtonForActionsPanel($tabindex, $tabindex_for_value) + . '' + . '
' + . $this->getSubmitTypeDropDown($where_clause, $tabindex, $tabindex_for_value) + . "\n"; + + $html_output .= '' + . '   ' + . __('and then') . '   ' + . '' + . $this->getAfterInsertDropDown( + $where_clause, + $after_insert, + $found_unique_key + ) + . '
' + . '
'; + return $html_output; + } + + /** + * Get a HTML drop down for submit types + * + * @param array|null $where_clause where clause + * @param integer $tabindex tab index + * @param integer $tabindex_for_value offset for the values tabindex + * + * @return string an html snippet + */ + private function getSubmitTypeDropDown( + $where_clause, + $tabindex, + $tabindex_for_value + ) { + $html_output = ''; + return $html_output; + } + + /** + * Get HTML drop down for after insert + * + * @param array|null $where_clause where clause + * @param string $after_insert insert mode, e.g. new_insert, same_insert + * @param boolean $found_unique_key boolean variable for unique key + * + * @return string an html snippet + */ + private function getAfterInsertDropDown($where_clause, $after_insert, $found_unique_key) + { + $html_output = ''; + return $html_output; + } + + /** + * get Submit button and Reset button for action panel + * + * @param integer $tabindex tab index + * @param integer $tabindex_for_value offset for the values tabindex + * + * @return string an html snippet + */ + private function getSubmitAndResetButtonForActionsPanel($tabindex, $tabindex_for_value) + { + return '' + . Util::showHint( + __( + 'Use TAB key to move from value to value,' + . ' or CTRL+arrows to move anywhere.' + ) + ) + . '' + . '' + . '' + . '' + . '' + . ''; + } + + /** + * Get table head and table foot for insert row table + * + * @param array $url_params url parameters + * + * @return string an html snippet + */ + private function getHeadAndFootOfInsertRowTable(array $url_params) + { + $html_output = '
' + . '' + . '' + . '' + . ''; + + if ($GLOBALS['cfg']['ShowFieldTypesInDataEditView']) { + $html_output .= $this->showTypeOrFunction('type', $url_params, true); + } + if ($GLOBALS['cfg']['ShowFunctionFields']) { + $html_output .= $this->showTypeOrFunction('function', $url_params, true); + } + + $html_output .= '' + . '' + . '' + . '' + . ' ' + . '' + . '' + . '' + . ''; + return $html_output; + } + + /** + * Prepares the field value and retrieve special chars, backup field and data array + * + * @param array $current_row a row of the table + * @param array $column description of column in given table + * @param array $extracted_columnspec associative array containing type, + * spec_in_brackets and possibly + * enum_set_values (another array) + * @param boolean $real_null_value whether column value null or not null + * @param array $gis_data_types list of GIS data types + * @param string $column_name_appendix string to append to column name in input + * @param bool $as_is use the data as is, used in repopulating + * + * @return array $real_null_value, $data, $special_chars, $backup_field, + * $special_chars_encoded + */ + private function getSpecialCharsAndBackupFieldForExistingRow( + array $current_row, + array $column, + array $extracted_columnspec, + $real_null_value, + array $gis_data_types, + $column_name_appendix, + $as_is + ) { + $special_chars_encoded = ''; + $data = null; + // (we are editing) + if (! isset($current_row[$column['Field']])) { + $real_null_value = true; + $current_row[$column['Field']] = ''; + $special_chars = ''; + $data = $current_row[$column['Field']]; + } elseif ($column['True_Type'] == 'bit') { + $special_chars = $as_is + ? $current_row[$column['Field']] + : Util::printableBitValue( + (int) $current_row[$column['Field']], + (int) $extracted_columnspec['spec_in_brackets'] + ); + } elseif ((substr($column['True_Type'], 0, 9) == 'timestamp' + || $column['True_Type'] == 'datetime' + || $column['True_Type'] == 'time') + && (mb_strpos($current_row[$column['Field']], ".") !== false) + ) { + $current_row[$column['Field']] = $as_is + ? $current_row[$column['Field']] + : Util::addMicroseconds( + $current_row[$column['Field']] + ); + $special_chars = htmlspecialchars($current_row[$column['Field']]); + } elseif (in_array($column['True_Type'], $gis_data_types)) { + // Convert gis data to Well Know Text format + $current_row[$column['Field']] = $as_is + ? $current_row[$column['Field']] + : Util::asWKT( + $current_row[$column['Field']], + true + ); + $special_chars = htmlspecialchars($current_row[$column['Field']]); + } else { + // special binary "characters" + if ($column['is_binary'] + || ($column['is_blob'] && $GLOBALS['cfg']['ProtectBinary'] !== 'all') + ) { + $current_row[$column['Field']] = $as_is + ? $current_row[$column['Field']] + : bin2hex( + $current_row[$column['Field']] + ); + } // end if + $special_chars = htmlspecialchars($current_row[$column['Field']]); + + //We need to duplicate the first \n or otherwise we will lose + //the first newline entered in a VARCHAR or TEXT column + $special_chars_encoded + = Util::duplicateFirstNewline($special_chars); + + $data = $current_row[$column['Field']]; + } // end if... else... + + //when copying row, it is useful to empty auto-increment column + // to prevent duplicate key error + if (isset($_POST['default_action']) + && $_POST['default_action'] === 'insert' + ) { + if ($column['Key'] === 'PRI' + && mb_strpos($column['Extra'], 'auto_increment') !== false + ) { + $data = $special_chars_encoded = $special_chars = null; + } + } + // If a timestamp field value is not included in an update + // statement MySQL auto-update it to the current timestamp; + // however, things have changed since MySQL 4.1, so + // it's better to set a fields_prev in this situation + $backup_field = ''; + + return [ + $real_null_value, + $special_chars_encoded, + $special_chars, + $data, + $backup_field, + ]; + } + + /** + * display default values + * + * @param array $column description of column in given table + * @param boolean $real_null_value whether column value null or not null + * + * @return array $real_null_value, $data, $special_chars, + * $backup_field, $special_chars_encoded + */ + private function getSpecialCharsAndBackupFieldForInsertingMode( + array $column, + $real_null_value + ) { + if (! isset($column['Default'])) { + $column['Default'] = ''; + $real_null_value = true; + $data = ''; + } else { + $data = $column['Default']; + } + + $trueType = $column['True_Type']; + + if ($trueType == 'bit') { + $special_chars = Util::convertBitDefaultValue( + $column['Default'] + ); + } elseif (substr($trueType, 0, 9) == 'timestamp' + || $trueType == 'datetime' + || $trueType == 'time' + ) { + $special_chars = Util::addMicroseconds($column['Default']); + } elseif ($trueType == 'binary' || $trueType == 'varbinary') { + $special_chars = bin2hex($column['Default']); + } elseif ('text' === substr($trueType, -4)) { + $textDefault = substr($column['Default'], 1, -1); + $special_chars = stripcslashes($textDefault !== false ? $textDefault : $column['Default']); + } else { + $special_chars = htmlspecialchars($column['Default']); + } + $backup_field = ''; + $special_chars_encoded = Util::duplicateFirstNewline( + $special_chars + ); + return [ + $real_null_value, + $data, + $special_chars, + $backup_field, + $special_chars_encoded, + ]; + } + + /** + * Prepares the update/insert of a row + * + * @return array $loop_array, $using_key, $is_insert, $is_insertignore + */ + public function getParamsForUpdateOrInsert() + { + if (isset($_POST['where_clause'])) { + // we were editing something => use the WHERE clause + $loop_array = is_array($_POST['where_clause']) + ? $_POST['where_clause'] + : [$_POST['where_clause']]; + $using_key = true; + $is_insert = isset($_POST['submit_type']) + && ($_POST['submit_type'] == 'insert' + || $_POST['submit_type'] == 'showinsert' + || $_POST['submit_type'] == 'insertignore'); + } else { + // new row => use indexes + $loop_array = []; + if (! empty($_POST['fields'])) { + foreach ($_POST['fields']['multi_edit'] as $key => $dummy) { + $loop_array[] = $key; + } + } + $using_key = false; + $is_insert = true; + } + $is_insertignore = isset($_POST['submit_type']) + && $_POST['submit_type'] == 'insertignore'; + return [ + $loop_array, + $using_key, + $is_insert, + $is_insertignore, + ]; + } + + /** + * Check wether insert row mode and if so include tbl_changen script and set + * global variables. + * + * @return void + */ + public function isInsertRow() + { + if (isset($_POST['insert_rows']) + && is_numeric($_POST['insert_rows']) + && $_POST['insert_rows'] != $GLOBALS['cfg']['InsertRows'] + ) { + $GLOBALS['cfg']['InsertRows'] = $_POST['insert_rows']; + $response = Response::getInstance(); + $header = $response->getHeader(); + $scripts = $header->getScripts(); + $scripts->addFile('vendor/jquery/additional-methods.js'); + $scripts->addFile('table/change.js'); + if (! defined('TESTSUITE')) { + include ROOT_PATH . 'tbl_change.php'; + exit; + } + } + } + + /** + * set $_SESSION for edit_next + * + * @param string $one_where_clause one where clause from where clauses array + * + * @return void + */ + public function setSessionForEditNext($one_where_clause) + { + $local_query = 'SELECT * FROM ' . Util::backquote($GLOBALS['db']) + . '.' . Util::backquote($GLOBALS['table']) . ' WHERE ' + . str_replace('` =', '` >', $one_where_clause) . ' LIMIT 1;'; + + $res = $this->dbi->query($local_query); + $row = $this->dbi->fetchRow($res); + $meta = $this->dbi->getFieldsMeta($res); + // must find a unique condition based on unique key, + // not a combination of all fields + list($unique_condition, $clause_is_unique) + = Util::getUniqueCondition( + $res, // handle + count($meta), // fields_cnt + $meta, // fields_meta + $row, // row + true, // force_unique + false, // restrict_to_table + null // analyzed_sql_results + ); + if (! empty($unique_condition)) { + $_SESSION['edit_next'] = $unique_condition; + } + unset($unique_condition, $clause_is_unique); + } + + /** + * set $goto_include variable for different cases and retrieve like, + * if $GLOBALS['goto'] empty, if $goto_include previously not defined + * and new_insert, same_insert, edit_next + * + * @param string $goto_include store some script for include, otherwise it is + * boolean false + * + * @return string + */ + public function getGotoInclude($goto_include) + { + $valid_options = [ + 'new_insert', + 'same_insert', + 'edit_next', + ]; + if (isset($_POST['after_insert']) + && in_array($_POST['after_insert'], $valid_options) + ) { + $goto_include = 'tbl_change.php'; + } elseif (! empty($GLOBALS['goto'])) { + if (! preg_match('@^[a-z_]+\.php$@', $GLOBALS['goto'])) { + // this should NOT happen + //$GLOBALS['goto'] = false; + $goto_include = false; + } else { + $goto_include = $GLOBALS['goto']; + } + if ($GLOBALS['goto'] == 'db_sql.php' && strlen($GLOBALS['table']) > 0) { + $GLOBALS['table'] = ''; + } + } + if (! $goto_include) { + if (strlen($GLOBALS['table']) === 0) { + $goto_include = 'db_sql.php'; + } else { + $goto_include = 'tbl_sql.php'; + } + } + return $goto_include; + } + + /** + * Defines the url to return in case of failure of the query + * + * @param array $url_params url parameters + * + * @return string error url for query failure + */ + public function getErrorUrl(array $url_params) + { + if (isset($_POST['err_url'])) { + return $_POST['err_url']; + } + + return 'tbl_change.php' . Url::getCommon($url_params); + } + + /** + * Builds the sql query + * + * @param boolean $is_insertignore $_POST['submit_type'] == 'insertignore' + * @param array $query_fields column names array + * @param array $value_sets array of query values + * + * @return array of query + */ + public function buildSqlQuery($is_insertignore, array $query_fields, array $value_sets) + { + if ($is_insertignore) { + $insert_command = 'INSERT IGNORE '; + } else { + $insert_command = 'INSERT '; + } + $query = [ + $insert_command . 'INTO ' + . Util::backquote($GLOBALS['table']) + . ' (' . implode(', ', $query_fields) . ') VALUES (' + . implode('), (', $value_sets) . ')', + ]; + return $query; + } + + /** + * Executes the sql query and get the result, then move back to the calling page + * + * @param array $url_params url parameters array + * @param array $query built query from buildSqlQuery() + * + * @return array $url_params, $total_affected_rows, $last_messages + * $warning_messages, $error_messages, $return_to_sql_query + */ + public function executeSqlQuery(array $url_params, array $query) + { + $return_to_sql_query = ''; + if (! empty($GLOBALS['sql_query'])) { + $url_params['sql_query'] = $GLOBALS['sql_query']; + $return_to_sql_query = $GLOBALS['sql_query']; + } + $GLOBALS['sql_query'] = implode('; ', $query) . ';'; + // to ensure that the query is displayed in case of + // "insert as new row" and then "insert another new row" + $GLOBALS['display_query'] = $GLOBALS['sql_query']; + + $total_affected_rows = 0; + $last_messages = []; + $warning_messages = []; + $error_messages = []; + + foreach ($query as $single_query) { + if ($_POST['submit_type'] == 'showinsert') { + $last_messages[] = Message::notice(__('Showing SQL query')); + continue; + } + if ($GLOBALS['cfg']['IgnoreMultiSubmitErrors']) { + $result = $this->dbi->tryQuery($single_query); + } else { + $result = $this->dbi->query($single_query); + } + if (! $result) { + $error_messages[] = $this->dbi->getError(); + } else { + // The next line contains a real assignment, it's not a typo + if ($tmp = @$this->dbi->affectedRows()) { + $total_affected_rows += $tmp; + } + unset($tmp); + + $insert_id = $this->dbi->insertId(); + if ($insert_id != 0) { + // insert_id is id of FIRST record inserted in one insert, so if we + // inserted multiple rows, we had to increment this + + if ($total_affected_rows > 0) { + $insert_id += $total_affected_rows - 1; + } + $last_message = Message::notice(__('Inserted row id: %1$d')); + $last_message->addParam($insert_id); + $last_messages[] = $last_message; + } + $this->dbi->freeResult($result); + } + $warning_messages = $this->getWarningMessages(); + } + return [ + $url_params, + $total_affected_rows, + $last_messages, + $warning_messages, + $error_messages, + $return_to_sql_query, + ]; + } + + /** + * get the warning messages array + * + * @return array + */ + private function getWarningMessages() + { + $warning_essages = []; + foreach ($this->dbi->getWarnings() as $warning) { + $warning_essages[] = Message::sanitize( + $warning['Level'] . ': #' . $warning['Code'] . ' ' . $warning['Message'] + ); + } + return $warning_essages; + } + + /** + * Column to display from the foreign table? + * + * @param string $where_comparison string that contain relation field value + * @param array $map all Relations to foreign tables for a given + * table or optionally a given column in a table + * @param string $relation_field relation field + * + * @return string display value from the foreign table + */ + public function getDisplayValueForForeignTableColumn( + $where_comparison, + array $map, + $relation_field + ) { + $foreigner = $this->relation->searchColumnInForeigners($map, $relation_field); + $display_field = $this->relation->getDisplayField( + $foreigner['foreign_db'], + $foreigner['foreign_table'] + ); + // Field to display from the foreign table? + if (is_string($display_field) && strlen($display_field) > 0) { + $dispsql = 'SELECT ' . Util::backquote($display_field) + . ' FROM ' . Util::backquote($foreigner['foreign_db']) + . '.' . Util::backquote($foreigner['foreign_table']) + . ' WHERE ' . Util::backquote($foreigner['foreign_field']) + . $where_comparison; + $dispresult = $this->dbi->tryQuery( + $dispsql, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + if ($dispresult && $this->dbi->numRows($dispresult) > 0) { + list($dispval) = $this->dbi->fetchRow($dispresult); + } else { + $dispval = ''; + } + if ($dispresult) { + $this->dbi->freeResult($dispresult); + } + return $dispval; + } + return ''; + } + + /** + * Display option in the cell according to user choices + * + * @param array $map all Relations to foreign tables for a given + * table or optionally a given column in a table + * @param string $relation_field relation field + * @param string $where_comparison string that contain relation field value + * @param string $dispval display value from the foreign table + * @param string $relation_field_value relation field value + * + * @return string HTML tag + */ + public function getLinkForRelationalDisplayField( + array $map, + $relation_field, + $where_comparison, + $dispval, + $relation_field_value + ) { + $foreigner = $this->relation->searchColumnInForeigners($map, $relation_field); + if ('K' == $_SESSION['tmpval']['relational_display']) { + // user chose "relational key" in the display options, so + // the title contains the display field + $title = ! empty($dispval) + ? ' title="' . htmlspecialchars($dispval) . '"' + : ''; + } else { + $title = ' title="' . htmlspecialchars($relation_field_value) . '"'; + } + $_url_params = [ + 'db' => $foreigner['foreign_db'], + 'table' => $foreigner['foreign_table'], + 'pos' => '0', + 'sql_query' => 'SELECT * FROM ' + . Util::backquote($foreigner['foreign_db']) + . '.' . Util::backquote($foreigner['foreign_table']) + . ' WHERE ' . Util::backquote($foreigner['foreign_field']) + . $where_comparison, + ]; + $output = ''; + + if ('D' == $_SESSION['tmpval']['relational_display']) { + // user chose "relational display field" in the + // display options, so show display field in the cell + $output .= ! empty($dispval) ? htmlspecialchars($dispval) : ''; + } else { + // otherwise display data in the cell + $output .= htmlspecialchars($relation_field_value); + } + $output .= ''; + return $output; + } + + /** + * Transform edited values + * + * @param string $db db name + * @param string $table table name + * @param array $transformation mimetypes for all columns of a table + * [field_name][field_key] + * @param array $edited_values transform columns list and new values + * @param string $file file containing the transformation plugin + * @param string $column_name column name + * @param array $extra_data extra data array + * @param string $type the type of transformation + * + * @return array + */ + public function transformEditedValues( + $db, + $table, + array $transformation, + array &$edited_values, + $file, + $column_name, + array $extra_data, + $type + ) { + $include_file = 'libraries/classes/Plugins/Transformations/' . $file; + if (is_file($include_file)) { + $_url_params = [ + 'db' => $db, + 'table' => $table, + 'where_clause' => $_POST['where_clause'], + 'transform_key' => $column_name, + ]; + $transform_options = $this->transformations->getOptions( + isset($transformation[$type . '_options']) + ? $transformation[$type . '_options'] + : '' + ); + $transform_options['wrapper_link'] = Url::getCommon($_url_params); + $class_name = $this->transformations->getClassName($include_file); + if (class_exists($class_name)) { + /** @var TransformationsPlugin $transformation_plugin */ + $transformation_plugin = new $class_name(); + + foreach ($edited_values as $cell_index => $curr_cell_edited_values) { + if (isset($curr_cell_edited_values[$column_name])) { + $edited_values[$cell_index][$column_name] + = $extra_data['transformations'][$cell_index] + = $transformation_plugin->applyTransformation( + $curr_cell_edited_values[$column_name], + $transform_options + ); + } + } // end of loop for each transformation cell + } + } + return $extra_data; + } + + /** + * Get current value in multi edit mode + * + * @param array $multi_edit_funcs multiple edit functions array + * @param array $multi_edit_salt multiple edit array with encryption salt + * @param array $gis_from_text_functions array that contains gis from text functions + * @param string $current_value current value in the column + * @param array $gis_from_wkb_functions initially $val is $multi_edit_columns[$key] + * @param array $func_optional_param array('RAND','UNIX_TIMESTAMP') + * @param array $func_no_param array of set of string + * @param string $key an md5 of the column name + * + * @return string + */ + public function getCurrentValueAsAnArrayForMultipleEdit( + $multi_edit_funcs, + $multi_edit_salt, + $gis_from_text_functions, + $current_value, + $gis_from_wkb_functions, + $func_optional_param, + $func_no_param, + $key + ) { + if (empty($multi_edit_funcs[$key])) { + return $current_value; + } elseif ('UUID' === $multi_edit_funcs[$key]) { + /* This way user will know what UUID new row has */ + $uuid = $this->dbi->fetchValue('SELECT UUID()'); + return "'" . $uuid . "'"; + } elseif ((in_array($multi_edit_funcs[$key], $gis_from_text_functions) + && substr($current_value, 0, 3) == "'''") + || in_array($multi_edit_funcs[$key], $gis_from_wkb_functions) + ) { + // Remove enclosing apostrophes + $current_value = mb_substr($current_value, 1, -1); + // Remove escaping apostrophes + $current_value = str_replace("''", "'", $current_value); + return $multi_edit_funcs[$key] . '(' . $current_value . ')'; + } elseif (! in_array($multi_edit_funcs[$key], $func_no_param) + || ($current_value != "''" + && in_array($multi_edit_funcs[$key], $func_optional_param)) + ) { + if ((isset($multi_edit_salt[$key]) + && ($multi_edit_funcs[$key] == "AES_ENCRYPT" + || $multi_edit_funcs[$key] == "AES_DECRYPT")) + || (! empty($multi_edit_salt[$key]) + && ($multi_edit_funcs[$key] == "DES_ENCRYPT" + || $multi_edit_funcs[$key] == "DES_DECRYPT" + || $multi_edit_funcs[$key] == "ENCRYPT")) + ) { + return $multi_edit_funcs[$key] . '(' . $current_value . ",'" + . $this->dbi->escapeString($multi_edit_salt[$key]) . "')"; + } + + return $multi_edit_funcs[$key] . '(' . $current_value . ')'; + } + + return $multi_edit_funcs[$key] . '()'; + } + + /** + * Get query values array and query fields array for insert and update in multi edit + * + * @param array $multi_edit_columns_name multiple edit columns name array + * @param array $multi_edit_columns_null multiple edit columns null array + * @param string $current_value current value in the column in loop + * @param array $multi_edit_columns_prev multiple edit previous columns array + * @param array $multi_edit_funcs multiple edit functions array + * @param boolean $is_insert boolean value whether insert or not + * @param array $query_values SET part of the sql query + * @param array $query_fields array of query fields + * @param string $current_value_as_an_array current value in the column + * as an array + * @param array $value_sets array of valu sets + * @param string $key an md5 of the column name + * @param array $multi_edit_columns_null_prev array of multiple edit columns + * null previous + * + * @return array ($query_values, $query_fields) + */ + public function getQueryValuesForInsertAndUpdateInMultipleEdit( + $multi_edit_columns_name, + $multi_edit_columns_null, + $current_value, + $multi_edit_columns_prev, + $multi_edit_funcs, + $is_insert, + $query_values, + $query_fields, + $current_value_as_an_array, + $value_sets, + $key, + $multi_edit_columns_null_prev + ) { + // i n s e r t + if ($is_insert) { + // no need to add column into the valuelist + if (strlen($current_value_as_an_array) > 0) { + $query_values[] = $current_value_as_an_array; + // first inserted row so prepare the list of fields + if (empty($value_sets)) { + $query_fields[] = Util::backquote( + $multi_edit_columns_name[$key] + ); + } + } + } elseif (! empty($multi_edit_columns_null_prev[$key]) + && ! isset($multi_edit_columns_null[$key]) + ) { + // u p d a t e + + // field had the null checkbox before the update + // field no longer has the null checkbox + $query_values[] + = Util::backquote($multi_edit_columns_name[$key]) + . ' = ' . $current_value_as_an_array; + } elseif (! (empty($multi_edit_funcs[$key]) + && isset($multi_edit_columns_prev[$key]) + && (("'" . $this->dbi->escapeString($multi_edit_columns_prev[$key]) . "'" === $current_value) + || ('0x' . $multi_edit_columns_prev[$key] === $current_value))) + && ! empty($current_value) + ) { + // avoid setting a field to NULL when it's already NULL + // (field had the null checkbox before the update + // field still has the null checkbox) + if (empty($multi_edit_columns_null_prev[$key]) + || empty($multi_edit_columns_null[$key]) + ) { + $query_values[] + = Util::backquote($multi_edit_columns_name[$key]) + . ' = ' . $current_value_as_an_array; + } + } + return [ + $query_values, + $query_fields, + ]; + } + + /** + * Get the current column value in the form for different data types + * + * @param string|false $possibly_uploaded_val uploaded file content + * @param string $key an md5 of the column name + * @param array|null $multi_edit_columns_type array of multi edit column types + * @param string $current_value current column value in the form + * @param array|null $multi_edit_auto_increment multi edit auto increment + * @param integer $rownumber index of where clause array + * @param array $multi_edit_columns_name multi edit column names array + * @param array $multi_edit_columns_null multi edit columns null array + * @param array $multi_edit_columns_null_prev multi edit columns previous null + * @param boolean $is_insert whether insert or not + * @param boolean $using_key whether editing or new row + * @param string $where_clause where clause + * @param string $table table name + * @param array $multi_edit_funcs multiple edit functions array + * + * @return string current column value in the form + */ + public function getCurrentValueForDifferentTypes( + $possibly_uploaded_val, + $key, + ?array $multi_edit_columns_type, + $current_value, + ?array $multi_edit_auto_increment, + $rownumber, + $multi_edit_columns_name, + $multi_edit_columns_null, + $multi_edit_columns_null_prev, + $is_insert, + $using_key, + $where_clause, + $table, + $multi_edit_funcs + ) { + // Fetch the current values of a row to use in case we have a protected field + if ($is_insert + && $using_key && isset($multi_edit_columns_type) + && is_array($multi_edit_columns_type) && ! empty($where_clause) + ) { + $protected_row = $this->dbi->fetchSingleRow( + 'SELECT * FROM ' . Util::backquote($table) + . ' WHERE ' . $where_clause . ';' + ); + } + + if (false !== $possibly_uploaded_val) { + $current_value = $possibly_uploaded_val; + } elseif (! empty($multi_edit_funcs[$key])) { + $current_value = "'" . $this->dbi->escapeString($current_value) + . "'"; + } else { + // c o l u m n v a l u e i n t h e f o r m + if (isset($multi_edit_columns_type[$key])) { + $type = $multi_edit_columns_type[$key]; + } else { + $type = ''; + } + + if ($type != 'protected' && $type != 'set' && strlen($current_value) === 0) { + // best way to avoid problems in strict mode + // (works also in non-strict mode) + if (isset($multi_edit_auto_increment) + && isset($multi_edit_auto_increment[$key]) + ) { + $current_value = 'NULL'; + } else { + $current_value = "''"; + } + } elseif ($type == 'set') { + if (! empty($_POST['fields']['multi_edit'][$rownumber][$key])) { + $current_value = implode( + ',', + $_POST['fields']['multi_edit'][$rownumber][$key] + ); + $current_value = "'" + . $this->dbi->escapeString($current_value) . "'"; + } else { + $current_value = "''"; + } + } elseif ($type == 'protected') { + // here we are in protected mode (asked in the config) + // so tbl_change has put this special value in the + // columns array, so we do not change the column value + // but we can still handle column upload + + // when in UPDATE mode, do not alter field's contents. When in INSERT + // mode, insert empty field because no values were submitted. + // If protected blobs where set, insert original fields content. + if (! empty($protected_row[$multi_edit_columns_name[$key]])) { + $current_value = '0x' + . bin2hex($protected_row[$multi_edit_columns_name[$key]]); + } else { + $current_value = ''; + } + } elseif ($type === 'hex') { + if (substr($current_value, 0, 2) != '0x') { + $current_value = '0x' . $current_value; + } + } elseif ($type == 'bit') { + $current_value = preg_replace('/[^01]/', '0', $current_value); + $current_value = "b'" . $this->dbi->escapeString($current_value) + . "'"; + } elseif (! ($type == 'datetime' || $type == 'timestamp') + || ($current_value != 'CURRENT_TIMESTAMP' + && $current_value != 'current_timestamp()') + ) { + $current_value = "'" . $this->dbi->escapeString($current_value) + . "'"; + } + + // Was the Null checkbox checked for this field? + // (if there is a value, we ignore the Null checkbox: this could + // be possible if Javascript is disabled in the browser) + if (! empty($multi_edit_columns_null[$key]) + && ($current_value == "''" || $current_value == '') + ) { + $current_value = 'NULL'; + } + + // The Null checkbox was unchecked for this field + if (empty($current_value) + && ! empty($multi_edit_columns_null_prev[$key]) + && ! isset($multi_edit_columns_null[$key]) + ) { + $current_value = "''"; + } + } // end else (column value in the form) + return $current_value; + } + + /** + * Check whether inline edited value can be truncated or not, + * and add additional parameters for extra_data array if needed + * + * @param string $db Database name + * @param string $table Table name + * @param string $column_name Column name + * @param array $extra_data Extra data for ajax response + * + * @return void + */ + public function verifyWhetherValueCanBeTruncatedAndAppendExtraData( + $db, + $table, + $column_name, + array &$extra_data + ) { + $extra_data['isNeedToRecheck'] = false; + + $sql_for_real_value = 'SELECT ' . Util::backquote($table) . '.' + . Util::backquote($column_name) + . ' FROM ' . Util::backquote($db) . '.' + . Util::backquote($table) + . ' WHERE ' . $_POST['where_clause'][0]; + + $result = $this->dbi->tryQuery($sql_for_real_value); + $fields_meta = $this->dbi->getFieldsMeta($result); + $meta = $fields_meta[0]; + if ($row = $this->dbi->fetchRow($result)) { + $new_value = $row[0]; + if ((substr($meta->type, 0, 9) == 'timestamp') + || ($meta->type == 'datetime') + || ($meta->type == 'time') + ) { + $new_value = Util::addMicroseconds($new_value); + } elseif (mb_strpos($meta->flags, 'binary') !== false) { + $new_value = '0x' . bin2hex($new_value); + } + $extra_data['isNeedToRecheck'] = true; + $extra_data['truncatableFieldValue'] = $new_value; + } + $this->dbi->freeResult($result); + } + + /** + * Function to get the columns of a table + * + * @param string $db current db + * @param string $table current table + * + * @return array + */ + public function getTableColumns($db, $table) + { + $this->dbi->selectDb($db); + return array_values($this->dbi->getColumns($db, $table, null, true)); + } + + /** + * Function to determine Insert/Edit rows + * + * @param string $where_clause where clause + * @param string $db current database + * @param string $table current table + * + * @return mixed + */ + public function determineInsertOrEdit($where_clause, $db, $table) + { + if (isset($_POST['where_clause'])) { + $where_clause = $_POST['where_clause']; + } + if (isset($_SESSION['edit_next'])) { + $where_clause = $_SESSION['edit_next']; + unset($_SESSION['edit_next']); + $after_insert = 'edit_next'; + } + if (isset($_POST['ShowFunctionFields'])) { + $GLOBALS['cfg']['ShowFunctionFields'] = $_POST['ShowFunctionFields']; + } + if (isset($_POST['ShowFieldTypesInDataEditView'])) { + $GLOBALS['cfg']['ShowFieldTypesInDataEditView'] + = $_POST['ShowFieldTypesInDataEditView']; + } + if (isset($_POST['after_insert'])) { + $after_insert = $_POST['after_insert']; + } + + if (isset($where_clause)) { + // we are editing + $insert_mode = false; + $where_clause_array = $this->getWhereClauseArray($where_clause); + list($where_clauses, $result, $rows, $found_unique_key) + = $this->analyzeWhereClauses( + $where_clause_array, + $table, + $db + ); + } else { + // we are inserting + $insert_mode = true; + $where_clause = null; + list($result, $rows) = $this->loadFirstRow($table, $db); + $where_clauses = null; + $where_clause_array = []; + $found_unique_key = false; + } + + // Copying a row - fetched data will be inserted as a new row, + // therefore the where clause is needless. + if (isset($_POST['default_action']) + && $_POST['default_action'] === 'insert' + ) { + $where_clause = $where_clauses = null; + } + + return [ + $insert_mode, + $where_clause, + $where_clause_array, + $where_clauses, + $result, + $rows, + $found_unique_key, + isset($after_insert) ? $after_insert : null, + ]; + } + + /** + * Function to get comments for the table columns + * + * @param string $db current database + * @param string $table current table + * + * @return array comments for columns + */ + public function getCommentsMap($db, $table) + { + $comments_map = []; + + if ($GLOBALS['cfg']['ShowPropertyComments']) { + $comments_map = $this->relation->getComments($db, $table); + } + + return $comments_map; + } + + /** + * Function to get URL parameters + * + * @param string $db current database + * @param string $table current table + * + * @return array url parameters + */ + public function getUrlParameters($db, $table) + { + /** + * @todo check if we could replace by "db_|tbl_" - please clarify!? + */ + $url_params = [ + 'db' => $db, + 'sql_query' => $_POST['sql_query'], + ]; + + if (0 === strpos($GLOBALS['goto'], "tbl_")) { + $url_params['table'] = $table; + } + + return $url_params; + } + + /** + * Function to get html for the gis editor div + * + * @return string + */ + public function getHtmlForGisEditor() + { + return '
' + . '' + . '
'; + } + + /** + * Function to get html for the ignore option in insert mode + * + * @param int $row_id row id + * @param bool $checked ignore option is checked or not + * + * @return string + */ + public function getHtmlForIgnoreOption($row_id, $checked = true) + { + return '' + . '
' . "\n"; + } + + /** + * Function to get html for the function option + * + * @param array $column column + * @param string $column_name_appendix column name appendix + * + * @return String + */ + private function getHtmlForFunctionOption(array $column, $column_name_appendix) + { + return '' + . ''; + } + + /** + * Function to get html for the column type + * + * @param array $column column + * + * @return string + */ + private function getHtmlForInsertEditColumnType(array $column) + { + return ''; + } + + /** + * Function to get html for the insert edit form header + * + * @param bool $has_blob_field whether has blob field + * @param bool $is_upload whether is upload + * + * @return string + */ + public function getHtmlForInsertEditFormHeader($has_blob_field, $is_upload) + { + $html_output = 'analyzeTableColumnsArray( + $column, + $comments_map, + $timestamp_seen + ); + } + $as_is = false; + if (! empty($repopulate) && ! empty($current_row)) { + $current_row[$column['Field']] = $repopulate[$column['Field_md5']]; + $as_is = true; + } + + $extracted_columnspec + = Util::extractColumnSpec($column['Type']); + + if (-1 === $column['len']) { + $column['len'] = $this->dbi->fieldLen( + $current_result, + $column_number + ); + // length is unknown for geometry fields, + // make enough space to edit very simple WKTs + if (-1 === $column['len']) { + $column['len'] = 30; + } + } + //Call validation when the form submitted... + $onChangeClause = $chg_evt_handler + . "=\"return verificationsAfterFieldChange('" + . Sanitize::escapeJsString($column['Field_md5']) . "', '" + . Sanitize::escapeJsString($jsvkey) . "','" . $column['pma_type'] . "')\""; + + // Use an MD5 as an array index to avoid having special characters + // in the name attribute (see bug #1746964 ) + $column_name_appendix = $vkey . '[' . $column['Field_md5'] . ']'; + + if ($column['Type'] === 'datetime' + && ! isset($column['Default']) + && $column['Default'] !== null + && $insert_mode + ) { + $column['Default'] = date('Y-m-d H:i:s', time()); + } + + $html_output = $this->getHtmlForFunctionOption( + $column, + $column_name_appendix + ); + + if ($GLOBALS['cfg']['ShowFieldTypesInDataEditView']) { + $html_output .= $this->getHtmlForInsertEditColumnType($column); + } //End if + + // Get a list of GIS data types. + $gis_data_types = Util::getGISDatatypes(); + + // Prepares the field value + $real_null_value = false; + $special_chars_encoded = ''; + if (! empty($current_row)) { + // (we are editing) + list( + $real_null_value, $special_chars_encoded, $special_chars, + $data, $backup_field + ) + = $this->getSpecialCharsAndBackupFieldForExistingRow( + $current_row, + $column, + $extracted_columnspec, + $real_null_value, + $gis_data_types, + $column_name_appendix, + $as_is + ); + } else { + // (we are inserting) + // display default values + $tmp = $column; + if (isset($repopulate[$column['Field_md5']])) { + $tmp['Default'] = $repopulate[$column['Field_md5']]; + } + list($real_null_value, $data, $special_chars, $backup_field, + $special_chars_encoded + ) + = $this->getSpecialCharsAndBackupFieldForInsertingMode( + $tmp, + $real_null_value + ); + unset($tmp); + } + + $idindex = ($o_rows * $columns_cnt) + $column_number + 1; + $tabindex = $idindex; + + // Get a list of data types that are not yet supported. + $no_support_types = Util::unsupportedDatatypes(); + + // The function column + // ------------------- + $foreignData = $this->relation->getForeignData( + $foreigners, + $column['Field'], + false, + '', + '' + ); + if ($GLOBALS['cfg']['ShowFunctionFields']) { + $html_output .= $this->getFunctionColumn( + $column, + $is_upload, + $column_name_appendix, + $onChangeClause, + $no_support_types, + $tabindex_for_function, + $tabindex, + $idindex, + $insert_mode, + $readOnly, + $foreignData + ); + } + + // The null column + // --------------- + $html_output .= $this->getNullColumn( + $column, + $column_name_appendix, + $real_null_value, + $tabindex, + $tabindex_for_null, + $idindex, + $vkey, + $foreigners, + $foreignData, + $readOnly + ); + + // The value column (depends on type) + // ---------------- + // See bug #1667887 for the reason why we don't use the maxlength + // HTML attribute + + //add data attributes "no of decimals" and "data type" + $no_decimals = 0; + $type = current(explode("(", $column['pma_type'])); + if (preg_match('/\(([^()]+)\)/', $column['pma_type'], $match)) { + $match[0] = trim($match[0], '()'); + $no_decimals = $match[0]; + } + $html_output .= ''; + + //store the default value for CharEditing + $default_char_editing = $GLOBALS['cfg']['CharEditing']; + $mime_map = $this->transformations->getMime($db, $table); + $where_clause = ''; + if (isset($where_clause_array[$row_id])) { + $where_clause = $where_clause_array[$row_id]; + } + for ($column_number = 0; $column_number < $columns_cnt; $column_number++) { + $table_column = $table_columns[$column_number]; + $column_mime = []; + if (isset($mime_map[$table_column['Field']])) { + $column_mime = $mime_map[$table_column['Field']]; + } + + $virtual = [ + 'VIRTUAL', + 'PERSISTENT', + 'VIRTUAL GENERATED', + 'STORED GENERATED', + ]; + if (! in_array($table_column['Extra'], $virtual)) { + $html_output .= $this->getHtmlForInsertEditFormColumn( + $table_columns, + $column_number, + $comments_map, + $timestamp_seen, + $current_result, + $chg_evt_handler, + $jsvkey, + $vkey, + $insert_mode, + $current_row, + $o_rows, + $tabindex, + $columns_cnt, + $is_upload, + $tabindex_for_function, + $foreigners, + $tabindex_for_null, + $tabindex_for_value, + $table, + $db, + $row_id, + $titles, + $biggest_max_file_size, + $default_char_editing, + $text_dir, + $repopulate, + $column_mime, + $where_clause + ); + } + } // end for + $o_rows++; + $html_output .= ' ' + . '
' . __('Column') . '' . __('Null') . '' . __('Value') . '
' + . '' + . '
' + . $column['Field_title'] + . '' + . '' + . '' . $column['pma_type'] . '' + . '' . "\n"; + // Will be used by js/table/change.js to set the default value + // for the "Continue insertion" feature + $html_output .= '' + . $special_chars . ''; + + // Check input transformation of column + $transformed_html = ''; + if (! empty($column_mime['input_transformation'])) { + $file = $column_mime['input_transformation']; + $include_file = 'libraries/classes/Plugins/Transformations/' . $file; + if (is_file($include_file)) { + $class_name = $this->transformations->getClassName($include_file); + if (class_exists($class_name)) { + $transformation_plugin = new $class_name(); + $transformation_options = $this->transformations->getOptions( + $column_mime['input_transformation_options'] + ); + $_url_params = [ + 'db' => $db, + 'table' => $table, + 'transform_key' => $column['Field'], + 'where_clause' => $where_clause, + ]; + $transformation_options['wrapper_link'] + = Url::getCommon($_url_params); + $current_value = ''; + if (isset($current_row[$column['Field']])) { + $current_value = $current_row[$column['Field']]; + } + if (method_exists($transformation_plugin, 'getInputHtml')) { + $transformed_html = $transformation_plugin->getInputHtml( + $column, + $row_id, + $column_name_appendix, + $transformation_options, + $current_value, + $text_dir, + $tabindex, + $tabindex_for_value, + $idindex + ); + } + if (method_exists($transformation_plugin, 'getScripts')) { + $GLOBALS['plugin_scripts'] = array_merge( + $GLOBALS['plugin_scripts'], + $transformation_plugin->getScripts() + ); + } + } + } + } + if (! empty($transformed_html)) { + $html_output .= $transformed_html; + } else { + $html_output .= $this->getValueColumn( + $column, + $backup_field, + $column_name_appendix, + $onChangeClause, + $tabindex, + $tabindex_for_value, + $idindex, + $data, + $special_chars, + $foreignData, + [ + $table, + $db, + ], + $row_id, + $titles, + $text_dir, + $special_chars_encoded, + $vkey, + $is_upload, + $biggest_max_file_size, + $default_char_editing, + $no_support_types, + $gis_data_types, + $extracted_columnspec, + $readOnly + ); + } + return $html_output; + } + + /** + * Function to get html for each insert/edit row + * + * @param array $url_params url parameters + * @param array $table_columns table columns + * @param array $comments_map comments map + * @param bool $timestamp_seen whether timestamp seen + * @param array $current_result current result + * @param string $chg_evt_handler javascript change event handler + * @param string $jsvkey javascript validation key + * @param string $vkey validation key + * @param bool $insert_mode whether insert mode + * @param array $current_row current row + * @param int $o_rows row offset + * @param int $tabindex tab index + * @param int $columns_cnt columns count + * @param bool $is_upload whether upload + * @param int $tabindex_for_function tab index offset for function + * @param array $foreigners foreigners + * @param int $tabindex_for_null tab index offset for null + * @param int $tabindex_for_value tab index offset for value + * @param string $table table + * @param string $db database + * @param int $row_id row id + * @param array $titles titles + * @param int $biggest_max_file_size biggest max file size + * @param string $text_dir text direction + * @param array $repopulate the data to be repopulated + * @param array $where_clause_array the array of where clauses + * + * @return string + */ + public function getHtmlForInsertEditRow( + array $url_params, + array $table_columns, + array $comments_map, + $timestamp_seen, + $current_result, + $chg_evt_handler, + $jsvkey, + $vkey, + $insert_mode, + array $current_row, + &$o_rows, + &$tabindex, + $columns_cnt, + $is_upload, + $tabindex_for_function, + array $foreigners, + $tabindex_for_null, + $tabindex_for_value, + $table, + $db, + $row_id, + array $titles, + $biggest_max_file_size, + $text_dir, + array $repopulate, + array $where_clause_array + ) { + $html_output = $this->getHeadAndFootOfInsertRowTable($url_params) + . '

' + . '
'; + + return $html_output; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/InternalRelations.php b/srcs/phpmyadmin/libraries/classes/InternalRelations.php new file mode 100644 index 0000000..771ccc9 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/InternalRelations.php @@ -0,0 +1,505 @@ + [ + 'DEFAULT_COLLATE_NAME' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + ], + 'COLLATIONS' => [ + 'CHARACTER_SET_NAME' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'CHARACTER_SETS', + 'foreign_field' => 'CHARACTER_SET_NAME', + ], + ], + 'COLLATION_CHARACTER_SET_APPLICABILITY' => [ + 'CHARACTER_SET_NAME' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'CHARACTER_SETS', + 'foreign_field' => 'CHARACTER_SET_NAME', + ], + 'COLLATION_NAME' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + ], + 'COLUMNS' => [ + 'TABLE_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + 'CHARACTER_SET_NAME' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'CHARACTER_SETS', + 'foreign_field' => 'CHARACTER_SET_NAME', + ], + 'COLLATION_NAME' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + ], + 'COLUMN_PRIVILEGES' => [ + 'TABLE_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + ], + 'EVENTS' => [ + 'EVENT_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + 'CHARACTER_SET_CLIENT' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'CHARACTER_SETS', + 'foreign_field' => 'CHARACTER_SET_NAME', + ], + 'COLLATION_CONNECTION' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + 'DATABASE_COLLATION' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + ], + 'FILES' => [ + 'TABLESPACE_NAME' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'TABLESPACES', + 'foreign_field' => 'TABLESPACE_NAME', + ], + 'TABLE_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + 'COLLATION_CONNECTION' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + 'ENGINE' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'ENGINES', + 'foreign_field' => 'ENGINE', + ], + ], + 'KEY_COLUMN_USAGE' => [ + 'CONSTRAINT_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + 'TABLE_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + 'REFERENCED_TABLE_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + ], + 'PARAMETERS' => [ + 'SPECIFIC_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + 'CHARACTER_SET_NAME' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'CHARACTER_SETS', + 'foreign_field' => 'CHARACTER_SET_NAME', + ], + 'COLLATION_NAME' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + ], + 'PARTITIONS' => [ + 'TABLE_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + 'TABLESPACE_NAME' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'TABLESPACES', + 'foreign_field' => 'TABLESPACE_NAME', + ], + ], + 'PROCESSLIST' => [ + 'DB' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + ], + 'REFERENTIAL_CONSTRAINTS' => [ + 'CONSTRAINT_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + 'UNIQUE_CONSTRAINT_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + ], + 'ROUTINES' => [ + 'ROUTINE_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + 'CHARACTER_SET_NAME' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'CHARACTER_SETS', + 'foreign_field' => 'CHARACTER_SET_NAME', + ], + 'COLLATION_NAME' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + 'CHARACTER_SET_CLIENT' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'CHARACTER_SETS', + 'foreign_field' => 'CHARACTER_SET_NAME', + ], + 'COLLATION_CONNECTION' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + 'DATABASE_COLLATION' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + ], + 'SCHEMATA' => [ + 'DEFAULT_CHARACTER_SET_NAME' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'CHARACTER_SETS', + 'foreign_field' => 'CHARACTER_SET_NAME', + ], + 'DEFAULT_COLLATION_NAME' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + ], + 'SCHEMA_PRIVILEGES' => [ + 'TABLE_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + ], + 'STATISTICS' => [ + 'TABLE_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + 'INDEX_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + ], + 'TABLES' => [ + 'TABLE_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + 'TABLE_COLLATION' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + 'ENGINE' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'ENGINES', + 'foreign_field' => 'ENGINE', + ], + ], + 'TABLESAPCES' => [ + 'ENGINE' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'ENGINES', + 'foreign_field' => 'ENGINE', + ], + ], + 'TABLE_CONSTRAINTS' => [ + 'CONSTRAINT_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + 'TABLE_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + ], + 'TABLE_PRIVILEGES' => [ + 'TABLE_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + ], + 'TRIGGERS' => [ + 'TRIGGER_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + 'EVENT_OBJECT_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + 'CHARACTER_SET_CLIENT' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'CHARACTER_SETS', + 'foreign_field' => 'CHARACTER_SET_NAME', + ], + 'COLLATION_CONNECTION' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + 'DATABASE_COLLATION' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + ], + 'VIEWS' => [ + 'TABLE_SCHEMA' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + 'CHARACTER_SET_CLIENT' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'CHARACTER_SETS', + 'foreign_field' => 'CHARACTER_SET_NAME', + ], + 'COLLATION_CONNECTION' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + ], + ]; + + /** + * @var array + */ + private static $mysql = [ + 'columns_priv' => [ + 'Db' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + ], + 'db' => [ + 'Db' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + ], + 'event' => [ + 'db' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + 'character_set_client' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'CHARACTER_SETS', + 'foreign_field' => 'CHARACTER_SET_NAME', + ], + 'collation_connection' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + 'db_collation' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + ], + 'help_category' => [ + 'parent_category_id' => [ + 'foreign_db' => 'mysql', + 'foreign_table' => 'help_category', + 'foreign_field' => 'help_category_id', + ], + ], + 'help_relation' => [ + 'help_topic_id' => [ + 'foreign_db' => 'mysql', + 'foreign_table' => 'help_topic', + 'foreign_field' => 'help_topic_id', + ], + 'help_keyword_id' => [ + 'foreign_db' => 'mysql', + 'foreign_table' => 'help_keyword', + 'foreign_field' => 'help_keyword_id', + ], + ], + 'help_topic' => [ + 'help_category_id' => [ + 'foreign_db' => 'mysql', + 'foreign_table' => 'help_category', + 'foreign_field' => 'help_category_id', + ], + ], + 'innodb_index_stats' => [ + 'database_name' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + ], + 'innodb_table_stats' => [ + 'database_name' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + ], + 'proc' => [ + 'db' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + 'character_set_client' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'CHARACTER_SETS', + 'foreign_field' => 'CHARACTER_SET_NAME', + ], + 'collation_connection' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + 'db_collation' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'COLLATIONS', + 'foreign_field' => 'COLLATION_NAME', + ], + ], + 'proc_priv' => [ + 'Db' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + ], + 'servers' => [ + 'Db' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + ], + 'slow_log' => [ + 'db' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + ], + 'tables_priv' => [ + 'Db' => [ + 'foreign_db' => 'information_schema', + 'foreign_table' => 'SCHEMATA', + 'foreign_field' => 'SCHEMA_NAME', + ], + ], + 'time_zone_name' => [ + 'Time_zone_id' => [ + 'foreign_db' => 'mysql', + 'foreign_table' => 'time_zone', + 'foreign_field' => 'Time_zone_id', + ], + ], + 'time_zone_transition' => [ + 'Time_zone_id' => [ + 'foreign_db' => 'mysql', + 'foreign_table' => 'time_zone', + 'foreign_field' => 'Time_zone_id', + ], + 'Transition_time' => [ + 'foreign_db' => 'mysql', + 'foreign_table' => 'time_zone_leap_second', + 'foreign_field' => 'Transition_time', + ], + ], + 'time_zone_transition_type' => [ + 'Time_zone_id' => [ + 'foreign_db' => 'mysql', + 'foreign_table' => 'time_zone', + 'foreign_field' => 'Time_zone_id', + ], + ], + ]; + + /** + * @return array + */ + public static function getInformationSchema(): array + { + return self::$informationSchema; + } + + /** + * @return array + */ + public static function getMySql(): array + { + return self::$mysql; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/IpAllowDeny.php b/srcs/phpmyadmin/libraries/classes/IpAllowDeny.php new file mode 100644 index 0000000..d1a7c79 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/IpAllowDeny.php @@ -0,0 +1,336 @@ + -1 + || mb_strpos($ipToTest, ':') > -1 + ) { + // assume IPv6 + $result = $this->ipv6MaskTest($testRange, $ipToTest); + } else { + $result = $this->ipv4MaskTest($testRange, $ipToTest); + } + + return $result; + } + + /** + * Based on IP Pattern Matcher + * Originally by J.Adams + * Found on + * Modified for phpMyAdmin + * + * Matches: + * xxx.xxx.xxx.xxx (exact) + * xxx.xxx.xxx.[yyy-zzz] (range) + * xxx.xxx.xxx.xxx/nn (CIDR) + * + * Does not match: + * xxx.xxx.xxx.xx[yyy-zzz] (range, partial octets not supported) + * + * @param string $testRange string of IP range to match + * @param string $ipToTest string of IP to test against range + * + * @return boolean whether the IP mask matches + * + * @access public + */ + public function ipv4MaskTest($testRange, $ipToTest) + { + $result = true; + $match = preg_match( + '|([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/([0-9]+)|', + $testRange, + $regs + ); + if ($match) { + // performs a mask match + $ipl = ip2long($ipToTest); + $rangel = ip2long( + $regs[1] . '.' . $regs[2] . '.' . $regs[3] . '.' . $regs[4] + ); + + $maskl = 0; + + for ($i = 0; $i < 31; $i++) { + if ($i < $regs[5] - 1) { + $maskl += pow(2, 30 - $i); + } // end if + } // end for + + return ($maskl & $rangel) == ($maskl & $ipl); + } + + // range based + $maskocts = explode('.', $testRange); + $ipocts = explode('.', $ipToTest); + + // perform a range match + for ($i = 0; $i < 4; $i++) { + if (preg_match('|\[([0-9]+)\-([0-9]+)\]|', $maskocts[$i], $regs)) { + if (($ipocts[$i] > $regs[2]) || ($ipocts[$i] < $regs[1])) { + $result = false; + } // end if + } else { + if ($maskocts[$i] <> $ipocts[$i]) { + $result = false; + } // end if + } // end if/else + } //end for + + return $result; + } + + /** + * IPv6 matcher + * CIDR section taken from https://stackoverflow.com/a/10086404 + * Modified for phpMyAdmin + * + * Matches: + * xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx + * (exact) + * xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:[yyyy-zzzz] + * (range, only at end of IP - no subnets) + * xxxx:xxxx:xxxx:xxxx/nn + * (CIDR) + * + * Does not match: + * xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xx[yyy-zzz] + * (range, partial octets not supported) + * + * @param string $test_range string of IP range to match + * @param string $ip_to_test string of IP to test against range + * + * @return boolean whether the IP mask matches + * + * @access public + */ + public function ipv6MaskTest($test_range, $ip_to_test) + { + $result = true; + + // convert to lowercase for easier comparison + $test_range = mb_strtolower($test_range); + $ip_to_test = mb_strtolower($ip_to_test); + + $is_cidr = mb_strpos($test_range, '/') > -1; + $is_range = mb_strpos($test_range, '[') > -1; + $is_single = ! $is_cidr && ! $is_range; + + $ip_hex = bin2hex(inet_pton($ip_to_test)); + + if ($is_single) { + $range_hex = bin2hex(inet_pton($test_range)); + $result = hash_equals($ip_hex, $range_hex); + return $result; + } + + if ($is_range) { + // what range do we operate on? + $range_match = []; + $match = preg_match( + '/\[([0-9a-f]+)\-([0-9a-f]+)\]/', + $test_range, + $range_match + ); + if ($match) { + $range_start = $range_match[1]; + $range_end = $range_match[2]; + + // get the first and last allowed IPs + $first_ip = str_replace($range_match[0], $range_start, $test_range); + $first_hex = bin2hex(inet_pton($first_ip)); + $last_ip = str_replace($range_match[0], $range_end, $test_range); + $last_hex = bin2hex(inet_pton($last_ip)); + + // check if the IP to test is within the range + $result = ($ip_hex >= $first_hex && $ip_hex <= $last_hex); + } + return $result; + } + + if ($is_cidr) { + // Split in address and prefix length + list($first_ip, $subnet) = explode('/', $test_range); + + // Parse the address into a binary string + $first_bin = inet_pton($first_ip); + $first_hex = bin2hex($first_bin); + + $flexbits = 128 - (int) $subnet; + + // Build the hexadecimal string of the last address + $last_hex = $first_hex; + + $pos = 31; + while ($flexbits > 0) { + // Get the character at this position + $orig = mb_substr($last_hex, $pos, 1); + + // Convert it to an integer + $origval = hexdec($orig); + + // OR it with (2^flexbits)-1, with flexbits limited to 4 at a time + $newval = $origval | (pow(2, min(4, $flexbits)) - 1); + + // Convert it back to a hexadecimal character + $new = dechex($newval); + + // And put that character back in the string + $last_hex = substr_replace($last_hex, $new, $pos, 1); + + // We processed one nibble, move to previous position + $flexbits -= 4; + --$pos; + } + + // check if the IP to test is within the range + $result = ($ip_hex >= $first_hex && $ip_hex <= $last_hex); + } + + return $result; + } + + /** + * Runs through IP Allow rules the use of it below for more information + * + * @return bool Whether rule has matched + * + * @access public + * + * @see Core::getIp() + */ + public function allow() + { + return $this->allowDeny("allow"); + } + + /** + * Runs through IP Deny rules the use of it below for more information + * + * @return bool Whether rule has matched + * + * @access public + * + * @see Core::getIp() + */ + public function deny() + { + return $this->allowDeny("deny"); + } + + /** + * Runs through IP Allow/Deny rules the use of it below for more information + * + * @param string $type 'allow' | 'deny' type of rule to match + * + * @return bool Whether rule has matched + * + * @access public + * + * @see Core::getIp() + */ + private function allowDeny($type) + { + global $cfg; + + // Grabs true IP of the user and returns if it can't be found + $remote_ip = Core::getIp(); + if (empty($remote_ip)) { + return false; + } + + // copy username + $username = $cfg['Server']['user']; + + // copy rule database + if (isset($cfg['Server']['AllowDeny']['rules'])) { + $rules = $cfg['Server']['AllowDeny']['rules']; + if (! is_array($rules)) { + $rules = []; + } + } else { + $rules = []; + } + + // lookup table for some name shortcuts + $shortcuts = [ + 'all' => '0.0.0.0/0', + 'localhost' => '127.0.0.1/8', + ]; + + // Provide some useful shortcuts if server gives us address: + if (Core::getenv('SERVER_ADDR')) { + $shortcuts['localnetA'] = Core::getenv('SERVER_ADDR') . '/8'; + $shortcuts['localnetB'] = Core::getenv('SERVER_ADDR') . '/16'; + $shortcuts['localnetC'] = Core::getenv('SERVER_ADDR') . '/24'; + } + + foreach ($rules as $rule) { + // extract rule data + $rule_data = explode(' ', $rule); + + // check for rule type + if ($rule_data[0] != $type) { + continue; + } + + // check for username + if (($rule_data[1] != '%') //wildcarded first + && (! hash_equals($rule_data[1], $username)) + ) { + continue; + } + + // check if the config file has the full string with an extra + // 'from' in it and if it does, just discard it + if ($rule_data[2] == 'from') { + $rule_data[2] = $rule_data[3]; + } + + // Handle shortcuts with above array + if (isset($shortcuts[$rule_data[2]])) { + $rule_data[2] = $shortcuts[$rule_data[2]]; + } + + // Add code for host lookups here + // Excluded for the moment + + // Do the actual matching now + if ($this->ipMaskTest($rule_data[2], $remote_ip)) { + return true; + } + } // end while + + return false; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Language.php b/srcs/phpmyadmin/libraries/classes/Language.php new file mode 100644 index 0000000..d88fdf9 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Language.php @@ -0,0 +1,204 @@ +code = $code; + $this->name = $name; + $this->native = $native; + if (strpos($regex, '[-_]') === false) { + $regex = str_replace('|', '([-_][[:alpha:]]{2,3})?|', $regex); + } + $this->regex = $regex; + $this->mysql = $mysql; + } + + /** + * Returns native name for language + * + * @return string + */ + public function getNativeName() + { + return $this->native; + } + + /** + * Returns English name for language + * + * @return string + */ + public function getEnglishName() + { + return $this->name; + } + + /** + * Returns verbose name for language + * + * @return string + */ + public function getName() + { + if (! empty($this->native)) { + return $this->native . ' - ' . $this->name; + } + + return $this->name; + } + + /** + * Returns language code + * + * @return string + */ + public function getCode() + { + return $this->code; + } + + /** + * Returns MySQL locale code, can be empty + * + * @return string + */ + public function getMySQLLocale() + { + return $this->mysql; + } + + /** + * Compare function used for sorting + * + * @param Language $other Other object to compare + * + * @return int same as strcmp + */ + public function cmp($other) + { + return strcmp($this->name, $other->name); + } + + /** + * Checks whether language is currently active. + * + * @return bool + */ + public function isActive() + { + return $GLOBALS['lang'] == $this->code; + } + + /** + * Checks whether language matches HTTP header Accept-Language. + * + * @param string $header Header content + * + * @return bool + */ + public function matchesAcceptLanguage($header) + { + $pattern = '/^(' + . addcslashes($this->regex, '/') + . ')(;q=[0-9]\\.[0-9])?$/i'; + return preg_match($pattern, $header); + } + + /** + * Checks whether language matches HTTP header User-Agent + * + * @param string $header Header content + * + * @return bool + */ + public function matchesUserAgent($header) + { + $pattern = '/(\(|\[|;[[:space:]])(' + . addcslashes($this->regex, '/') + . ')(;|\]|\))/i'; + return preg_match($pattern, $header); + } + + /** + * Checks whether language is RTL + * + * @return bool + */ + public function isRTL() + { + return in_array($this->code, ['ar', 'fa', 'he', 'ur']); + } + + /** + * Activates given translation + * + * @return void + */ + public function activate() + { + $GLOBALS['lang'] = $this->code; + + // Set locale + _setlocale(0, $this->code); + _bindtextdomain('phpmyadmin', LOCALE_PATH); + _textdomain('phpmyadmin'); + // Set PHP locale as well + if (function_exists('setlocale')) { + setlocale(0, $this->code); + } + + /* Text direction for language */ + if ($this->isRTL()) { + $GLOBALS['text_dir'] = 'rtl'; + } else { + $GLOBALS['text_dir'] = 'ltr'; + } + + /* TCPDF */ + $GLOBALS['l'] = []; + + /* TCPDF settings */ + $GLOBALS['l']['a_meta_charset'] = 'UTF-8'; + $GLOBALS['l']['a_meta_dir'] = $GLOBALS['text_dir']; + $GLOBALS['l']['a_meta_language'] = $this->code; + + /* TCPDF translations */ + $GLOBALS['l']['w_page'] = __('Page number:'); + + /* Show possible warnings from langauge selection */ + LanguageManager::getInstance()->showWarnings(); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/LanguageManager.php b/srcs/phpmyadmin/libraries/classes/LanguageManager.php new file mode 100644 index 0000000..84b12b7 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/LanguageManager.php @@ -0,0 +1,975 @@ + [ + 'af', + 'Afrikaans', + '', + 'af|afrikaans', + '', + ], + 'am' => [ + 'am', + 'Amharic', + 'አማርኛ', + 'am|amharic', + '', + ], + 'ar' => [ + 'ar', + 'Arabic', + 'العربية', + 'ar|arabic', + 'ar_AE', + ], + 'az' => [ + 'az', + 'Azerbaijani', + 'Azərbaycanca', + 'az|azerbaijani', + '', + ], + 'bn' => [ + 'bn', + 'Bangla', + 'বাংলা', + 'bn|bangla', + '', + ], + 'be' => [ + 'be', + 'Belarusian', + 'Беларуская', + 'be|belarusian', + 'be_BY', + ], + 'be@latin' => [ + 'be@latin', + 'Belarusian (latin)', + 'Biełaruskaja', + 'be[-_]lat|be@latin|belarusian latin', + '', + ], + 'bg' => [ + 'bg', + 'Bulgarian', + 'Български', + 'bg|bulgarian', + 'bg_BG', + ], + 'bs' => [ + 'bs', + 'Bosnian', + 'Bosanski', + 'bs|bosnian', + '', + ], + 'br' => [ + 'br', + 'Breton', + 'Brezhoneg', + 'br|breton', + '', + ], + 'brx' => [ + 'brx', + 'Bodo', + 'बड़ो', + 'brx|bodo', + '', + ], + 'ca' => [ + 'ca', + 'Catalan', + 'Català', + 'ca|catalan', + 'ca_ES', + ], + 'ckb' => [ + 'ckb', + 'Sorani', + 'سۆرانی', + 'ckb|sorani', + '', + ], + 'cs' => [ + 'cs', + 'Czech', + 'Čeština', + 'cs|czech', + 'cs_CZ', + ], + 'cy' => [ + 'cy', + 'Welsh', + 'Cymraeg', + 'cy|welsh', + '', + ], + 'da' => [ + 'da', + 'Danish', + 'Dansk', + 'da|danish', + 'da_DK', + ], + 'de' => [ + 'de', + 'German', + 'Deutsch', + 'de|german', + 'de_DE', + ], + 'el' => [ + 'el', + 'Greek', + 'Ελληνικά', + 'el|greek', + '', + ], + 'en' => [ + 'en', + 'English', + '', + 'en|english', + 'en_US', + ], + 'en_gb' => [ + 'en_GB', + 'English (United Kingdom)', + '', + 'en[_-]gb|english (United Kingdom)', + 'en_GB', + ], + 'eo' => [ + 'eo', + 'Esperanto', + 'Esperanto', + 'eo|esperanto', + '', + ], + 'es' => [ + 'es', + 'Spanish', + 'Español', + 'es|spanish', + 'es_ES', + ], + 'et' => [ + 'et', + 'Estonian', + 'Eesti', + 'et|estonian', + 'et_EE', + ], + 'eu' => [ + 'eu', + 'Basque', + 'Euskara', + 'eu|basque', + 'eu_ES', + ], + 'fa' => [ + 'fa', + 'Persian', + 'فارسی', + 'fa|persian', + '', + ], + 'fi' => [ + 'fi', + 'Finnish', + 'Suomi', + 'fi|finnish', + 'fi_FI', + ], + 'fil' => [ + 'fil', + 'Filipino', + 'Pilipino', + 'fil|filipino', + '', + ], + 'fr' => [ + 'fr', + 'French', + 'Français', + 'fr|french', + 'fr_FR', + ], + 'fy' => [ + 'fy', + 'Frisian', + 'Frysk', + 'fy|frisian', + '', + ], + 'gl' => [ + 'gl', + 'Galician', + 'Galego', + 'gl|galician', + 'gl_ES', + ], + 'gu' => [ + 'gu', + 'Gujarati', + 'ગુજરાતી', + 'gu|gujarati', + 'gu_IN', + ], + 'he' => [ + 'he', + 'Hebrew', + 'עברית', + 'he|hebrew', + 'he_IL', + ], + 'hi' => [ + 'hi', + 'Hindi', + 'हिन्दी', + 'hi|hindi', + 'hi_IN', + ], + 'hr' => [ + 'hr', + 'Croatian', + 'Hrvatski', + 'hr|croatian', + 'hr_HR', + ], + 'hu' => [ + 'hu', + 'Hungarian', + 'Magyar', + 'hu|hungarian', + 'hu_HU', + ], + 'hy' => [ + 'hy', + 'Armenian', + 'Հայերէն', + 'hy|armenian', + '', + ], + 'ia' => [ + 'ia', + 'Interlingua', + '', + 'ia|interlingua', + '', + ], + 'id' => [ + 'id', + 'Indonesian', + 'Bahasa Indonesia', + 'id|indonesian', + 'id_ID', + ], + 'ig' => [ + 'ig', + 'Igbo', + 'Asụsụ Igbo', + 'ig|igbo', + '', + ], + 'it' => [ + 'it', + 'Italian', + 'Italiano', + 'it|italian', + 'it_IT', + ], + 'ja' => [ + 'ja', + 'Japanese', + '日本語', + 'ja|japanese', + 'ja_JP', + ], + 'ko' => [ + 'ko', + 'Korean', + '한국어', + 'ko|korean', + 'ko_KR', + ], + 'ka' => [ + 'ka', + 'Georgian', + 'ქართული', + 'ka|georgian', + '', + ], + 'kab' => [ + 'kab', + 'Kabylian', + 'Taqbaylit', + 'kab|kabylian', + '', + ], + 'kk' => [ + 'kk', + 'Kazakh', + 'Қазақ', + 'kk|kazakh', + '', + ], + 'km' => [ + 'km', + 'Khmer', + 'ខ្មែរ', + 'km|khmer', + '', + ], + 'kn' => [ + 'kn', + 'Kannada', + 'ಕನ್ನಡ', + 'kn|kannada', + '', + ], + 'ksh' => [ + 'ksh', + 'Colognian', + 'Kölsch', + 'ksh|colognian', + '', + ], + 'ku' => [ + 'ku', + 'Kurdish', + 'کوردی', + 'ku|kurdish', + '', + ], + 'ky' => [ + 'ky', + 'Kyrgyz', + 'Кыргызча', + 'ky|kyrgyz', + '', + ], + 'li' => [ + 'li', + 'Limburgish', + 'Lèmbörgs', + 'li|limburgish', + '', + ], + 'lt' => [ + 'lt', + 'Lithuanian', + 'Lietuvių', + 'lt|lithuanian', + 'lt_LT', + ], + 'lv' => [ + 'lv', + 'Latvian', + 'Latviešu', + 'lv|latvian', + 'lv_LV', + ], + 'mk' => [ + 'mk', + 'Macedonian', + 'Macedonian', + 'mk|macedonian', + 'mk_MK', + ], + 'ml' => [ + 'ml', + 'Malayalam', + 'Malayalam', + 'ml|malayalam', + '', + ], + 'mn' => [ + 'mn', + 'Mongolian', + 'Монгол', + 'mn|mongolian', + 'mn_MN', + ], + 'ms' => [ + 'ms', + 'Malay', + 'Bahasa Melayu', + 'ms|malay', + 'ms_MY', + ], + 'my' => [ + 'my', + 'Burmese', + 'မြန်မာ', + 'my|burmese', + '', + ], + 'ne' => [ + 'ne', + 'Nepali', + 'नेपाली', + 'ne|nepali', + '', + ], + 'nb' => [ + 'nb', + 'Norwegian', + 'Norsk', + 'nb|norwegian', + 'nb_NO', + ], + 'nn' => [ + 'nn', + 'Norwegian Nynorsk', + 'Nynorsk', + 'nn|nynorsk', + 'nn_NO', + ], + 'nl' => [ + 'nl', + 'Dutch', + 'Nederlands', + 'nl|dutch', + 'nl_NL', + ], + 'pa' => [ + 'pa', + 'Punjabi', + 'ਪੰਜਾਬੀ', + 'pa|punjabi', + '', + ], + 'pl' => [ + 'pl', + 'Polish', + 'Polski', + 'pl|polish', + 'pl_PL', + ], + 'pt' => [ + 'pt', + 'Portuguese', + 'Português', + 'pt|portuguese', + 'pt_PT', + ], + 'pt_br' => [ + 'pt_BR', + 'Portuguese (Brazil)', + 'Português (Brasil)', + 'pt[-_]br|portuguese (brazil)', + 'pt_BR', + ], + 'ro' => [ + 'ro', + 'Romanian', + 'Română', + 'ro|romanian', + 'ro_RO', + ], + 'ru' => [ + 'ru', + 'Russian', + 'Русский', + 'ru|russian', + 'ru_RU', + ], + 'si' => [ + 'si', + 'Sinhala', + 'සිංහල', + 'si|sinhala', + '', + ], + 'sk' => [ + 'sk', + 'Slovak', + 'Slovenčina', + 'sk|slovak', + 'sk_SK', + ], + 'sl' => [ + 'sl', + 'Slovenian', + 'Slovenščina', + 'sl|slovenian', + 'sl_SI', + ], + 'sq' => [ + 'sq', + 'Albanian', + 'Shqip', + 'sq|albanian', + 'sq_AL', + ], + 'sr@latin' => [ + 'sr@latin', + 'Serbian (latin)', + 'Srpski', + 'sr[-_]lat|sr@latin|serbian latin', + 'sr_YU', + ], + 'sr' => [ + 'sr', + 'Serbian', + 'Српски', + 'sr|serbian', + 'sr_YU', + ], + 'sv' => [ + 'sv', + 'Swedish', + 'Svenska', + 'sv|swedish', + 'sv_SE', + ], + 'ta' => [ + 'ta', + 'Tamil', + 'தமிழ்', + 'ta|tamil', + 'ta_IN', + ], + 'te' => [ + 'te', + 'Telugu', + 'తెలుగు', + 'te|telugu', + 'te_IN', + ], + 'th' => [ + 'th', + 'Thai', + 'ภาษาไทย', + 'th|thai', + 'th_TH', + ], + 'tk' => [ + 'tk', + 'Turkmen', + 'Türkmençe', + 'tk|turkmen', + '', + ], + 'tr' => [ + 'tr', + 'Turkish', + 'Türkçe', + 'tr|turkish', + 'tr_TR', + ], + 'tt' => [ + 'tt', + 'Tatarish', + 'Tatarça', + 'tt|tatarish', + '', + ], + 'ug' => [ + 'ug', + 'Uyghur', + 'ئۇيغۇرچە', + 'ug|uyghur', + '', + ], + 'uk' => [ + 'uk', + 'Ukrainian', + 'Українська', + 'uk|ukrainian', + 'uk_UA', + ], + 'ur' => [ + 'ur', + 'Urdu', + 'اُردوُ', + 'ur|urdu', + 'ur_PK', + ], + 'uz@latin' => [ + 'uz@latin', + 'Uzbek (latin)', + 'O‘zbekcha', + 'uz[-_]lat|uz@latin|uzbek-latin', + '', + ], + 'uz' => [ + 'uz', + 'Uzbek (cyrillic)', + 'Ўзбекча', + 'uz[-_]cyr|uz@cyrillic|uzbek-cyrillic', + '', + ], + 'vi' => [ + 'vi', + 'Vietnamese', + 'Tiếng Việt', + 'vi|vietnamese', + 'vi_VN', + ], + 'vls' => [ + 'vls', + 'Flemish', + 'West-Vlams', + 'vls|flemish', + '', + ], + 'zh_tw' => [ + 'zh_TW', + 'Chinese traditional', + '中文', + 'zh[-_](tw|hk)|chinese traditional', + 'zh_TW', + ], + // only TW and HK use traditional Chinese while others (CN, SG, MY) + // use simplified Chinese + 'zh_cn' => [ + 'zh_CN', + 'Chinese simplified', + '中文', + 'zh(?![-_](tw|hk))([-_][[:alpha:]]{2,3})?|chinese simplified', + 'zh_CN', + ], + ]; + + private $_available_locales; + private $_available_languages; + private $_lang_failed_cfg; + private $_lang_failed_cookie; + private $_lang_failed_request; + + /** + * @var LanguageManager + */ + private static $instance; + + /** + * Returns LanguageManager singleton + * + * @return LanguageManager + */ + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new LanguageManager(); + } + return self::$instance; + } + + /** + * Returns list of available locales + * + * @return array + */ + public function listLocaleDir() + { + $result = ['en']; + + /* Check for existing directory */ + if (! is_dir(LOCALE_PATH)) { + return $result; + } + + /* Open the directory */ + $handle = @opendir(LOCALE_PATH); + /* This can happen if the kit is English-only */ + if ($handle === false) { + return $result; + } + + /* Process all files */ + while (false !== ($file = readdir($handle))) { + $path = LOCALE_PATH + . '/' . $file + . '/LC_MESSAGES/phpmyadmin.mo'; + if ($file != "." + && $file != ".." + && @file_exists($path) + ) { + $result[] = $file; + } + } + /* Close the handle */ + closedir($handle); + + return $result; + } + + /** + * Returns (cached) list of all available locales + * + * @return array of strings + */ + public function availableLocales() + { + if (! $this->_available_locales) { + if (! isset($GLOBALS['PMA_Config']) || empty($GLOBALS['PMA_Config']->get('FilterLanguages'))) { + $this->_available_locales = $this->listLocaleDir(); + } else { + $this->_available_locales = preg_grep( + '@' . $GLOBALS['PMA_Config']->get('FilterLanguages') . '@', + $this->listLocaleDir() + ); + } + } + return $this->_available_locales; + } + + /** + * Checks whether there are some languages available + * + * @return boolean + */ + public function hasChoice() + { + return count($this->availableLanguages()) > 1; + } + + /** + * Returns (cached) list of all available languages + * + * @return Language[] array of Language objects + */ + public function availableLanguages() + { + if (! $this->_available_languages) { + $this->_available_languages = []; + + foreach ($this->availableLocales() as $lang) { + $lang = strtolower($lang); + if (isset(static::$_language_data[$lang])) { + $data = static::$_language_data[$lang]; + $this->_available_languages[$lang] = new Language( + $data[0], + $data[1], + $data[2], + $data[3], + $data[4] + ); + } else { + $this->_available_languages[$lang] = new Language( + $lang, + ucfirst($lang), + ucfirst($lang), + $lang, + '' + ); + } + } + } + return $this->_available_languages; + } + + /** + * Returns (cached) list of all available languages sorted + * by name + * + * @return Language[] array of Language objects + */ + public function sortedLanguages() + { + $this->availableLanguages(); + uasort($this->_available_languages, function ($a, $b) { + return $a->cmp($b); + }); + return $this->_available_languages; + } + + /** + * Return Language object for given code + * + * @param string $code Language code + * + * @return Language|false Language object or false on failure + */ + public function getLanguage($code) + { + $code = strtolower($code); + $langs = $this->availableLanguages(); + if (isset($langs[$code])) { + return $langs[$code]; + } + return false; + } + + /** + * Return currently active Language object + * + * @return Language Language object + */ + public function getCurrentLanguage() + { + return $this->_available_languages[strtolower($GLOBALS['lang'])]; + } + + /** + * Activates language based on configuration, user preferences or + * browser + * + * @return Language + */ + public function selectLanguage() + { + // check forced language + if (! empty($GLOBALS['PMA_Config']->get('Lang'))) { + $lang = $this->getLanguage($GLOBALS['PMA_Config']->get('Lang')); + if ($lang !== false) { + return $lang; + } + $this->_lang_failed_cfg = true; + } + + // Don't use REQUEST in following code as it might be confused by cookies + // with same name. Check user requested language (POST) + if (! empty($_POST['lang'])) { + $lang = $this->getLanguage($_POST['lang']); + if ($lang !== false) { + return $lang; + } + $this->_lang_failed_request = true; + } + + // check user requested language (GET) + if (! empty($_GET['lang'])) { + $lang = $this->getLanguage($_GET['lang']); + if ($lang !== false) { + return $lang; + } + $this->_lang_failed_request = true; + } + + // check previous set language + if (! empty($GLOBALS['PMA_Config']->getCookie('pma_lang'))) { + $lang = $this->getLanguage($GLOBALS['PMA_Config']->getCookie('pma_lang')); + if ($lang !== false) { + return $lang; + } + $this->_lang_failed_cookie = true; + } + + $langs = $this->availableLanguages(); + + // try to find out user's language by checking its HTTP_ACCEPT_LANGUAGE variable; + $accepted_languages = Core::getenv('HTTP_ACCEPT_LANGUAGE'); + if ($accepted_languages) { + foreach (explode(',', $accepted_languages) as $header) { + foreach ($langs as $language) { + if ($language->matchesAcceptLanguage($header)) { + return $language; + } + } + } + } + + // try to find out user's language by checking its HTTP_USER_AGENT variable + $user_agent = Core::getenv('HTTP_USER_AGENT'); + if (! empty($user_agent)) { + foreach ($langs as $language) { + if ($language->matchesUserAgent($user_agent)) { + return $language; + } + } + } + + // Didn't catch any valid lang : we use the default settings + if (isset($langs[$GLOBALS['PMA_Config']->get('DefaultLang')])) { + return $langs[$GLOBALS['PMA_Config']->get('DefaultLang')]; + } + + // Fallback to English + return $langs['en']; + } + + /** + * Displays warnings about invalid languages. This needs to be postponed + * to show messages at time when language is initialized. + * + * @return void + */ + public function showWarnings() + { + // now, that we have loaded the language strings we can send the errors + if ($this->_lang_failed_cfg + || $this->_lang_failed_cookie + || $this->_lang_failed_request + ) { + trigger_error( + __('Ignoring unsupported language code.'), + E_USER_ERROR + ); + } + } + + + /** + * Returns HTML code for the language selector + * + * @param Template $template Template instance + * @param boolean $use_fieldset whether to use fieldset for selection + * @param boolean $show_doc whether to show documentation links + * + * @return string + * + * @access public + */ + public function getSelectorDisplay(Template $template, $use_fieldset = false, $show_doc = true) + { + $_form_params = [ + 'db' => $GLOBALS['db'], + 'table' => $GLOBALS['table'], + ]; + + // For non-English, display "Language" with emphasis because it's + // not a proper word in the current language; we show it to help + // people recognize the dialog + $language_title = __('Language') + . (__('Language') != 'Language' ? ' - Language' : ''); + if ($show_doc) { + $language_title .= Util::showDocu('faq', 'faq7-2'); + } + + $available_languages = $this->sortedLanguages(); + + return $template->render('select_lang', [ + 'language_title' => $language_title, + 'use_fieldset' => $use_fieldset, + 'available_languages' => $available_languages, + '_form_params' => $_form_params, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Linter.php b/srcs/phpmyadmin/libraries/classes/Linter.php new file mode 100644 index 0000000..ff31bfa --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Linter.php @@ -0,0 +1,186 @@ +length() : strlen($str); + + $lines = [0]; + for ($i = 0; $i < $len; ++$i) { + if ($str[$i] === "\n") { + $lines[] = $i + 1; + } + } + return $lines; + } + + /** + * Computes the number of the line and column given an absolute position. + * + * @param array $lines The starting position of each line. + * @param int $pos The absolute position + * + * @return array + */ + public static function findLineNumberAndColumn(array $lines, $pos) + { + $line = 0; + foreach ($lines as $lineNo => $lineStart) { + if ($lineStart > $pos) { + break; + } + $line = $lineNo; + } + return [ + $line, + $pos - $lines[$line], + ]; + } + + /** + * Runs the linting process. + * + * @param string $query The query to be checked. + * + * @return array + */ + public static function lint($query) + { + // Disabling lint for huge queries to save some resources. + if (mb_strlen($query) > 10000) { + return [ + [ + 'message' => __( + 'Linting is disabled for this query because it exceeds the ' + . 'maximum length.' + ), + 'fromLine' => 0, + 'fromColumn' => 0, + 'toLine' => 0, + 'toColumn' => 0, + 'severity' => 'warning', + ], + ]; + } + + /** + * Lexer used for tokenizing the query. + * + * @var Lexer + */ + $lexer = new Lexer($query); + + /** + * Parsed used for analysing the query. + * + * @var Parser + */ + $parser = new Parser($lexer->list); + + /** + * Array containing all errors. + * + * @var array + */ + $errors = ParserError::get([$lexer, $parser]); + + /** + * The response containing of all errors. + * + * @var array + */ + $response = []; + + /** + * The starting position for each line. + * + * CodeMirror requires relative position to line, but the parser stores + * only the absolute position of the character in string. + * + * @var array + */ + $lines = static::getLines($query); + + // Building the response. + foreach ($errors as $idx => $error) { + // Starting position of the string that caused the error. + list($fromLine, $fromColumn) = static::findLineNumberAndColumn( + $lines, + $error[3] + ); + + // Ending position of the string that caused the error. + list($toLine, $toColumn) = static::findLineNumberAndColumn( + $lines, + $error[3] + mb_strlen((string) $error[2]) + ); + + // Building the response. + $response[] = [ + 'message' => sprintf( + __('%1$s (near %2$s)'), + htmlspecialchars((string) $error[0]), + htmlspecialchars((string) $error[2]) + ), + 'fromLine' => $fromLine, + 'fromColumn' => $fromColumn, + 'toLine' => $toLine, + 'toColumn' => $toColumn, + 'severity' => 'error', + ]; + } + + // Sending back the answer. + return $response; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/ListAbstract.php b/srcs/phpmyadmin/libraries/classes/ListAbstract.php new file mode 100644 index 0000000..e5b9a7a --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/ListAbstract.php @@ -0,0 +1,107 @@ +item_empty; + } + + /** + * checks if the given db names exists in the current list, if there is + * missing at least one item it returns false otherwise true + * + * @param mixed[] ...$params params + * @return bool true if all items exists, otherwise false + */ + public function exists(...$params) + { + $this_elements = $this->getArrayCopy(); + foreach ($params as $result) { + if (! in_array($result, $this_elements)) { + return false; + } + } + return true; + } + + /** + * returns HTML '; + return $html; + } + + /** + * Gets HTML for replace_prefix_tbl or copy_tbl_change_prefix + * + * @param string $action action type + * @param array $urlParams URL params + * + * @return string + */ + public function getHtmlForReplacePrefixTable($action, array $urlParams) + { + $html = '
'; + $html .= Url::getHiddenInputs($urlParams); + $html .= '
'; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= '
' . __('From') . ''; + $html .= ''; + $html .= '
' . __('To') . ''; + $html .= ''; + $html .= '
'; + $html .= '
'; + $html .= ''; + $html .= '
'; + + return $html; + } + + /** + * Gets HTML for add_prefix_tbl + * + * @param string $action action type + * @param array $urlParams URL params + * + * @return string + */ + public function getHtmlForAddPrefixTable($action, array $urlParams) + { + $html = '
'; + $html .= Url::getHiddenInputs($urlParams); + $html .= '
'; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= '
' . __('Add prefix') . ''; + $html .= ''; + $html .= '
'; + $html .= '
'; + $html .= ''; + $html .= '
'; + + return $html; + } + + /** + * Gets HTML for other mult_submits actions + * + * @param string $what mult_submit type + * @param string $action action type + * @param array $urlParams URL params + * @param string $fullQuery full sql query string + * + * @return string + */ + public function getHtmlForOtherActions($what, $action, array $urlParams, $fullQuery) + { + $html = '
'; + $html .= Url::getHiddenInputs($urlParams); + $html .= '
'; + $html .= ''; + if ($what == 'drop_db') { + $html .= __('You are about to DESTROY a complete database!') . ' '; + } + $html .= __('Do you really want to execute the following query?'); + $html .= ''; + $html .= '' . $fullQuery . ''; + $html .= '
'; + $html .= '
'; + // Display option to disable foreign key checks while dropping tables + if ($what === 'drop_tbl' || $what === 'empty_tbl' || $what === 'row_delete') { + $html .= '
'; + $html .= Util::getFKCheckbox(); + $html .= '
'; + } + $html .= ''; + $html .= ''; + $html .= '
'; + $html .= '
'; + + return $html; + } + + /** + * Get query string from Selected + * + * @param string $what mult_submit type + * @param string $table table name + * @param array $selected the selected columns + * @param array $views table views + * + * @return array + */ + public function getQueryFromSelected($what, $table, array $selected, array $views) + { + $reload = false; + $fullQueryViews = null; + $fullQuery = ''; + + if ($what == 'drop_tbl') { + $fullQueryViews = ''; + } + + $selectedCount = count($selected); + $i = 0; + foreach ($selected as $selectedValue) { + switch ($what) { + case 'row_delete': + $fullQuery .= 'DELETE FROM ' + . Util::backquote(htmlspecialchars($table)) + // Do not append a "LIMIT 1" clause here + // (it's not binlog friendly). + // We don't need the clause because the calling panel permits + // this feature only when there is a unique index. + . ' WHERE ' . htmlspecialchars($selectedValue) + . ';
'; + break; + case 'drop_db': + $fullQuery .= 'DROP DATABASE ' + . Util::backquote(htmlspecialchars($selectedValue)) + . ';
'; + $reload = true; + break; + + case 'drop_tbl': + $current = $selectedValue; + if (! empty($views) && in_array($current, $views)) { + $fullQueryViews .= (empty($fullQueryViews) ? 'DROP VIEW ' : ', ') + . Util::backquote(htmlspecialchars($current)); + } else { + $fullQuery .= (empty($fullQuery) ? 'DROP TABLE ' : ', ') + . Util::backquote(htmlspecialchars($current)); + } + break; + + case 'empty_tbl': + $fullQuery .= 'TRUNCATE '; + $fullQuery .= Util::backquote(htmlspecialchars($selectedValue)) + . ';
'; + break; + + case 'primary_fld': + if ($fullQuery == '') { + $fullQuery .= 'ALTER TABLE ' + . Util::backquote(htmlspecialchars($table)) + . '
  DROP PRIMARY KEY,' + . '
   ADD PRIMARY KEY(' + . '
     ' + . Util::backquote(htmlspecialchars($selectedValue)) + . ','; + } else { + $fullQuery .= '
     ' + . Util::backquote(htmlspecialchars($selectedValue)) + . ','; + } + if ($i == $selectedCount - 1) { + $fullQuery = preg_replace('@,$@', ');
', $fullQuery); + } + break; + + case 'drop_fld': + if ($fullQuery == '') { + $fullQuery .= 'ALTER TABLE ' + . Util::backquote(htmlspecialchars($table)); + } + $fullQuery .= '
  DROP ' + . Util::backquote(htmlspecialchars($selectedValue)) + . ','; + if ($i == $selectedCount - 1) { + $fullQuery = preg_replace('@,$@', ';
', $fullQuery); + } + break; + } // end switch + $i++; + } + + if ($what == 'drop_tbl') { + if (! empty($fullQuery)) { + $fullQuery .= ';
' . "\n"; + } + if (! empty($fullQueryViews)) { + $fullQuery .= $fullQueryViews . ';
' . "\n"; + } + unset($fullQueryViews); + } + + $fullQueryViews = isset($fullQueryViews) ? $fullQueryViews : null; + + return [ + $fullQuery, + $reload, + $fullQueryViews, + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Navigation.php b/srcs/phpmyadmin/libraries/classes/Navigation/Navigation.php new file mode 100644 index 0000000..49d40df --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Navigation.php @@ -0,0 +1,280 @@ +template = $template; + $this->relation = $relation; + $this->dbi = $dbi; + $this->tree = new NavigationTree($this->template, $this->dbi); + } + + /** + * Renders the navigation tree, or part of it + * + * @return string The navigation tree + */ + public function getDisplay(): string + { + global $cfg; + + $logo = [ + 'is_displayed' => $cfg['NavigationDisplayLogo'], + 'has_link' => false, + 'link' => '#', + 'attributes' => ' target="_blank" rel="noopener noreferrer"', + 'source' => '', + ]; + + $response = Response::getInstance(); + if (! $response->isAjax()) { + $logo['source'] = $this->getLogoSource(); + $logo['has_link'] = (string) $cfg['NavigationLogoLink'] !== ''; + $logo['link'] = trim((string) $cfg['NavigationLogoLink']); + if (! Sanitize::checkLink($logo['link'], true)) { + $logo['link'] = 'index.php'; + } + if ($cfg['NavigationLogoLinkWindow'] === 'main') { + if (empty(parse_url($logo['link'], PHP_URL_HOST))) { + $hasStartChar = strpos($logo['link'], '?'); + $logo['link'] .= Url::getCommon( + [], + is_bool($hasStartChar) ? '?' : Url::getArgSeparator() + ); + } + $logo['attributes'] = ''; + } + + if ($cfg['NavigationDisplayServers'] && count($cfg['Servers']) > 1) { + $serverSelect = Select::render(true, true); + } + + if (! defined('PMA_DISABLE_NAVI_SETTINGS')) { + $navigationSettings = PageSettings::getNaviSettings(); + } + } + if (! $response->isAjax() + || ! empty($_POST['full']) + || ! empty($_POST['reload']) + ) { + if ($cfg['ShowDatabasesNavigationAsTree']) { + // provide database tree in navigation + $navRender = $this->tree->renderState(); + } else { + // provide legacy pre-4.0 navigation + $navRender = $this->tree->renderDbSelect(); + } + } else { + $navRender = $this->tree->renderPath(); + } + + return $this->template->render('navigation/main', [ + 'is_ajax' => $response->isAjax(), + 'logo' => $logo, + 'is_synced' => $cfg['NavigationLinkWithMainPanel'], + 'is_highlighted' => $cfg['NavigationTreePointerEnable'], + 'is_autoexpanded' => $cfg['NavigationTreeAutoexpandSingleDb'], + 'server' => $GLOBALS['server'], + 'auth_type' => $cfg['Server']['auth_type'], + 'is_servers_displayed' => $cfg['NavigationDisplayServers'], + 'servers' => $cfg['Servers'], + 'server_select' => $serverSelect ?? '', + 'navigation_tree' => $navRender, + 'is_navigation_settings_enabled' => ! defined('PMA_DISABLE_NAVI_SETTINGS'), + 'navigation_settings' => $navigationSettings ?? '', + 'is_drag_drop_import_enabled' => $cfg['enable_drag_drop_import'] === true, + ]); + } + + /** + * Add an item of navigation tree to the hidden items list in PMA database. + * + * @param string $itemName name of the navigation tree item + * @param string $itemType type of the navigation tree item + * @param string $dbName database name + * @param string $tableName table name if applicable + * + * @return void + */ + public function hideNavigationItem( + $itemName, + $itemType, + $dbName, + $tableName = null + ) { + $navTable = Util::backquote($GLOBALS['cfgRelation']['db']) + . "." . Util::backquote($GLOBALS['cfgRelation']['navigationhiding']); + $sqlQuery = "INSERT INTO " . $navTable + . "(`username`, `item_name`, `item_type`, `db_name`, `table_name`)" + . " VALUES (" + . "'" . $this->dbi->escapeString($GLOBALS['cfg']['Server']['user']) . "'," + . "'" . $this->dbi->escapeString($itemName) . "'," + . "'" . $this->dbi->escapeString($itemType) . "'," + . "'" . $this->dbi->escapeString($dbName) . "'," + . "'" . (! empty($tableName) ? $this->dbi->escapeString($tableName) : "" ) + . "')"; + $this->relation->queryAsControlUser($sqlQuery, false); + } + + /** + * Remove a hidden item of navigation tree from the + * list of hidden items in PMA database. + * + * @param string $itemName name of the navigation tree item + * @param string $itemType type of the navigation tree item + * @param string $dbName database name + * @param string $tableName table name if applicable + * + * @return void + */ + public function unhideNavigationItem( + $itemName, + $itemType, + $dbName, + $tableName = null + ) { + $navTable = Util::backquote($GLOBALS['cfgRelation']['db']) + . "." . Util::backquote($GLOBALS['cfgRelation']['navigationhiding']); + $sqlQuery = "DELETE FROM " . $navTable + . " WHERE" + . " `username`='" + . $this->dbi->escapeString($GLOBALS['cfg']['Server']['user']) . "'" + . " AND `item_name`='" . $this->dbi->escapeString($itemName) . "'" + . " AND `item_type`='" . $this->dbi->escapeString($itemType) . "'" + . " AND `db_name`='" . $this->dbi->escapeString($dbName) . "'" + . (! empty($tableName) + ? " AND `table_name`='" . $this->dbi->escapeString($tableName) . "'" + : "" + ); + $this->relation->queryAsControlUser($sqlQuery, false); + } + + /** + * Returns HTML for the dialog to show hidden navigation items. + * + * @param string $database database name + * @param string $itemType type of the items to include + * @param string $table table name + * + * @return string HTML for the dialog to show hidden navigation items + */ + public function getItemUnhideDialog($database, $itemType = null, $table = null) + { + $hidden = $this->getHiddenItems($database, $table); + + $typeMap = [ + 'group' => __('Groups:'), + 'event' => __('Events:'), + 'function' => __('Functions:'), + 'procedure' => __('Procedures:'), + 'table' => __('Tables:'), + 'view' => __('Views:'), + ]; + + return $this->template->render('navigation/item_unhide_dialog', [ + 'database' => $database, + 'table' => $table, + 'hidden' => $hidden, + 'types' => $typeMap, + 'item_type' => $itemType, + ]); + } + + /** + * @param string $database Database name + * @param string|null $table Table name + * @return array + */ + private function getHiddenItems(string $database, ?string $table): array + { + $navTable = Util::backquote($GLOBALS['cfgRelation']['db']) + . "." . Util::backquote($GLOBALS['cfgRelation']['navigationhiding']); + $sqlQuery = "SELECT `item_name`, `item_type` FROM " . $navTable + . " WHERE `username`='" + . $this->dbi->escapeString($GLOBALS['cfg']['Server']['user']) . "'" + . " AND `db_name`='" . $this->dbi->escapeString($database) . "'" + . " AND `table_name`='" + . (! empty($table) ? $this->dbi->escapeString($table) : '') . "'"; + $result = $this->relation->queryAsControlUser($sqlQuery, false); + + $hidden = []; + if ($result) { + while ($row = $this->dbi->fetchArray($result)) { + $type = $row['item_type']; + if (! isset($hidden[$type])) { + $hidden[$type] = []; + } + $hidden[$type][] = $row['item_name']; + } + } + $this->dbi->freeResult($result); + return $hidden; + } + + /** + * @return string Logo source + */ + private function getLogoSource(): string + { + global $pmaThemeImage; + + if (isset($pmaThemeImage) && @file_exists($pmaThemeImage . 'logo_left.png')) { + return $pmaThemeImage . 'logo_left.png'; + } elseif (isset($pmaThemeImage) && @file_exists($pmaThemeImage . 'pma_logo2.png')) { + return $pmaThemeImage . 'pma_logo2.png'; + } + return ''; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/NavigationTree.php b/srcs/phpmyadmin/libraries/classes/Navigation/NavigationTree.php new file mode 100644 index 0000000..289ed6e --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/NavigationTree.php @@ -0,0 +1,1581 @@ +template = $template; + $this->dbi = $dbi; + + $checkUserPrivileges = new CheckUserPrivileges($this->dbi); + $checkUserPrivileges->getPrivileges(); + + // Save the position at which we are in the database list + if (isset($_POST['pos'])) { + $this->pos = (int) $_POST['pos']; + } elseif (isset($_GET['pos'])) { + $this->pos = (int) $_GET['pos']; + } + if (! isset($this->pos)) { + $this->pos = $this->getNavigationDbPos(); + } + // Get the active node + if (isset($_REQUEST['aPath'])) { + $this->aPath[0] = $this->parsePath($_REQUEST['aPath']); + $this->pos2Name[0] = $_REQUEST['pos2_name']; + $this->pos2Value[0] = $_REQUEST['pos2_value']; + if (isset($_REQUEST['pos3_name'])) { + $this->pos3Name[0] = $_REQUEST['pos3_name']; + $this->pos3Value[0] = $_REQUEST['pos3_value']; + } + } else { + if (isset($_POST['n0_aPath'])) { + $count = 0; + while (isset($_POST['n' . $count . '_aPath'])) { + $this->aPath[$count] = $this->parsePath( + $_POST['n' . $count . '_aPath'] + ); + $index = 'n' . $count . '_pos2_'; + $this->pos2Name[$count] = $_POST[$index . 'name']; + $this->pos2Value[$count] = $_POST[$index . 'value']; + $index = 'n' . $count . '_pos3_'; + if (isset($_POST[$index])) { + $this->pos3Name[$count] = $_POST[$index . 'name']; + $this->pos3Value[$count] = $_POST[$index . 'value']; + } + $count++; + } + } + } + if (isset($_REQUEST['vPath'])) { + $this->vPath[0] = $this->parsePath($_REQUEST['vPath']); + } else { + if (isset($_POST['n0_vPath'])) { + $count = 0; + while (isset($_POST['n' . $count . '_vPath'])) { + $this->vPath[$count] = $this->parsePath( + $_POST['n' . $count . '_vPath'] + ); + $count++; + } + } + } + if (isset($_REQUEST['searchClause'])) { + $this->searchClause = $_REQUEST['searchClause']; + } + if (isset($_REQUEST['searchClause2'])) { + $this->searchClause2 = $_REQUEST['searchClause2']; + } + // Initialise the tree by creating a root node + $node = NodeFactory::getInstance('NodeDatabaseContainer', 'root'); + $this->tree = $node; + if ($GLOBALS['cfg']['NavigationTreeEnableGrouping'] + && $GLOBALS['cfg']['ShowDatabasesNavigationAsTree'] + ) { + $this->tree->separator = $GLOBALS['cfg']['NavigationTreeDbSeparator']; + $this->tree->separatorDepth = 10000; + } + } + + /** + * Returns the database position for the page selector + * + * @return int + */ + private function getNavigationDbPos() + { + $retval = 0; + + if (strlen($GLOBALS['db']) == 0) { + return $retval; + } + + /* + * @todo describe a scenario where this code is executed + */ + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $dbSeparator = $this->dbi->escapeString( + $GLOBALS['cfg']['NavigationTreeDbSeparator'] + ); + $query = "SELECT (COUNT(DB_first_level) DIV %d) * %d "; + $query .= "from ( "; + $query .= " SELECT distinct SUBSTRING_INDEX(SCHEMA_NAME, "; + $query .= " '%s', 1) "; + $query .= " DB_first_level "; + $query .= " FROM INFORMATION_SCHEMA.SCHEMATA "; + $query .= " WHERE `SCHEMA_NAME` < '%s' "; + $query .= ") t "; + + $retval = $this->dbi->fetchValue( + sprintf( + $query, + (int) $GLOBALS['cfg']['FirstLevelNavigationItems'], + (int) $GLOBALS['cfg']['FirstLevelNavigationItems'], + $dbSeparator, + $this->dbi->escapeString($GLOBALS['db']) + ) + ); + + return $retval; + } + + $prefixMap = []; + if ($GLOBALS['dbs_to_test'] === false) { + $handle = $this->dbi->tryQuery("SHOW DATABASES"); + if ($handle !== false) { + while ($arr = $this->dbi->fetchArray($handle)) { + if (strcasecmp($arr[0], $GLOBALS['db']) >= 0) { + break; + } + + $prefix = strstr( + $arr[0], + $GLOBALS['cfg']['NavigationTreeDbSeparator'], + true + ); + if ($prefix === false) { + $prefix = $arr[0]; + } + $prefixMap[$prefix] = 1; + } + } + } else { + $databases = []; + foreach ($GLOBALS['dbs_to_test'] as $db) { + $query = "SHOW DATABASES LIKE '" . $db . "'"; + $handle = $this->dbi->tryQuery($query); + if ($handle === false) { + continue; + } + while ($arr = $this->dbi->fetchArray($handle)) { + $databases[] = $arr[0]; + } + } + sort($databases); + foreach ($databases as $database) { + if (strcasecmp($database, $GLOBALS['db']) >= 0) { + break; + } + + $prefix = strstr( + $database, + $GLOBALS['cfg']['NavigationTreeDbSeparator'], + true + ); + if ($prefix === false) { + $prefix = $database; + } + $prefixMap[$prefix] = 1; + } + } + + $navItems = (int) $GLOBALS['cfg']['FirstLevelNavigationItems']; + $retval = (int) floor(count($prefixMap) / $navItems) * $navItems; + + return $retval; + } + + /** + * Converts an encoded path to a node in string format to an array + * + * @param string $string The path to parse + * + * @return array + */ + private function parsePath($string) + { + $path = explode('.', $string); + foreach ($path as $key => $value) { + $path[$key] = base64_decode($value); + } + + return $path; + } + + /** + * Generates the tree structure so that it can be rendered later + * + * @return Node|false The active node or false in case of failure + */ + private function buildPath() + { + $retval = $this->tree; + + // Add all databases unconditionally + $data = $this->tree->getData( + 'databases', + $this->pos, + $this->searchClause + ); + $hiddenCounts = $this->tree->getNavigationHidingData(); + foreach ($data as $db) { + $node = NodeFactory::getInstance('NodeDatabase', $db); + if (isset($hiddenCounts[$db])) { + $node->setHiddenCount($hiddenCounts[$db]); + } + $this->tree->addChild($node); + } + + // Whether build other parts of the tree depends + // on whether we have any paths in $this->_aPath + foreach ($this->aPath as $key => $path) { + $retval = $this->buildPathPart( + $path, + $this->pos2Name[$key], + $this->pos2Value[$key], + isset($this->pos3Name[$key]) ? $this->pos3Name[$key] : '', + isset($this->pos3Value[$key]) ? $this->pos3Value[$key] : '' + ); + } + + return $retval; + } + + /** + * Builds a branch of the tree + * + * @param array $path A paths pointing to the branch + * of the tree that needs to be built + * @param string $type2 The type of item being paginated on + * the second level of the tree + * @param int $pos2 The position for the pagination of + * the branch at the second level of the tree + * @param string $type3 The type of item being paginated on + * the third level of the tree + * @param int $pos3 The position for the pagination of + * the branch at the third level of the tree + * + * @return Node|bool The active node or false in case of failure, true if the path contains <= 1 items + */ + private function buildPathPart(array $path, $type2, $pos2, $type3, $pos3) + { + if (empty($pos2)) { + $pos2 = 0; + } + if (empty($pos3)) { + $pos3 = 0; + } + + $retval = true; + if (count($path) <= 1) { + return $retval; + } + + array_shift($path); // remove 'root' + /** @var NodeDatabase $db */ + $db = $this->tree->getChild($path[0]); + $retval = $db; + + if ($db === false) { + return false; + } + + $containers = $this->addDbContainers($db, $type2, $pos2); + + array_shift($path); // remove db + + if ((count($path) <= 0 || ! array_key_exists($path[0], $containers)) + && count($containers) != 1 + ) { + return $retval; + } + + if (count($containers) === 1) { + $container = array_shift($containers); + } else { + $container = $db->getChild($path[0], true); + if ($container === false) { + return false; + } + } + $retval = $container; + + if (count($container->children) <= 1) { + $dbData = $db->getData( + $container->realName, + $pos2, + $this->searchClause2 + ); + foreach ($dbData as $item) { + switch ($container->realName) { + case 'events': + $node = NodeFactory::getInstance( + 'NodeEvent', + $item + ); + break; + case 'functions': + $node = NodeFactory::getInstance( + 'NodeFunction', + $item + ); + break; + case 'procedures': + $node = NodeFactory::getInstance( + 'NodeProcedure', + $item + ); + break; + case 'tables': + $node = NodeFactory::getInstance( + 'NodeTable', + $item + ); + break; + case 'views': + $node = NodeFactory::getInstance( + 'NodeView', + $item + ); + break; + default: + break; + } + if (isset($node)) { + if ($type2 == $container->realName) { + $node->pos2 = $pos2; + } + $container->addChild($node); + } + } + } + if (count($path) > 1 && $path[0] != 'tables') { + $retval = false; + + return $retval; + } + + array_shift($path); // remove container + if (count($path) <= 0) { + return $retval; + } + + /** @var NodeTable $table */ + $table = $container->getChild($path[0], true); + if ($table === false) { + if (! $db->getPresence('tables', $path[0])) { + return false; + } + + $node = NodeFactory::getInstance( + 'NodeTable', + $path[0] + ); + if ($type2 == $container->realName) { + $node->pos2 = $pos2; + } + $container->addChild($node); + $table = $container->getChild($path[0], true); + } + $retval = $table; + $containers = $this->addTableContainers( + $table, + $pos2, + $type3, + $pos3 + ); + array_shift($path); // remove table + if (count($path) <= 0 + || ! array_key_exists($path[0], $containers) + ) { + return $retval; + } + + $container = $table->getChild($path[0], true); + $retval = $container; + $tableData = $table->getData( + $container->realName, + $pos3 + ); + foreach ($tableData as $item) { + switch ($container->realName) { + case 'indexes': + $node = NodeFactory::getInstance( + 'NodeIndex', + $item + ); + break; + case 'columns': + $node = NodeFactory::getInstance( + 'NodeColumn', + $item + ); + break; + case 'triggers': + $node = NodeFactory::getInstance( + 'NodeTrigger', + $item + ); + break; + default: + break; + } + if (isset($node)) { + $node->pos2 = $container->parent->pos2; + if ($type3 == $container->realName) { + $node->pos3 = $pos3; + } + $container->addChild($node); + } + } + + return $retval; + } + + /** + * Adds containers to a node that is a table + * + * References to existing children are returned + * if this function is called twice on the same node + * + * @param NodeTable $table The table node, new containers will be + * attached to this node + * @param int $pos2 The position for the pagination of + * the branch at the second level of the tree + * @param string $type3 The type of item being paginated on + * the third level of the tree + * @param int $pos3 The position for the pagination of + * the branch at the third level of the tree + * + * @return array An array of new nodes + */ + private function addTableContainers($table, $pos2, $type3, $pos3) + { + $retval = []; + if ($table->hasChildren(true) == 0) { + if ($table->getPresence('columns')) { + $retval['columns'] = NodeFactory::getInstance( + 'NodeColumnContainer' + ); + } + if ($table->getPresence('indexes')) { + $retval['indexes'] = NodeFactory::getInstance( + 'NodeIndexContainer' + ); + } + if ($table->getPresence('triggers')) { + $retval['triggers'] = NodeFactory::getInstance( + 'NodeTriggerContainer' + ); + } + // Add all new Nodes to the tree + foreach ($retval as $node) { + $node->pos2 = $pos2; + if ($type3 == $node->realName) { + $node->pos3 = $pos3; + } + $table->addChild($node); + } + } else { + foreach ($table->children as $node) { + if ($type3 == $node->realName) { + $node->pos3 = $pos3; + } + $retval[$node->realName] = $node; + } + } + + return $retval; + } + + /** + * Adds containers to a node that is a database + * + * References to existing children are returned + * if this function is called twice on the same node + * + * @param NodeDatabase $db The database node, new containers will be + * attached to this node + * @param string $type The type of item being paginated on + * the second level of the tree + * @param int $pos2 The position for the pagination of + * the branch at the second level of the tree + * + * @return array An array of new nodes + */ + private function addDbContainers($db, $type, $pos2) + { + // Get items to hide + $hidden = $db->getHiddenItems('group'); + if (! $GLOBALS['cfg']['NavigationTreeShowTables'] + && ! in_array('tables', $hidden) + ) { + $hidden[] = 'tables'; + } + if (! $GLOBALS['cfg']['NavigationTreeShowViews'] + && ! in_array('views', $hidden) + ) { + $hidden[] = 'views'; + } + if (! $GLOBALS['cfg']['NavigationTreeShowFunctions'] + && ! in_array('functions', $hidden) + ) { + $hidden[] = 'functions'; + } + if (! $GLOBALS['cfg']['NavigationTreeShowProcedures'] + && ! in_array('procedures', $hidden) + ) { + $hidden[] = 'procedures'; + } + if (! $GLOBALS['cfg']['NavigationTreeShowEvents'] + && ! in_array('events', $hidden) + ) { + $hidden[] = 'events'; + } + + $retval = []; + if ($db->hasChildren(true) == 0) { + if (! in_array('tables', $hidden) && $db->getPresence('tables')) { + $retval['tables'] = NodeFactory::getInstance( + 'NodeTableContainer' + ); + } + if (! in_array('views', $hidden) && $db->getPresence('views')) { + $retval['views'] = NodeFactory::getInstance( + 'NodeViewContainer' + ); + } + if (! in_array('functions', $hidden) && $db->getPresence('functions')) { + $retval['functions'] = NodeFactory::getInstance( + 'NodeFunctionContainer' + ); + } + if (! in_array('procedures', $hidden) && $db->getPresence('procedures')) { + $retval['procedures'] = NodeFactory::getInstance( + 'NodeProcedureContainer' + ); + } + if (! in_array('events', $hidden) && $db->getPresence('events')) { + $retval['events'] = NodeFactory::getInstance( + 'NodeEventContainer' + ); + } + // Add all new Nodes to the tree + foreach ($retval as $node) { + if ($type == $node->realName) { + $node->pos2 = $pos2; + } + $db->addChild($node); + } + } else { + foreach ($db->children as $node) { + if ($type == $node->realName) { + $node->pos2 = $pos2; + } + $retval[$node->realName] = $node; + } + } + + return $retval; + } + + /** + * Recursively groups tree nodes given a separator + * + * @param mixed $node The node to group or null + * to group the whole tree. If + * passed as an argument, $node + * must be of type CONTAINER + * + * @return void + */ + public function groupTree($node = null) + { + if (! isset($node)) { + $node = $this->tree; + } + $this->groupNode($node); + foreach ($node->children as $child) { + $this->groupTree($child); + } + } + + /** + * Recursively groups tree nodes given a separator + * + * @param Node $node The node to group + * + * @return void + */ + public function groupNode($node) + { + if ($node->type != Node::CONTAINER + || ! $GLOBALS['cfg']['NavigationTreeEnableExpansion'] + ) { + return; + } + + $separators = []; + if (is_array($node->separator)) { + $separators = $node->separator; + } else { + if (strlen($node->separator)) { + $separators[] = $node->separator; + } + } + $prefixes = []; + if ($node->separatorDepth > 0) { + foreach ($node->children as $child) { + $prefixPos = false; + foreach ($separators as $separator) { + $sepPos = mb_strpos((string) $child->name, $separator); + if ($sepPos != false + && $sepPos != mb_strlen($child->name) + && $sepPos != 0 + && ($prefixPos === false || $sepPos < $prefixPos) + ) { + $prefixPos = $sepPos; + } + } + if ($prefixPos !== false) { + $prefix = mb_substr($child->name, 0, $prefixPos); + if (! isset($prefixes[$prefix])) { + $prefixes[$prefix] = 1; + } else { + $prefixes[$prefix]++; + } + } + //Bug #4375: Check if prefix is the name of a DB, to create a group. + foreach ($node->children as $otherChild) { + if (array_key_exists($otherChild->name, $prefixes)) { + $prefixes[$otherChild->name]++; + } + } + } + //Check if prefix is the name of a DB, to create a group. + foreach ($node->children as $child) { + if (array_key_exists($child->name, $prefixes)) { + $prefixes[$child->name]++; + } + } + } + // It is not a group if it has only one item + foreach ($prefixes as $key => $value) { + if ($value == 1) { + unset($prefixes[$key]); + } + } + // rfe #1634 Don't group if there's only one group and no other items + if (count($prefixes) === 1) { + $keys = array_keys($prefixes); + $key = $keys[0]; + if ($prefixes[$key] == count($node->children) - 1) { + unset($prefixes[$key]); + } + } + if (count($prefixes)) { + /** @var Node[] $groups */ + $groups = []; + foreach ($prefixes as $key => $value) { + // warn about large groups + if ($value > 500 && ! $this->largeGroupWarning) { + trigger_error( + __( + 'There are large item groups in navigation panel which ' + . 'may affect the performance. Consider disabling item ' + . 'grouping in the navigation panel.' + ), + E_USER_WARNING + ); + $this->largeGroupWarning = true; + } + + $groups[$key] = new Node( + htmlspecialchars((string) $key), + Node::CONTAINER, + true + ); + $groups[$key]->separator = $node->separator; + $groups[$key]->separatorDepth = $node->separatorDepth - 1; + $groups[$key]->icon = Util::getImage( + 'b_group', + __('Groups') + ); + $groups[$key]->pos2 = $node->pos2; + $groups[$key]->pos3 = $node->pos3; + if ($node instanceof NodeTableContainer + || $node instanceof NodeViewContainer + ) { + $tblGroup = '&tbl_group=' . urlencode($key); + $groups[$key]->links = [ + 'text' => $node->links['text'] . $tblGroup, + 'icon' => $node->links['icon'] . $tblGroup, + ]; + } + $node->addChild($groups[$key]); + foreach ($separators as $separator) { + $separatorLength = strlen($separator); + // FIXME: this could be more efficient + foreach ($node->children as $child) { + $keySeparatorLength = mb_strlen((string) $key) + $separatorLength; + $nameSubstring = mb_substr( + (string) $child->name, + 0, + $keySeparatorLength + ); + if (($nameSubstring != $key . $separator + && $child->name != $key) + || $child->type != Node::OBJECT + ) { + continue; + } + $class = get_class($child); + $className = substr($class, strrpos($class, '\\') + 1); + unset($class); + $newChild = NodeFactory::getInstance( + $className, + mb_substr( + $child->name, + $keySeparatorLength + ) + ); + + if ($newChild instanceof NodeDatabase + && $child->getHiddenCount() > 0 + ) { + $newChild->setHiddenCount($child->getHiddenCount()); + } + + $newChild->realName = $child->realName; + $newChild->icon = $child->icon; + $newChild->links = $child->links; + $newChild->pos2 = $child->pos2; + $newChild->pos3 = $child->pos3; + $groups[$key]->addChild($newChild); + foreach ($child->children as $elm) { + $newChild->addChild($elm); + } + $node->removeChild($child->name); + } + } + } + foreach ($prefixes as $key => $value) { + $this->groupNode($groups[$key]); + $groups[$key]->classes = "navGroup"; + } + } + } + + /** + * Renders a state of the tree, used in light mode when + * either JavaScript and/or Ajax are disabled + * + * @return string HTML code for the navigation tree + */ + public function renderState() + { + $this->buildPath(); + + $quickWarp = $this->quickWarp(); + $fastFilter = $this->fastFilterHtml($this->tree); + $controls = ''; + if ($GLOBALS['cfg']['NavigationTreeEnableExpansion']) { + $controls = $this->controls(); + } + $pageSelector = $this->getPageSelector($this->tree); + + $this->groupTree(); + $children = $this->tree->children; + usort($children, [ + NavigationTree::class, + 'sortNode', + ]); + $this->setVisibility(); + + $nodes = ''; + for ($i = 0, $nbChildren = count($children); $i < $nbChildren; $i++) { + if ($i == 0) { + $nodes .= $this->renderNode($children[0], true, 'first'); + } else { + if ($i + 1 != $nbChildren) { + $nodes .= $this->renderNode($children[$i], true); + } else { + $nodes .= $this->renderNode($children[$i], true, 'last'); + } + } + } + + return $this->template->render('navigation/tree/state', [ + 'quick_warp' => $quickWarp, + 'fast_filter' => $fastFilter, + 'controls' => $controls, + 'page_selector' => $pageSelector, + 'nodes' => $nodes, + ]); + } + + /** + * Renders a part of the tree, used for Ajax requests in light mode + * + * @return string|false HTML code for the navigation tree + */ + public function renderPath() + { + $node = $this->buildPath(); + if ($node !== false) { + $this->groupTree(); + + $listContent = $this->fastFilterHtml($node); + $listContent .= $this->getPageSelector($node); + $children = $node->children; + usort($children, [ + NavigationTree::class, + 'sortNode', + ]); + + for ($i = 0, $nbChildren = count($children); $i < $nbChildren; $i++) { + if ($i + 1 != $nbChildren) { + $listContent .= $this->renderNode($children[$i], true); + } else { + $listContent .= $this->renderNode($children[$i], true, 'last'); + } + } + + if (! $GLOBALS['cfg']['ShowDatabasesNavigationAsTree']) { + $parents = $node->parents(true); + $parentName = $parents[0]->realName; + } + } + + if (! empty($this->searchClause) || ! empty($this->searchClause2)) { + $results = 0; + if (! empty($this->searchClause2)) { + if (is_object($node->realParent())) { + $results = $node->realParent() + ->getPresence( + $node->realName, + $this->searchClause2 + ); + } + } else { + $results = $this->tree->getPresence( + 'databases', + $this->searchClause + ); + } + $results = sprintf( + _ngettext( + '%s result found', + '%s results found', + $results + ), + $results + ); + Response::getInstance() + ->addJSON( + 'results', + $results + ); + } + + if ($node !== false) { + return $this->template->render('navigation/tree/path', [ + 'has_search_results' => ! empty($this->searchClause) || ! empty($this->searchClause2), + 'list_content' => $listContent ?? '', + 'is_tree' => $GLOBALS['cfg']['ShowDatabasesNavigationAsTree'], + 'parent_name' => $parentName ?? '', + ]); + } + return false; + } + + /** + * Renders the parameters that are required on the client + * side to know which page(s) we will be requesting data from + * + * @param Node $node The node to create the pagination parameters for + * + * @return string + */ + private function getPaginationParamsHtml($node) + { + $retval = ''; + $paths = $node->getPaths(); + if (isset($paths['aPath_clean'][2])) { + $retval .= ""; + $retval .= $paths['aPath_clean'][2]; + $retval .= ""; + $retval .= ""; + $retval .= htmlspecialchars((string) $node->pos2); + $retval .= ""; + } + if (isset($paths['aPath_clean'][4])) { + $retval .= ""; + $retval .= $paths['aPath_clean'][4]; + $retval .= ""; + $retval .= ""; + $retval .= htmlspecialchars((string) $node->pos3); + $retval .= ""; + } + + return $retval; + } + + /** + * Finds whether given tree matches this tree. + * + * @param array $tree Tree to check + * @param array $paths Paths to check + * + * @return boolean + */ + private function findTreeMatch(array $tree, array $paths) + { + $match = false; + foreach ($tree as $path) { + $match = true; + foreach ($paths as $key => $part) { + if (! isset($path[$key]) || $part != $path[$key]) { + $match = false; + break; + } + } + if ($match) { + break; + } + } + + return $match; + } + + /** + * Renders a single node or a branch of the tree + * + * @param Node $node The node to render + * @param bool $recursive Bool: Whether to render a single node or a branch + * @param string $class An additional class for the list item + * + * @return string HTML code for the tree node or branch + */ + private function renderNode($node, $recursive, $class = '') + { + $retval = ''; + $paths = $node->getPaths(); + if ($node->hasSiblings() + || $node->realParent() === false + ) { + $response = Response::getInstance(); + if ($node->type == Node::CONTAINER + && count($node->children) === 0 + && ! $response->isAjax() + ) { + return ''; + } + $retval .= '
  • '; + $sterile = [ + 'events', + 'triggers', + 'functions', + 'procedures', + 'views', + 'columns', + 'indexes', + ]; + $parentName = ''; + $parents = $node->parents(false, true); + if (count($parents)) { + $parentName = $parents[0]->realName; + } + // if node name itself is in sterile, then allow + if ($node->isGroup + || (! in_array($parentName, $sterile) && ! $node->isNew) + || in_array($node->realName, $sterile) + ) { + $retval .= "
    "; + $iClass = ''; + if ($class == 'first') { + $iClass = " class='first'"; + } + $retval .= ""; + if (strpos($class, 'last') === false) { + $retval .= ""; + } + + $match = $this->findTreeMatch( + $this->vPath, + $paths['vPath_clean'] + ); + + $retval .= 'pos; + $retval .= ""; + $retval .= $this->getPaginationParamsHtml($node); + if ($GLOBALS['cfg']['ShowDatabasesNavigationAsTree'] + || $parentName != 'root' + ) { + $retval .= $node->getIcon($match); + } + + $retval .= ""; + $retval .= "
    "; + } else { + $retval .= "
    "; + $iClass = ''; + if ($class == 'first') { + $iClass = " class='first'"; + } + $retval .= ""; + $retval .= $this->getPaginationParamsHtml($node); + $retval .= "
    "; + } + + $linkClass = ''; + $haveAjax = [ + 'functions', + 'procedures', + 'events', + 'triggers', + 'indexes', + ]; + $parent = $node->parents(false, true); + $isNewView = $parent[0]->realName == 'views' && $node->isNew === true; + if ($parent[0]->type == Node::CONTAINER + && (in_array($parent[0]->realName, $haveAjax) || $isNewView) + ) { + $linkClass = ' ajax'; + } + + if ($node->type == Node::CONTAINER) { + $retval .= ""; + } + + $divClass = ''; + + if (isset($node->links['icon']) && ! empty($node->links['icon'])) { + $iconLinks = $node->links['icon']; + $icons = $node->icon; + if (! is_array($iconLinks)) { + $iconLinks = [$iconLinks]; + $icons = [$icons]; + } + + if (count($icons) > 1) { + $divClass = 'double'; + } + } + + $retval .= "
    "; + + if (isset($node->links['icon']) && ! empty($node->links['icon'])) { + $args = []; + foreach ($node->parents(true) as $parent) { + $args[] = urlencode($parent->realName); + } + + foreach ($icons as $key => $icon) { + $link = vsprintf($iconLinks[$key], $args); + if ($linkClass != '') { + $retval .= ""; + $retval .= "{$icon}"; + } else { + $retval .= "{$icon}"; + } + } + } else { + $retval .= "{$node->icon}"; + } + $retval .= "
    "; + + if (isset($node->links['text'])) { + $args = []; + foreach ($node->parents(true) as $parent) { + $args[] = urlencode($parent->realName); + } + $link = vsprintf($node->links['text'], $args); + $title = isset($node->links['title']) ? $node->links['title'] : ''; + if ($node->type == Node::CONTAINER) { + $retval .= " "; + $retval .= htmlspecialchars($node->name); + $retval .= ""; + } else { + $retval .= "displayName ?? $node->realName); + $retval .= ""; + } + } else { + $retval .= " {$node->name}"; + } + $retval .= $node->getHtmlForControlButtons(); + if ($node->type == Node::CONTAINER) { + $retval .= "
    "; + } + $retval .= '
    '; + $wrap = true; + } else { + $node->visible = true; + $wrap = false; + $retval .= $this->getPaginationParamsHtml($node); + } + + if ($recursive) { + $hide = ''; + if (! $node->visible) { + $hide = " style='display: none;'"; + } + $children = $node->children; + usort( + $children, + [ + NavigationTree::class, + 'sortNode', + ] + ); + $buffer = ''; + $extraClass = ''; + for ($i = 0, $nbChildren = count($children); $i < $nbChildren; $i++) { + if ($i + 1 == $nbChildren) { + $extraClass = ' last'; + } + $buffer .= $this->renderNode( + $children[$i], + true, + $children[$i]->classes . $extraClass + ); + } + if (! empty($buffer)) { + if ($wrap) { + $retval .= "
      "; + } + $retval .= $this->fastFilterHtml($node); + $retval .= $this->getPageSelector($node); + $retval .= $buffer; + if ($wrap) { + $retval .= "
  • "; + } + } + } + if ($node->hasSiblings()) { + $retval .= ""; + } + + return $retval; + } + + /** + * Renders a database select box like the pre-4.0 navigation panel + * + * @return string HTML code + */ + public function renderDbSelect() + { + $this->buildPath(); + + $quickWarp = $this->quickWarp(); + + $this->tree->isGroup = false; + + // Provide for pagination in database select + $listNavigator = Util::getListNavigator( + $this->tree->getPresence('databases', ''), + $this->pos, + ['server' => $GLOBALS['server']], + 'navigation.php', + 'frame_navigation', + $GLOBALS['cfg']['FirstLevelNavigationItems'], + 'pos', + ['dbselector'] + ); + + $children = $this->tree->children; + $selected = $GLOBALS['db']; + $options = ''; + foreach ($children as $node) { + if ($node->isNew) { + continue; + } + $paths = $node->getPaths(); + if (isset($node->links['text'])) { + $title = isset($node->links['title']) ? '' : $node->links['title']; + $options .= ''; + } + } + + $children = $this->tree->children; + usort($children, [ + NavigationTree::class, + 'sortNode', + ]); + $this->setVisibility(); + + $nodes = ''; + for ($i = 0, $nbChildren = count($children); $i < $nbChildren; $i++) { + if ($i == 0) { + $nodes .= $this->renderNode($children[0], true, 'first'); + } else { + if ($i + 1 != $nbChildren) { + $nodes .= $this->renderNode($children[$i], true); + } else { + $nodes .= $this->renderNode($children[$i], true, 'last'); + } + } + } + + return $this->template->render('navigation/tree/database_select', [ + 'quick_warp' => $quickWarp, + 'list_navigator' => $listNavigator, + 'server' => $GLOBALS['server'], + 'options' => $options, + 'nodes' => $nodes, + ]); + } + + /** + * Makes some nodes visible based on the which node is active + * + * @return void + */ + private function setVisibility() + { + foreach ($this->vPath as $path) { + $node = $this->tree; + foreach ($path as $value) { + $child = $node->getChild($value); + if ($child !== false) { + $child->visible = true; + $node = $child; + } + } + } + } + + /** + * Generates the HTML code for displaying the fast filter for tables + * + * @param Node $node The node for which to generate the fast filter html + * + * @return string LI element used for the fast filter + */ + private function fastFilterHtml($node) + { + $retval = ''; + $filterDbMin + = (int) $GLOBALS['cfg']['NavigationTreeDisplayDbFilterMinimum']; + $filterItemMin + = (int) $GLOBALS['cfg']['NavigationTreeDisplayItemFilterMinimum']; + if ($node === $this->tree + && $this->tree->getPresence() >= $filterDbMin + ) { + $urlParams = [ + 'pos' => 0, + ]; + $retval .= '
  • '; + $retval .= '
    '; + $retval .= Url::getHiddenInputs($urlParams); + $retval .= 'X'; + $retval .= "
    "; + $retval .= "
  • "; + + return $retval; + } + + if (($node->type == Node::CONTAINER + && ($node->realName == 'tables' + || $node->realName == 'views' + || $node->realName == 'functions' + || $node->realName == 'procedures' + || $node->realName == 'events')) + && method_exists($node->realParent(), 'getPresence') + && $node->realParent()->getPresence($node->realName) >= $filterItemMin + ) { + $paths = $node->getPaths(); + $urlParams = [ + 'pos' => $this->pos, + 'aPath' => $paths['aPath'], + 'vPath' => $paths['vPath'], + 'pos2_name' => $node->realName, + 'pos2_value' => 0, + ]; + $retval .= "
  • "; + $retval .= "
    "; + $retval .= Url::getHiddenFields($urlParams); + $retval .= ""; + $retval .= "X"; + $retval .= "
    "; + $retval .= "
  • "; + } + + return $retval; + } + + /** + * Creates the code for displaying the controls + * at the top of the navigation tree + * + * @return string HTML code for the controls + */ + private function controls() + { + // always iconic + $showIcon = true; + $showText = false; + + $retval = ''; + $retval .= ''; + $retval .= ''; + + return $retval; + } + + /** + * Generates the HTML code for displaying the list pagination + * + * @param Node $node The node for whose children the page + * selector will be created + * + * @return string + */ + private function getPageSelector($node) + { + $retval = ''; + if ($node === $this->tree) { + $retval .= Util::getListNavigator( + $this->tree->getPresence('databases', $this->searchClause), + $this->pos, + ['server' => $GLOBALS['server']], + 'navigation.php', + 'frame_navigation', + $GLOBALS['cfg']['FirstLevelNavigationItems'], + 'pos', + ['dbselector'] + ); + } else { + if ($node->type == Node::CONTAINER && ! $node->isGroup) { + $paths = $node->getPaths(); + + $level = isset($paths['aPath_clean'][4]) ? 3 : 2; + $urlParams = [ + 'aPath' => $paths['aPath'], + 'vPath' => $paths['vPath'], + 'pos' => $this->pos, + 'server' => $GLOBALS['server'], + 'pos2_name' => $paths['aPath_clean'][2], + ]; + if ($level == 3) { + $pos = $node->pos3; + $urlParams['pos2_value'] = $node->pos2; + $urlParams['pos3_name'] = $paths['aPath_clean'][4]; + } else { + $pos = $node->pos2; + } + $num = $node->realParent() + ->getPresence( + $node->realName, + $this->searchClause2 + ); + $retval .= Util::getListNavigator( + $num, + $pos, + $urlParams, + 'navigation.php', + 'frame_navigation', + $GLOBALS['cfg']['MaxNavigationItems'], + 'pos' . $level . '_value' + ); + } + } + + return $retval; + } + + /** + * Called by usort() for sorting the nodes in a container + * + * @param Node $a The first element used in the comparison + * @param Node $b The second element used in the comparison + * + * @return int See strnatcmp() and strcmp() + */ + public static function sortNode($a, $b) + { + if ($a->isNew) { + return -1; + } + + if ($b->isNew) { + return 1; + } + + if ($GLOBALS['cfg']['NaturalOrder']) { + return strnatcasecmp($a->name, $b->name); + } + + return strcasecmp($a->name, $b->name); + } + + /** + * Display quick warp links, contain Recents and Favorites + * + * @return string HTML code + */ + private function quickWarp() + { + $retval = '
    '; + if ($GLOBALS['cfg']['NumRecentTables'] > 0) { + $retval .= RecentFavoriteTable::getInstance('recent') + ->getHtml(); + } + if ($GLOBALS['cfg']['NumFavoriteTables'] > 0) { + $retval .= RecentFavoriteTable::getInstance('favorite') + ->getHtml(); + } + $retval .= '
    '; + $retval .= '
    '; + + return $retval; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/NodeFactory.php b/srcs/phpmyadmin/libraries/classes/Navigation/NodeFactory.php new file mode 100644 index 0000000..3e751b2 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/NodeFactory.php @@ -0,0 +1,93 @@ +name = $name; + $this->realName = $name; + } + if ($type === Node::CONTAINER) { + $this->type = Node::CONTAINER; + } + $this->isGroup = (bool) $isGroup; + $this->relation = new Relation($GLOBALS['dbi']); + } + + /** + * Adds a child node to this node + * + * @param Node $child A child node + * + * @return void + */ + public function addChild($child) + { + $this->children[] = $child; + $child->parent = $this; + } + + /** + * Returns a child node given it's name + * + * @param string $name The name of requested child + * @param bool $realName Whether to use the "realName" + * instead of "name" in comparisons + * + * @return false|Node The requested child node or false, + * if the requested node cannot be found + */ + public function getChild($name, $realName = false) + { + if ($realName) { + foreach ($this->children as $child) { + if ($child->realName == $name) { + return $child; + } + } + } else { + foreach ($this->children as $child) { + if ($child->name == $name) { + return $child; + } + } + } + + return false; + } + + /** + * Removes a child node from this node + * + * @param string $name The name of child to be removed + * + * @return void + */ + public function removeChild($name) + { + foreach ($this->children as $key => $child) { + if ($child->name == $name) { + unset($this->children[$key]); + break; + } + } + } + + /** + * Retrieves the parents for a node + * + * @param bool $self Whether to include the Node itself in the results + * @param bool $containers Whether to include nodes of type CONTAINER + * @param bool $groups Whether to include nodes which have $group == true + * + * @return array An array of parent Nodes + */ + public function parents($self = false, $containers = false, $groups = false) + { + $parents = []; + if ($self + && ($this->type != Node::CONTAINER || $containers) + && (! $this->isGroup || $groups) + ) { + $parents[] = $this; + } + $parent = $this->parent; + while ($parent !== null) { + if (($parent->type != Node::CONTAINER || $containers) + && (! $parent->isGroup || $groups) + ) { + $parents[] = $parent; + } + $parent = $parent->parent; + } + + return $parents; + } + + /** + * Returns the actual parent of a node. If used twice on an index or columns + * node, it will return the table and database nodes. The names of the returned + * nodes can be used in SQL queries, etc... + * + * @return Node|false + */ + public function realParent() + { + $retval = $this->parents(); + if (count($retval) <= 0) { + return false; + } + + return $retval[0]; + } + + /** + * This function checks if the node has children nodes associated with it + * + * @param bool $countEmptyContainers Whether to count empty child + * containers as valid children + * + * @return bool Whether the node has child nodes + */ + public function hasChildren($countEmptyContainers = true) + { + $retval = false; + if ($countEmptyContainers) { + if (count($this->children)) { + $retval = true; + } + } else { + foreach ($this->children as $child) { + if ($child->type == Node::OBJECT || $child->hasChildren(false)) { + $retval = true; + break; + } + } + } + + return $retval; + } + + /** + * Returns true if the node has some siblings (other nodes on the same tree + * level, in the same branch), false otherwise. + * The only exception is for nodes on + * the third level of the tree (columns and indexes), for which the function + * always returns true. This is because we want to render the containers + * for these nodes + * + * @return bool + */ + public function hasSiblings() + { + $retval = false; + $paths = $this->getPaths(); + if (count($paths['aPath_clean']) > 3) { + return true; + } + + foreach ($this->parent->children as $child) { + if ($child !== $this + && ($child->type == Node::OBJECT || $child->hasChildren(false)) + ) { + $retval = true; + break; + } + } + + return $retval; + } + + /** + * Returns the number of child nodes that a node has associated with it + * + * @return int The number of children nodes + */ + public function numChildren() + { + $retval = 0; + foreach ($this->children as $child) { + if ($child->type == Node::OBJECT) { + $retval++; + } else { + $retval += $child->numChildren(); + } + } + + return $retval; + } + + /** + * Returns the actual path and the virtual paths for a node + * both as clean arrays and base64 encoded strings + * + * @return array + */ + public function getPaths() + { + $aPath = []; + $aPathClean = []; + foreach ($this->parents(true, true, false) as $parent) { + $aPath[] = base64_encode($parent->realName); + $aPathClean[] = $parent->realName; + } + $aPath = implode('.', array_reverse($aPath)); + $aPathClean = array_reverse($aPathClean); + + $vPath = []; + $vPathClean = []; + foreach ($this->parents(true, true, true) as $parent) { + $vPath[] = base64_encode((string) $parent->name); + $vPathClean[] = $parent->name; + } + $vPath = implode('.', array_reverse($vPath)); + $vPathClean = array_reverse($vPathClean); + + return [ + 'aPath' => $aPath, + 'aPath_clean' => $aPathClean, + 'vPath' => $vPath, + 'vPath_clean' => $vPathClean, + ]; + } + + /** + * Returns the names of children of type $type present inside this container + * This method is overridden by the PhpMyAdmin\Navigation\Nodes\NodeDatabase and PhpMyAdmin\Navigation\Nodes\NodeTable classes + * + * @param string $type The type of item we are looking for + * ('tables', 'views', etc) + * @param int $pos The offset of the list within the results + * @param string $searchClause A string used to filter the results of the query + * + * @return array + */ + public function getData($type, $pos, $searchClause = '') + { + $maxItems = $GLOBALS['cfg']['FirstLevelNavigationItems']; + if (! $GLOBALS['cfg']['NavigationTreeEnableGrouping'] + || ! $GLOBALS['cfg']['ShowDatabasesNavigationAsTree'] + ) { + if (isset($GLOBALS['cfg']['Server']['DisableIS']) + && ! $GLOBALS['cfg']['Server']['DisableIS'] + ) { + $query = "SELECT `SCHEMA_NAME` "; + $query .= "FROM `INFORMATION_SCHEMA`.`SCHEMATA` "; + $query .= $this->getWhereClause('SCHEMA_NAME', $searchClause); + $query .= "ORDER BY `SCHEMA_NAME` "; + $query .= "LIMIT $pos, $maxItems"; + $retval = $GLOBALS['dbi']->fetchResult($query); + + return $retval; + } + + if ($GLOBALS['dbs_to_test'] === false) { + $retval = []; + $query = "SHOW DATABASES "; + $query .= $this->getWhereClause('Database', $searchClause); + $handle = $GLOBALS['dbi']->tryQuery($query); + if ($handle === false) { + return $retval; + } + + $count = 0; + if (! $GLOBALS['dbi']->dataSeek($handle, $pos)) { + return $retval; + } + + while ($arr = $GLOBALS['dbi']->fetchArray($handle)) { + if ($count < $maxItems) { + $retval[] = $arr[0]; + $count++; + } else { + break; + } + } + + return $retval; + } + + $retval = []; + $count = 0; + foreach ($this->getDatabasesToSearch($searchClause) as $db) { + $query = "SHOW DATABASES LIKE '" . $db . "'"; + $handle = $GLOBALS['dbi']->tryQuery($query); + if ($handle === false) { + continue; + } + + while ($arr = $GLOBALS['dbi']->fetchArray($handle)) { + if ($this->isHideDb($arr[0])) { + continue; + } + if (in_array($arr[0], $retval)) { + continue; + } + + if ($pos <= 0 && $count < $maxItems) { + $retval[] = $arr[0]; + $count++; + } + $pos--; + } + } + sort($retval); + + return $retval; + } + + $dbSeparator = $GLOBALS['cfg']['NavigationTreeDbSeparator']; + if (isset($GLOBALS['cfg']['Server']['DisableIS']) + && ! $GLOBALS['cfg']['Server']['DisableIS'] + ) { + $query = "SELECT `SCHEMA_NAME` "; + $query .= "FROM `INFORMATION_SCHEMA`.`SCHEMATA`, "; + $query .= "("; + $query .= "SELECT DB_first_level "; + $query .= "FROM ( "; + $query .= "SELECT DISTINCT SUBSTRING_INDEX(SCHEMA_NAME, "; + $query .= "'" . $GLOBALS['dbi']->escapeString($dbSeparator) . "', 1) "; + $query .= "DB_first_level "; + $query .= "FROM INFORMATION_SCHEMA.SCHEMATA "; + $query .= $this->getWhereClause('SCHEMA_NAME', $searchClause); + $query .= ") t "; + $query .= "ORDER BY DB_first_level ASC "; + $query .= "LIMIT $pos, $maxItems"; + $query .= ") t2 "; + $query .= $this->getWhereClause('SCHEMA_NAME', $searchClause); + $query .= "AND 1 = LOCATE(CONCAT(DB_first_level, "; + $query .= "'" . $GLOBALS['dbi']->escapeString($dbSeparator) . "'), "; + $query .= "CONCAT(SCHEMA_NAME, "; + $query .= "'" . $GLOBALS['dbi']->escapeString($dbSeparator) . "')) "; + $query .= "ORDER BY SCHEMA_NAME ASC"; + $retval = $GLOBALS['dbi']->fetchResult($query); + + return $retval; + } + + if ($GLOBALS['dbs_to_test'] === false) { + $query = "SHOW DATABASES "; + $query .= $this->getWhereClause('Database', $searchClause); + $handle = $GLOBALS['dbi']->tryQuery($query); + $prefixes = []; + if ($handle !== false) { + $prefixMap = []; + $total = $pos + $maxItems; + while ($arr = $GLOBALS['dbi']->fetchArray($handle)) { + $prefix = strstr($arr[0], $dbSeparator, true); + if ($prefix === false) { + $prefix = $arr[0]; + } + $prefixMap[$prefix] = 1; + if (count($prefixMap) == $total) { + break; + } + } + $prefixes = array_slice(array_keys($prefixMap), (int) $pos); + } + + $query = "SHOW DATABASES "; + $query .= $this->getWhereClause('Database', $searchClause); + $query .= "AND ("; + $subClauses = []; + foreach ($prefixes as $prefix) { + $subClauses[] = " LOCATE('" + . $GLOBALS['dbi']->escapeString((string) $prefix) . $dbSeparator + . "', " + . "CONCAT(`Database`, '" . $dbSeparator . "')) = 1 "; + } + $query .= implode("OR", $subClauses) . ")"; + $retval = $GLOBALS['dbi']->fetchResult($query); + + return $retval; + } + + $retval = []; + $prefixMap = []; + $total = $pos + $maxItems; + foreach ($this->getDatabasesToSearch($searchClause) as $db) { + $query = "SHOW DATABASES LIKE '" . $db . "'"; + $handle = $GLOBALS['dbi']->tryQuery($query); + if ($handle === false) { + continue; + } + + while ($arr = $GLOBALS['dbi']->fetchArray($handle)) { + if ($this->isHideDb($arr[0])) { + continue; + } + $prefix = strstr($arr[0], $dbSeparator, true); + if ($prefix === false) { + $prefix = $arr[0]; + } + $prefixMap[$prefix] = 1; + if (count($prefixMap) == $total) { + break 2; + } + } + } + $prefixes = array_slice(array_keys($prefixMap), $pos); + + foreach ($this->getDatabasesToSearch($searchClause) as $db) { + $query = "SHOW DATABASES LIKE '" . $db . "'"; + $handle = $GLOBALS['dbi']->tryQuery($query); + if ($handle === false) { + continue; + } + + while ($arr = $GLOBALS['dbi']->fetchArray($handle)) { + if ($this->isHideDb($arr[0])) { + continue; + } + if (in_array($arr[0], $retval)) { + continue; + } + + foreach ($prefixes as $prefix) { + $startsWith = strpos( + $arr[0] . $dbSeparator, + $prefix . $dbSeparator + ) === 0; + if ($startsWith) { + $retval[] = $arr[0]; + break; + } + } + } + } + sort($retval); + + return $retval; + } + + /** + * Returns the number of children of type $type present inside this container + * This method is overridden by the PhpMyAdmin\Navigation\Nodes\NodeDatabase and PhpMyAdmin\Navigation\Nodes\NodeTable classes + * + * @param string $type The type of item we are looking for + * ('tables', 'views', etc) + * @param string $searchClause A string used to filter the results of the query + * + * @return int + */ + public function getPresence($type = '', $searchClause = '') + { + if (! $GLOBALS['cfg']['NavigationTreeEnableGrouping'] + || ! $GLOBALS['cfg']['ShowDatabasesNavigationAsTree'] + ) { + if (isset($GLOBALS['cfg']['Server']['DisableIS']) + && ! $GLOBALS['cfg']['Server']['DisableIS'] + ) { + $query = "SELECT COUNT(*) "; + $query .= "FROM INFORMATION_SCHEMA.SCHEMATA "; + $query .= $this->getWhereClause('SCHEMA_NAME', $searchClause); + $retval = (int) $GLOBALS['dbi']->fetchValue($query); + + return $retval; + } + + if ($GLOBALS['dbs_to_test'] === false) { + $query = "SHOW DATABASES "; + $query .= $this->getWhereClause('Database', $searchClause); + $retval = $GLOBALS['dbi']->numRows( + $GLOBALS['dbi']->tryQuery($query) + ); + + return $retval; + } + + $retval = 0; + foreach ($this->getDatabasesToSearch($searchClause) as $db) { + $query = "SHOW DATABASES LIKE '" . $db . "'"; + $retval += $GLOBALS['dbi']->numRows( + $GLOBALS['dbi']->tryQuery($query) + ); + } + + return $retval; + } + + $dbSeparator = $GLOBALS['cfg']['NavigationTreeDbSeparator']; + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $query = "SELECT COUNT(*) "; + $query .= "FROM ( "; + $query .= "SELECT DISTINCT SUBSTRING_INDEX(SCHEMA_NAME, "; + $query .= "'$dbSeparator', 1) "; + $query .= "DB_first_level "; + $query .= "FROM INFORMATION_SCHEMA.SCHEMATA "; + $query .= $this->getWhereClause('SCHEMA_NAME', $searchClause); + $query .= ") t "; + $retval = (int) $GLOBALS['dbi']->fetchValue($query); + + return $retval; + } + + if ($GLOBALS['dbs_to_test'] !== false) { + $prefixMap = []; + foreach ($this->getDatabasesToSearch($searchClause) as $db) { + $query = "SHOW DATABASES LIKE '" . $db . "'"; + $handle = $GLOBALS['dbi']->tryQuery($query); + if ($handle === false) { + continue; + } + + while ($arr = $GLOBALS['dbi']->fetchArray($handle)) { + if ($this->isHideDb($arr[0])) { + continue; + } + $prefix = strstr($arr[0], $dbSeparator, true); + if ($prefix === false) { + $prefix = $arr[0]; + } + $prefixMap[$prefix] = 1; + } + } + $retval = count($prefixMap); + + return $retval; + } + + $prefixMap = []; + $query = "SHOW DATABASES "; + $query .= $this->getWhereClause('Database', $searchClause); + $handle = $GLOBALS['dbi']->tryQuery($query); + if ($handle !== false) { + while ($arr = $GLOBALS['dbi']->fetchArray($handle)) { + $prefix = strstr($arr[0], $dbSeparator, true); + if ($prefix === false) { + $prefix = $arr[0]; + } + $prefixMap[$prefix] = 1; + } + } + $retval = count($prefixMap); + + return $retval; + } + + /** + * Detemines whether a given database should be hidden according to 'hide_db' + * + * @param string $db database name + * + * @return boolean whether to hide + */ + private function isHideDb($db) + { + return ! empty($GLOBALS['cfg']['Server']['hide_db']) + && preg_match('/' . $GLOBALS['cfg']['Server']['hide_db'] . '/', $db); + } + + /** + * Get the list of databases for 'SHOW DATABASES LIKE' queries. + * If a search clause is set it gets the highest priority while only_db gets + * the next priority. In case both are empty list of databases determined by + * GRANTs are used + * + * @param string $searchClause search clause + * + * @return array array of databases + */ + private function getDatabasesToSearch($searchClause) + { + $databases = []; + if (! empty($searchClause)) { + $databases = [ + "%" . $GLOBALS['dbi']->escapeString($searchClause) . "%", + ]; + } elseif (! empty($GLOBALS['cfg']['Server']['only_db'])) { + $databases = $GLOBALS['cfg']['Server']['only_db']; + } elseif (! empty($GLOBALS['dbs_to_test'])) { + $databases = $GLOBALS['dbs_to_test']; + } + sort($databases); + + return $databases; + } + + /** + * Returns the WHERE clause depending on the $searchClause parameter + * and the hide_db directive + * + * @param string $columnName Column name of the column having database names + * @param string $searchClause A string used to filter the results of the query + * + * @return string + */ + private function getWhereClause($columnName, $searchClause = '') + { + $whereClause = "WHERE TRUE "; + if (! empty($searchClause)) { + $whereClause .= "AND " . Util::backquote($columnName) + . " LIKE '%"; + $whereClause .= $GLOBALS['dbi']->escapeString($searchClause); + $whereClause .= "%' "; + } + + if (! empty($GLOBALS['cfg']['Server']['hide_db'])) { + $whereClause .= "AND " . Util::backquote($columnName) + . " NOT REGEXP '" + . $GLOBALS['dbi']->escapeString($GLOBALS['cfg']['Server']['hide_db']) + . "' "; + } + + if (! empty($GLOBALS['cfg']['Server']['only_db'])) { + if (is_string($GLOBALS['cfg']['Server']['only_db'])) { + $GLOBALS['cfg']['Server']['only_db'] = [ + $GLOBALS['cfg']['Server']['only_db'], + ]; + } + $whereClause .= "AND ("; + $subClauses = []; + foreach ($GLOBALS['cfg']['Server']['only_db'] as $eachOnlyDb) { + $subClauses[] = " " . Util::backquote($columnName) + . " LIKE '" + . $GLOBALS['dbi']->escapeString($eachOnlyDb) . "' "; + } + $whereClause .= implode("OR", $subClauses) . ") "; + } + + return $whereClause; + } + + /** + * Returns HTML for control buttons displayed infront of a node + * + * @return String HTML for control buttons + */ + public function getHtmlForControlButtons() + { + return ''; + } + + /** + * Returns CSS classes for a node + * + * @param boolean $match Whether the node matched loaded tree + * + * @return String with html classes. + */ + public function getCssClasses($match) + { + if (! $GLOBALS['cfg']['NavigationTreeEnableExpansion'] + ) { + return ''; + } + + $result = ['expander']; + + if ($this->isGroup || $match) { + $result[] = 'loaded'; + } + if ($this->type == Node::CONTAINER) { + $result[] = 'container'; + } + + return implode(' ', $result); + } + + /** + * Returns icon for the node + * + * @param boolean $match Whether the node matched loaded tree + * + * @return String with image name + */ + public function getIcon($match) + { + if (! $GLOBALS['cfg']['NavigationTreeEnableExpansion'] + ) { + return ''; + } elseif ($match) { + $this->visible = true; + + return Util::getImage('b_minus'); + } + + return Util::getImage('b_plus', __('Expand/Collapse')); + } + + /** + * Gets the count of hidden elements for each database + * + * @return array|null array containing the count of hidden elements for each database + */ + public function getNavigationHidingData() + { + $cfgRelation = $this->relation->getRelationsParam(); + if ($cfgRelation['navwork']) { + $navTable = Util::backquote($cfgRelation['db']) + . "." . Util::backquote( + $cfgRelation['navigationhiding'] + ); + $sqlQuery = "SELECT `db_name`, COUNT(*) AS `count` FROM " . $navTable + . " WHERE `username`='" + . $GLOBALS['dbi']->escapeString( + $GLOBALS['cfg']['Server']['user'] + ) . "'" + . " GROUP BY `db_name`"; + $counts = $GLOBALS['dbi']->fetchResult( + $sqlQuery, + 'db_name', + 'count', + DatabaseInterface::CONNECT_CONTROL + ); + + return $counts; + } + + return null; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeColumn.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeColumn.php new file mode 100644 index 0000000..9c9e605 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeColumn.php @@ -0,0 +1,116 @@ +displayName = $this->getDisplayName($item); + + parent::__construct($item['name'], $type, $isGroup); + $this->icon = Util::getImage($this->getColumnIcon($item['key']), __('Column')); + $this->links = [ + 'text' => 'tbl_structure.php?server=' . $GLOBALS['server'] + . '&db=%3$s&table=%2$s&field=%1$s' + . '&change_column=1', + 'icon' => 'tbl_structure.php?server=' . $GLOBALS['server'] + . '&db=%3$s&table=%2$s&field=%1$s' + . '&change_column=1', + 'title' => __('Structure'), + ]; + } + + /** + * Get customized Icon for columns in navigation tree + * + * @param string $key The key type - (primary, foreign etc.) + * + * @return string Icon name for required key. + */ + private function getColumnIcon($key) + { + switch ($key) { + case 'PRI': + $retval = 'b_primary'; + break; + case 'UNI': + $retval = 'bd_primary'; + break; + default: + $retval = 'pause'; + break; + } + return $retval; + } + + /** + * Get displayable name for navigation tree (key_type, data_type, default) + * + * @param array $item Item is array containing required info + * + * @return string Display name for navigation tree + */ + private function getDisplayName($item) + { + $retval = $item['name']; + $flag = 0; + foreach ($item as $key => $value) { + if (! empty($value) && $key != 'name') { + $flag == 0 ? $retval .= ' (' : $retval .= ', '; + $flag = 1; + $retval .= $this->getTruncateValue($key, $value); + } + } + $retval .= ')'; + return $retval; + } + + /** + * Get truncated value for display in node column view + * + * @param string $key key to identify default,datatype etc + * @param string $value value corresponding to key + * + * @return string truncated value + */ + public function getTruncateValue($key, $value) + { + $retval = ''; + + switch ($key) { + case 'default': + strlen($value) > 6 ? + $retval .= substr($value, 0, 6) . '...' : + $retval = $value; + break; + default: + $retval = $value; + break; + } + + return $retval; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeColumnContainer.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeColumnContainer.php new file mode 100644 index 0000000..22cba58 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeColumnContainer.php @@ -0,0 +1,55 @@ +icon = Util::getImage('pause', __('Columns')); + $this->links = [ + 'text' => 'tbl_structure.php?server=' . $GLOBALS['server'] + . '&db=%2$s&table=%1$s', + 'icon' => 'tbl_structure.php?server=' . $GLOBALS['server'] + . '&db=%2$s&table=%1$s', + ]; + $this->realName = 'columns'; + + $newLabel = _pgettext('Create new column', 'New'); + $new = NodeFactory::getInstance( + 'Node', + $newLabel + ); + $new->isNew = true; + $new->icon = Util::getImage('b_column_add', $newLabel); + $new->links = [ + 'text' => 'tbl_addfield.php?server=' . $GLOBALS['server'] + . '&db=%3$s&table=%2$s' + . '&field_where=last&after_field=', + 'icon' => 'tbl_addfield.php?server=' . $GLOBALS['server'] + . '&db=%3$s&table=%2$s' + . '&field_where=last&after_field=', + ]; + $new->classes = 'new_column italics'; + $this->addChild($new); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeDatabase.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeDatabase.php new file mode 100644 index 0000000..33c93fb --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeDatabase.php @@ -0,0 +1,717 @@ +icon = Util::getImage( + 's_db', + __('Database operations') + ); + + $scriptName = Util::getScriptNameForOption( + $GLOBALS['cfg']['DefaultTabDatabase'], + 'database' + ); + $this->links = [ + 'text' => $scriptName + . '?server=' . $GLOBALS['server'] + . '&db=%1$s', + 'icon' => 'db_operations.php?server=' . $GLOBALS['server'] + . '&db=%1$s&', + 'title' => __('Structure'), + ]; + $this->classes = 'database'; + } + + /** + * Returns the number of children of type $type present inside this container + * This method is overridden by the PhpMyAdmin\Navigation\Nodes\NodeDatabase + * and PhpMyAdmin\Navigation\Nodes\NodeTable classes + * + * @param string $type The type of item we are looking for + * ('tables', 'views', etc) + * @param string $searchClause A string used to filter the results of + * the query + * @param boolean $singleItem Whether to get presence of a single known + * item or false in none + * + * @return int + */ + public function getPresence($type = '', $searchClause = '', $singleItem = false) + { + $retval = 0; + switch ($type) { + case 'tables': + $retval = $this->getTableCount($searchClause, $singleItem); + break; + case 'views': + $retval = $this->getViewCount($searchClause, $singleItem); + break; + case 'procedures': + $retval = $this->getProcedureCount($searchClause, $singleItem); + break; + case 'functions': + $retval = $this->getFunctionCount($searchClause, $singleItem); + break; + case 'events': + $retval = $this->getEventCount($searchClause, $singleItem); + break; + default: + break; + } + + return $retval; + } + + /** + * Returns the number of tables or views present inside this database + * + * @param string $which tables|views + * @param string $searchClause A string used to filter the results of + * the query + * @param boolean $singleItem Whether to get presence of a single known + * item or false in none + * + * @return int + */ + private function getTableOrViewCount($which, $searchClause, $singleItem) + { + $db = $this->realName; + if ($which == 'tables') { + $condition = 'IN'; + } else { + $condition = 'NOT IN'; + } + + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $db = $GLOBALS['dbi']->escapeString($db); + $query = "SELECT COUNT(*) "; + $query .= "FROM `INFORMATION_SCHEMA`.`TABLES` "; + $query .= "WHERE `TABLE_SCHEMA`='$db' "; + $query .= "AND `TABLE_TYPE`" . $condition . "('BASE TABLE', 'SYSTEM VERSIONED') "; + if (! empty($searchClause)) { + $query .= "AND " . $this->getWhereClauseForSearch( + $searchClause, + $singleItem, + 'TABLE_NAME' + ); + } + $retval = (int) $GLOBALS['dbi']->fetchValue($query); + } else { + $query = "SHOW FULL TABLES FROM "; + $query .= Util::backquote($db); + $query .= " WHERE `Table_type`" . $condition . "('BASE TABLE', 'SYSTEM VERSIONED') "; + if (! empty($searchClause)) { + $query .= "AND " . $this->getWhereClauseForSearch( + $searchClause, + $singleItem, + 'Tables_in_' . $db + ); + } + $retval = $GLOBALS['dbi']->numRows( + $GLOBALS['dbi']->tryQuery($query) + ); + } + + return $retval; + } + + /** + * Returns the number of tables present inside this database + * + * @param string $searchClause A string used to filter the results of + * the query + * @param boolean $singleItem Whether to get presence of a single known + * item or false in none + * + * @return int + */ + private function getTableCount($searchClause, $singleItem) + { + return $this->getTableOrViewCount( + 'tables', + $searchClause, + $singleItem + ); + } + + /** + * Returns the number of views present inside this database + * + * @param string $searchClause A string used to filter the results of + * the query + * @param boolean $singleItem Whether to get presence of a single known + * item or false in none + * + * @return int + */ + private function getViewCount($searchClause, $singleItem) + { + return $this->getTableOrViewCount( + 'views', + $searchClause, + $singleItem + ); + } + + /** + * Returns the number of procedures present inside this database + * + * @param string $searchClause A string used to filter the results of + * the query + * @param boolean $singleItem Whether to get presence of a single known + * item or false in none + * + * @return int + */ + private function getProcedureCount($searchClause, $singleItem) + { + $db = $this->realName; + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $db = $GLOBALS['dbi']->escapeString($db); + $query = "SELECT COUNT(*) "; + $query .= "FROM `INFORMATION_SCHEMA`.`ROUTINES` "; + $query .= "WHERE `ROUTINE_SCHEMA` " + . Util::getCollateForIS() . "='$db'"; + $query .= "AND `ROUTINE_TYPE`='PROCEDURE' "; + if (! empty($searchClause)) { + $query .= "AND " . $this->getWhereClauseForSearch( + $searchClause, + $singleItem, + 'ROUTINE_NAME' + ); + } + $retval = (int) $GLOBALS['dbi']->fetchValue($query); + } else { + $db = $GLOBALS['dbi']->escapeString($db); + $query = "SHOW PROCEDURE STATUS WHERE `Db`='$db' "; + if (! empty($searchClause)) { + $query .= "AND " . $this->getWhereClauseForSearch( + $searchClause, + $singleItem, + 'Name' + ); + } + $retval = $GLOBALS['dbi']->numRows( + $GLOBALS['dbi']->tryQuery($query) + ); + } + + return $retval; + } + + /** + * Returns the number of functions present inside this database + * + * @param string $searchClause A string used to filter the results of + * the query + * @param boolean $singleItem Whether to get presence of a single known + * item or false in none + * + * @return int + */ + private function getFunctionCount($searchClause, $singleItem) + { + $db = $this->realName; + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $db = $GLOBALS['dbi']->escapeString($db); + $query = "SELECT COUNT(*) "; + $query .= "FROM `INFORMATION_SCHEMA`.`ROUTINES` "; + $query .= "WHERE `ROUTINE_SCHEMA` " + . Util::getCollateForIS() . "='$db' "; + $query .= "AND `ROUTINE_TYPE`='FUNCTION' "; + if (! empty($searchClause)) { + $query .= "AND " . $this->getWhereClauseForSearch( + $searchClause, + $singleItem, + 'ROUTINE_NAME' + ); + } + $retval = (int) $GLOBALS['dbi']->fetchValue($query); + } else { + $db = $GLOBALS['dbi']->escapeString($db); + $query = "SHOW FUNCTION STATUS WHERE `Db`='$db' "; + if (! empty($searchClause)) { + $query .= "AND " . $this->getWhereClauseForSearch( + $searchClause, + $singleItem, + 'Name' + ); + } + $retval = $GLOBALS['dbi']->numRows( + $GLOBALS['dbi']->tryQuery($query) + ); + } + + return $retval; + } + + /** + * Returns the number of events present inside this database + * + * @param string $searchClause A string used to filter the results of + * the query + * @param boolean $singleItem Whether to get presence of a single known + * item or false in none + * + * @return int + */ + private function getEventCount($searchClause, $singleItem) + { + $db = $this->realName; + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $db = $GLOBALS['dbi']->escapeString($db); + $query = "SELECT COUNT(*) "; + $query .= "FROM `INFORMATION_SCHEMA`.`EVENTS` "; + $query .= "WHERE `EVENT_SCHEMA` " + . Util::getCollateForIS() . "='$db' "; + if (! empty($searchClause)) { + $query .= "AND " . $this->getWhereClauseForSearch( + $searchClause, + $singleItem, + 'EVENT_NAME' + ); + } + $retval = (int) $GLOBALS['dbi']->fetchValue($query); + } else { + $db = Util::backquote($db); + $query = "SHOW EVENTS FROM $db "; + if (! empty($searchClause)) { + $query .= "WHERE " . $this->getWhereClauseForSearch( + $searchClause, + $singleItem, + 'Name' + ); + } + $retval = $GLOBALS['dbi']->numRows( + $GLOBALS['dbi']->tryQuery($query) + ); + } + + return $retval; + } + + /** + * Returns the WHERE clause for searching inside a database + * + * @param string $searchClause A string used to filter the results of the query + * @param boolean $singleItem Whether to get presence of a single known item + * @param string $columnName Name of the column in the result set to match + * + * @return string WHERE clause for searching + */ + private function getWhereClauseForSearch( + $searchClause, + $singleItem, + $columnName + ) { + $query = ''; + if ($singleItem) { + $query .= Util::backquote($columnName) . " = "; + $query .= "'" . $GLOBALS['dbi']->escapeString($searchClause) . "'"; + } else { + $query .= Util::backquote($columnName) . " LIKE "; + $query .= "'%" . $GLOBALS['dbi']->escapeString($searchClause) + . "%'"; + } + + return $query; + } + + /** + * Returns the names of children of type $type present inside this container + * This method is overridden by the PhpMyAdmin\Navigation\Nodes\NodeDatabase + * and PhpMyAdmin\Navigation\Nodes\NodeTable classes + * + * @param string $type The type of item we are looking for + * ('tables', 'views', etc) + * @param int $pos The offset of the list within the results + * @param string $searchClause A string used to filter the results of the query + * + * @return array + */ + public function getData($type, $pos, $searchClause = '') + { + $retval = []; + switch ($type) { + case 'tables': + $retval = $this->getTables($pos, $searchClause); + break; + case 'views': + $retval = $this->getViews($pos, $searchClause); + break; + case 'procedures': + $retval = $this->getProcedures($pos, $searchClause); + break; + case 'functions': + $retval = $this->getFunctions($pos, $searchClause); + break; + case 'events': + $retval = $this->getEvents($pos, $searchClause); + break; + default: + break; + } + + // Remove hidden items so that they are not displayed in navigation tree + $cfgRelation = $this->relation->getRelationsParam(); + if ($cfgRelation['navwork']) { + $hiddenItems = $this->getHiddenItems(substr($type, 0, -1)); + foreach ($retval as $key => $item) { + if (in_array($item, $hiddenItems)) { + unset($retval[$key]); + } + } + } + + return $retval; + } + + /** + * Return list of hidden items of given type + * + * @param string $type The type of items we are looking for + * ('table', 'function', 'group', etc.) + * + * @return array Array containing hidden items of given type + */ + public function getHiddenItems($type) + { + $db = $this->realName; + $cfgRelation = $this->relation->getRelationsParam(); + if (! $cfgRelation['navwork']) { + return []; + } + $navTable = Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['navigationhiding']); + $sqlQuery = "SELECT `item_name` FROM " . $navTable + . " WHERE `username`='" . $cfgRelation['user'] . "'" + . " AND `item_type`='" . $type + . "' AND `db_name`='" . $GLOBALS['dbi']->escapeString($db) + . "'"; + $result = $this->relation->queryAsControlUser($sqlQuery, false); + $hiddenItems = []; + if ($result) { + while ($row = $GLOBALS['dbi']->fetchArray($result)) { + $hiddenItems[] = $row[0]; + } + } + $GLOBALS['dbi']->freeResult($result); + + return $hiddenItems; + } + + /** + * Returns the list of tables or views inside this database + * + * @param string $which tables|views + * @param int $pos The offset of the list within the results + * @param string $searchClause A string used to filter the results of the query + * + * @return array + */ + private function getTablesOrViews($which, $pos, $searchClause) + { + if ($which == 'tables') { + $condition = 'IN'; + } else { + $condition = 'NOT IN'; + } + $maxItems = $GLOBALS['cfg']['MaxNavigationItems']; + $retval = []; + $db = $this->realName; + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $escdDb = $GLOBALS['dbi']->escapeString($db); + $query = "SELECT `TABLE_NAME` AS `name` "; + $query .= "FROM `INFORMATION_SCHEMA`.`TABLES` "; + $query .= "WHERE `TABLE_SCHEMA`='$escdDb' "; + $query .= "AND `TABLE_TYPE`" . $condition . "('BASE TABLE', 'SYSTEM VERSIONED') "; + if (! empty($searchClause)) { + $query .= "AND `TABLE_NAME` LIKE '%"; + $query .= $GLOBALS['dbi']->escapeString($searchClause); + $query .= "%'"; + } + $query .= "ORDER BY `TABLE_NAME` ASC "; + $query .= "LIMIT " . intval($pos) . ", $maxItems"; + $retval = $GLOBALS['dbi']->fetchResult($query); + } else { + $query = " SHOW FULL TABLES FROM "; + $query .= Util::backquote($db); + $query .= " WHERE `Table_type`" . $condition . "('BASE TABLE', 'SYSTEM VERSIONED') "; + if (! empty($searchClause)) { + $query .= "AND " . Util::backquote( + "Tables_in_" . $db + ); + $query .= " LIKE '%" . $GLOBALS['dbi']->escapeString( + $searchClause + ); + $query .= "%'"; + } + $handle = $GLOBALS['dbi']->tryQuery($query); + if ($handle !== false) { + $count = 0; + if ($GLOBALS['dbi']->dataSeek($handle, $pos)) { + while ($arr = $GLOBALS['dbi']->fetchArray($handle)) { + if ($count < $maxItems) { + $retval[] = $arr[0]; + $count++; + } else { + break; + } + } + } + } + } + + return $retval; + } + + /** + * Returns the list of tables inside this database + * + * @param int $pos The offset of the list within the results + * @param string $searchClause A string used to filter the results of the query + * + * @return array + */ + private function getTables($pos, $searchClause) + { + return $this->getTablesOrViews('tables', $pos, $searchClause); + } + + /** + * Returns the list of views inside this database + * + * @param int $pos The offset of the list within the results + * @param string $searchClause A string used to filter the results of the query + * + * @return array + */ + private function getViews($pos, $searchClause) + { + return $this->getTablesOrViews('views', $pos, $searchClause); + } + + /** + * Returns the list of procedures or functions inside this database + * + * @param string $routineType PROCEDURE|FUNCTION + * @param int $pos The offset of the list within the results + * @param string $searchClause A string used to filter the results of the query + * + * @return array + */ + private function getRoutines($routineType, $pos, $searchClause) + { + $maxItems = $GLOBALS['cfg']['MaxNavigationItems']; + $retval = []; + $db = $this->realName; + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $escdDb = $GLOBALS['dbi']->escapeString($db); + $query = "SELECT `ROUTINE_NAME` AS `name` "; + $query .= "FROM `INFORMATION_SCHEMA`.`ROUTINES` "; + $query .= "WHERE `ROUTINE_SCHEMA` " + . Util::getCollateForIS() . "='$escdDb'"; + $query .= "AND `ROUTINE_TYPE`='" . $routineType . "' "; + if (! empty($searchClause)) { + $query .= "AND `ROUTINE_NAME` LIKE '%"; + $query .= $GLOBALS['dbi']->escapeString($searchClause); + $query .= "%'"; + } + $query .= "ORDER BY `ROUTINE_NAME` ASC "; + $query .= "LIMIT " . intval($pos) . ", $maxItems"; + $retval = $GLOBALS['dbi']->fetchResult($query); + } else { + $escdDb = $GLOBALS['dbi']->escapeString($db); + $query = "SHOW " . $routineType . " STATUS WHERE `Db`='$escdDb' "; + if (! empty($searchClause)) { + $query .= "AND `Name` LIKE '%"; + $query .= $GLOBALS['dbi']->escapeString($searchClause); + $query .= "%'"; + } + $handle = $GLOBALS['dbi']->tryQuery($query); + if ($handle !== false) { + $count = 0; + if ($GLOBALS['dbi']->dataSeek($handle, $pos)) { + while ($arr = $GLOBALS['dbi']->fetchArray($handle)) { + if ($count < $maxItems) { + $retval[] = $arr['Name']; + $count++; + } else { + break; + } + } + } + } + } + + return $retval; + } + + /** + * Returns the list of procedures inside this database + * + * @param int $pos The offset of the list within the results + * @param string $searchClause A string used to filter the results of the query + * + * @return array + */ + private function getProcedures($pos, $searchClause) + { + return $this->getRoutines('PROCEDURE', $pos, $searchClause); + } + + /** + * Returns the list of functions inside this database + * + * @param int $pos The offset of the list within the results + * @param string $searchClause A string used to filter the results of the query + * + * @return array + */ + private function getFunctions($pos, $searchClause) + { + return $this->getRoutines('FUNCTION', $pos, $searchClause); + } + + /** + * Returns the list of events inside this database + * + * @param int $pos The offset of the list within the results + * @param string $searchClause A string used to filter the results of the query + * + * @return array + */ + private function getEvents($pos, $searchClause) + { + $maxItems = $GLOBALS['cfg']['MaxNavigationItems']; + $retval = []; + $db = $this->realName; + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $escdDb = $GLOBALS['dbi']->escapeString($db); + $query = "SELECT `EVENT_NAME` AS `name` "; + $query .= "FROM `INFORMATION_SCHEMA`.`EVENTS` "; + $query .= "WHERE `EVENT_SCHEMA` " + . Util::getCollateForIS() . "='$escdDb' "; + if (! empty($searchClause)) { + $query .= "AND `EVENT_NAME` LIKE '%"; + $query .= $GLOBALS['dbi']->escapeString($searchClause); + $query .= "%'"; + } + $query .= "ORDER BY `EVENT_NAME` ASC "; + $query .= "LIMIT " . intval($pos) . ", $maxItems"; + $retval = $GLOBALS['dbi']->fetchResult($query); + } else { + $escdDb = Util::backquote($db); + $query = "SHOW EVENTS FROM $escdDb "; + if (! empty($searchClause)) { + $query .= "WHERE `Name` LIKE '%"; + $query .= $GLOBALS['dbi']->escapeString($searchClause); + $query .= "%'"; + } + $handle = $GLOBALS['dbi']->tryQuery($query); + if ($handle !== false) { + $count = 0; + if ($GLOBALS['dbi']->dataSeek($handle, $pos)) { + while ($arr = $GLOBALS['dbi']->fetchArray($handle)) { + if ($count < $maxItems) { + $retval[] = $arr['Name']; + $count++; + } else { + break; + } + } + } + } + } + + return $retval; + } + + /** + * Returns HTML for control buttons displayed infront of a node + * + * @return String HTML for control buttons + */ + public function getHtmlForControlButtons() + { + $ret = ''; + $cfgRelation = $this->relation->getRelationsParam(); + if ($cfgRelation['navwork']) { + if ($this->hiddenCount > 0) { + $params = [ + 'showUnhideDialog' => true, + 'dbName' => $this->realName, + ]; + $ret = '' + . '' + . Util::getImage( + 'show', + __('Show hidden items') + ) + . ''; + } + } + + return $ret; + } + + /** + * Sets the number of hidden items in this database + * + * @param int $count hidden item count + * + * @return void + */ + public function setHiddenCount($count) + { + $this->hiddenCount = $count; + } + + /** + * Returns the number of hidden items in this database + * + * @return int hidden item count + */ + public function getHiddenCount() + { + return $this->hiddenCount; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeDatabaseChild.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeDatabaseChild.php new file mode 100644 index 0000000..0005915 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeDatabaseChild.php @@ -0,0 +1,62 @@ +relation->getRelationsParam(); + if ($cfgRelation['navwork']) { + $db = $this->realParent()->realName; + $item = $this->realName; + + $params = [ + 'hideNavItem' => true, + 'itemType' => $this->getItemType(), + 'itemName' => $item, + 'dbName' => $db, + ]; + + $ret = '' + . '' + . Util::getImage('hide', __('Hide')) + . ''; + } + + return $ret; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeDatabaseChildContainer.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeDatabaseChildContainer.php new file mode 100644 index 0000000..7c182c6 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeDatabaseChildContainer.php @@ -0,0 +1,43 @@ +separator = $GLOBALS['cfg']['NavigationTreeTableSeparator']; + $this->separatorDepth = (int) $GLOBALS['cfg']['NavigationTreeTableLevel']; + } + } + + /** + * Returns the type of the item represented by the node. + * + * @return string type of the item + */ + protected function getItemType() + { + return 'group'; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeDatabaseContainer.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeDatabaseContainer.php new file mode 100644 index 0000000..9b5746f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeDatabaseContainer.php @@ -0,0 +1,52 @@ +getPrivileges(); + + parent::__construct($name, Node::CONTAINER); + + if ($GLOBALS['is_create_db_priv'] + && $GLOBALS['cfg']['ShowCreateDb'] !== false + ) { + $new = NodeFactory::getInstance( + 'Node', + _pgettext('Create new database', 'New') + ); + $new->isNew = true; + $new->icon = Util::getImage('b_newdb', ''); + $new->links = [ + 'text' => 'server_databases.php?server=' . $GLOBALS['server'], + 'icon' => 'server_databases.php?server=' . $GLOBALS['server'], + ]; + $new->classes = 'new_database italics'; + $this->addChild($new); + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeEvent.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeEvent.php new file mode 100644 index 0000000..91aa5ce --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeEvent.php @@ -0,0 +1,51 @@ +icon = Util::getImage('b_events'); + $this->links = [ + 'text' => 'db_events.php?server=' . $GLOBALS['server'] + . '&db=%2$s&item_name=%1$s&edit_item=1', + 'icon' => 'db_events.php?server=' . $GLOBALS['server'] + . '&db=%2$s&item_name=%1$s&export_item=1', + ]; + $this->classes = 'event'; + } + + /** + * Returns the type of the item represented by the node. + * + * @return string type of the item + */ + protected function getItemType() + { + return 'event'; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeEventContainer.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeEventContainer.php new file mode 100644 index 0000000..86c2937 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeEventContainer.php @@ -0,0 +1,52 @@ +icon = Util::getImage('b_events', ''); + $this->links = [ + 'text' => 'db_events.php?server=' . $GLOBALS['server'] + . '&db=%1$s', + 'icon' => 'db_events.php?server=' . $GLOBALS['server'] + . '&db=%1$s', + ]; + $this->realName = 'events'; + + $new = NodeFactory::getInstance( + 'Node', + _pgettext('Create new event', 'New') + ); + $new->isNew = true; + $new->icon = Util::getImage('b_event_add', ''); + $new->links = [ + 'text' => 'db_events.php?server=' . $GLOBALS['server'] + . '&db=%2$s&add_item=1', + 'icon' => 'db_events.php?server=' . $GLOBALS['server'] + . '&db=%2$s&add_item=1', + ]; + $new->classes = 'new_event italics'; + $this->addChild($new); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeFunction.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeFunction.php new file mode 100644 index 0000000..ffd2afe --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeFunction.php @@ -0,0 +1,53 @@ +icon = Util::getImage('b_routines', __('Function')); + $this->links = [ + 'text' => 'db_routines.php?server=' . $GLOBALS['server'] + . '&db=%2$s&item_name=%1$s&item_type=FUNCTION' + . '&edit_item=1', + 'icon' => 'db_routines.php?server=' . $GLOBALS['server'] + . '&db=%2$s&item_name=%1$s&item_type=FUNCTION' + . '&execute_dialog=1', + ]; + $this->classes = 'function'; + } + + /** + * Returns the type of the item represented by the node. + * + * @return string type of the item + */ + protected function getItemType() + { + return 'function'; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeFunctionContainer.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeFunctionContainer.php new file mode 100644 index 0000000..52715a3 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeFunctionContainer.php @@ -0,0 +1,53 @@ +icon = Util::getImage('b_routines', __('Functions')); + $this->links = [ + 'text' => 'db_routines.php?server=' . $GLOBALS['server'] + . '&db=%1$s&type=FUNCTION', + 'icon' => 'db_routines.php?server=' . $GLOBALS['server'] + . '&db=%1$s&type=FUNCTION', + ]; + $this->realName = 'functions'; + + $newLabel = _pgettext('Create new function', 'New'); + $new = NodeFactory::getInstance( + 'Node', + $newLabel + ); + $new->isNew = true; + $new->icon = Util::getImage('b_routine_add', $newLabel); + $new->links = [ + 'text' => 'db_routines.php?server=' . $GLOBALS['server'] + . '&db=%2$s&add_item=1&item_type=FUNCTION', + 'icon' => 'db_routines.php?server=' . $GLOBALS['server'] + . '&db=%2$s&add_item=1&item_type=FUNCTION', + ]; + $new->classes = 'new_function italics'; + $this->addChild($new); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeIndex.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeIndex.php new file mode 100644 index 0000000..c464114 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeIndex.php @@ -0,0 +1,41 @@ +icon = Util::getImage('b_index', __('Index')); + $this->links = [ + 'text' => 'tbl_indexes.php?server=' . $GLOBALS['server'] + . '&db=%3$s&table=%2$s&index=%1$s', + 'icon' => 'tbl_indexes.php?server=' . $GLOBALS['server'] + . '&db=%3$s&table=%2$s&index=%1$s', + ]; + $this->classes = 'index'; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeIndexContainer.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeIndexContainer.php new file mode 100644 index 0000000..9be90d3 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeIndexContainer.php @@ -0,0 +1,55 @@ +icon = Util::getImage('b_index', __('Indexes')); + $this->links = [ + 'text' => 'tbl_structure.php?server=' . $GLOBALS['server'] + . '&db=%2$s&table=%1$s', + 'icon' => 'tbl_structure.php?server=' . $GLOBALS['server'] + . '&db=%2$s&table=%1$s', + ]; + $this->realName = 'indexes'; + + $newLabel = _pgettext('Create new index', 'New'); + $new = NodeFactory::getInstance( + 'Node', + $newLabel + ); + $new->isNew = true; + $new->icon = Util::getImage('b_index_add', $newLabel); + $new->links = [ + 'text' => 'tbl_indexes.php?server=' . $GLOBALS['server'] + . '&create_index=1&added_fields=2' + . '&db=%3$s&table=%2$s', + 'icon' => 'tbl_indexes.php?server=' . $GLOBALS['server'] + . '&create_index=1&added_fields=2' + . '&db=%3$s&table=%2$s', + ]; + $new->classes = 'new_index italics'; + $this->addChild($new); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeProcedure.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeProcedure.php new file mode 100644 index 0000000..fbced93 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeProcedure.php @@ -0,0 +1,53 @@ +icon = Util::getImage('b_routines', __('Procedure')); + $this->links = [ + 'text' => 'db_routines.php?server=' . $GLOBALS['server'] + . '&db=%2$s&item_name=%1$s&item_type=PROCEDURE' + . '&edit_item=1', + 'icon' => 'db_routines.php?server=' . $GLOBALS['server'] + . '&db=%2$s&item_name=%1$s&item_type=PROCEDURE' + . '&execute_dialog=1', + ]; + $this->classes = 'procedure'; + } + + /** + * Returns the type of the item represented by the node. + * + * @return string type of the item + */ + protected function getItemType() + { + return 'procedure'; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeProcedureContainer.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeProcedureContainer.php new file mode 100644 index 0000000..1978ede --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeProcedureContainer.php @@ -0,0 +1,53 @@ +icon = Util::getImage('b_routines', __('Procedures')); + $this->links = [ + 'text' => 'db_routines.php?server=' . $GLOBALS['server'] + . '&db=%1$s&type=PROCEDURE', + 'icon' => 'db_routines.php?server=' . $GLOBALS['server'] + . '&db=%1$s&type=PROCEDURE', + ]; + $this->realName = 'procedures'; + + $newLabel = _pgettext('Create new procedure', 'New'); + $new = NodeFactory::getInstance( + 'Node', + $newLabel + ); + $new->isNew = true; + $new->icon = Util::getImage('b_routine_add', $newLabel); + $new->links = [ + 'text' => 'db_routines.php?server=' . $GLOBALS['server'] + . '&db=%2$s&add_item=1', + 'icon' => 'db_routines.php?server=' . $GLOBALS['server'] + . '&db=%2$s&add_item=1', + ]; + $new->classes = 'new_procedure italics'; + $this->addChild($new); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeTable.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeTable.php new file mode 100644 index 0000000..89965eb --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeTable.php @@ -0,0 +1,310 @@ +icon = []; + $this->addIcon( + Util::getScriptNameForOption( + $GLOBALS['cfg']['NavigationTreeDefaultTabTable'], + 'table' + ) + ); + $this->addIcon( + Util::getScriptNameForOption( + $GLOBALS['cfg']['NavigationTreeDefaultTabTable2'], + 'table' + ) + ); + $title = Util::getTitleForTarget( + $GLOBALS['cfg']['DefaultTabTable'] + ); + $this->title = $title; + + $scriptName = Util::getScriptNameForOption( + $GLOBALS['cfg']['DefaultTabTable'], + 'table' + ); + $this->links = [ + 'text' => $scriptName + . '?server=' . $GLOBALS['server'] + . '&db=%2$s&table=%1$s' + . '&pos=0', + 'icon' => [ + Util::getScriptNameForOption( + $GLOBALS['cfg']['NavigationTreeDefaultTabTable'], + 'table' + ) + . '?server=' . $GLOBALS['server'] + . '&db=%2$s&table=%1$s', + Util::getScriptNameForOption( + $GLOBALS['cfg']['NavigationTreeDefaultTabTable2'], + 'table' + ) + . '?server=' . $GLOBALS['server'] + . '&db=%2$s&table=%1$s', + ], + 'title' => $this->title, + ]; + $this->classes = 'table'; + } + + /** + * Returns the number of children of type $type present inside this container + * This method is overridden by the PhpMyAdmin\Navigation\Nodes\NodeDatabase + * and PhpMyAdmin\Navigation\Nodes\NodeTable classes + * + * @param string $type The type of item we are looking for + * ('columns' or 'indexes') + * @param string $searchClause A string used to filter the results of the query + * + * @return int + */ + public function getPresence($type = '', $searchClause = '') + { + $retval = 0; + $db = $this->realParent()->realName; + $table = $this->realName; + switch ($type) { + case 'columns': + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $db = $GLOBALS['dbi']->escapeString($db); + $table = $GLOBALS['dbi']->escapeString($table); + $query = "SELECT COUNT(*) "; + $query .= "FROM `INFORMATION_SCHEMA`.`COLUMNS` "; + $query .= "WHERE `TABLE_NAME`='$table' "; + $query .= "AND `TABLE_SCHEMA`='$db'"; + $retval = (int) $GLOBALS['dbi']->fetchValue($query); + } else { + $db = Util::backquote($db); + $table = Util::backquote($table); + $query = "SHOW COLUMNS FROM $table FROM $db"; + $retval = (int) $GLOBALS['dbi']->numRows( + $GLOBALS['dbi']->tryQuery($query) + ); + } + break; + case 'indexes': + $db = Util::backquote($db); + $table = Util::backquote($table); + $query = "SHOW INDEXES FROM $table FROM $db"; + $retval = (int) $GLOBALS['dbi']->numRows( + $GLOBALS['dbi']->tryQuery($query) + ); + break; + case 'triggers': + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $db = $GLOBALS['dbi']->escapeString($db); + $table = $GLOBALS['dbi']->escapeString($table); + $query = "SELECT COUNT(*) "; + $query .= "FROM `INFORMATION_SCHEMA`.`TRIGGERS` "; + $query .= "WHERE `EVENT_OBJECT_SCHEMA` " + . Util::getCollateForIS() . "='$db' "; + $query .= "AND `EVENT_OBJECT_TABLE` " + . Util::getCollateForIS() . "='$table'"; + $retval = (int) $GLOBALS['dbi']->fetchValue($query); + } else { + $db = Util::backquote($db); + $table = $GLOBALS['dbi']->escapeString($table); + $query = "SHOW TRIGGERS FROM $db WHERE `Table` = '$table'"; + $retval = (int) $GLOBALS['dbi']->numRows( + $GLOBALS['dbi']->tryQuery($query) + ); + } + break; + default: + break; + } + + return $retval; + } + + /** + * Returns the names of children of type $type present inside this container + * This method is overridden by the PhpMyAdmin\Navigation\Nodes\NodeDatabase + * and PhpMyAdmin\Navigation\Nodes\NodeTable classes + * + * @param string $type The type of item we are looking for + * ('tables', 'views', etc) + * @param int $pos The offset of the list within the results + * @param string $searchClause A string used to filter the results of the query + * + * @return array + */ + public function getData($type, $pos, $searchClause = '') + { + $maxItems = $GLOBALS['cfg']['MaxNavigationItems']; + $retval = []; + $db = $this->realParent()->realName; + $table = $this->realName; + switch ($type) { + case 'columns': + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $db = $GLOBALS['dbi']->escapeString($db); + $table = $GLOBALS['dbi']->escapeString($table); + $query = "SELECT `COLUMN_NAME` AS `name` "; + $query .= ",`COLUMN_KEY` AS `key` "; + $query .= ",`DATA_TYPE` AS `type` "; + $query .= ",`COLUMN_DEFAULT` AS `default` "; + $query .= ",IF (`IS_NULLABLE` = 'NO', '', 'nullable') AS `nullable` "; + $query .= "FROM `INFORMATION_SCHEMA`.`COLUMNS` "; + $query .= "WHERE `TABLE_NAME`='$table' "; + $query .= "AND `TABLE_SCHEMA`='$db' "; + $query .= "ORDER BY `COLUMN_NAME` ASC "; + $query .= "LIMIT " . intval($pos) . ", $maxItems"; + $retval = $GLOBALS['dbi']->fetchResult($query); + break; + } + + $db = Util::backquote($db); + $table = Util::backquote($table); + $query = "SHOW COLUMNS FROM $table FROM $db"; + $handle = $GLOBALS['dbi']->tryQuery($query); + if ($handle === false) { + break; + } + + $count = 0; + if ($GLOBALS['dbi']->dataSeek($handle, $pos)) { + while ($arr = $GLOBALS['dbi']->fetchArray($handle)) { + if ($count < $maxItems) { + $retval[] = $arr['Field']; + $count++; + } else { + break; + } + } + } + break; + case 'indexes': + $db = Util::backquote($db); + $table = Util::backquote($table); + $query = "SHOW INDEXES FROM $table FROM $db"; + $handle = $GLOBALS['dbi']->tryQuery($query); + if ($handle === false) { + break; + } + + $count = 0; + while ($arr = $GLOBALS['dbi']->fetchArray($handle)) { + if (in_array($arr['Key_name'], $retval)) { + continue; + } + if ($pos <= 0 && $count < $maxItems) { + $retval[] = $arr['Key_name']; + $count++; + } + $pos--; + } + break; + case 'triggers': + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $db = $GLOBALS['dbi']->escapeString($db); + $table = $GLOBALS['dbi']->escapeString($table); + $query = "SELECT `TRIGGER_NAME` AS `name` "; + $query .= "FROM `INFORMATION_SCHEMA`.`TRIGGERS` "; + $query .= "WHERE `EVENT_OBJECT_SCHEMA` " + . Util::getCollateForIS() . "='$db' "; + $query .= "AND `EVENT_OBJECT_TABLE` " + . Util::getCollateForIS() . "='$table' "; + $query .= "ORDER BY `TRIGGER_NAME` ASC "; + $query .= "LIMIT " . intval($pos) . ", $maxItems"; + $retval = $GLOBALS['dbi']->fetchResult($query); + break; + } + + $db = Util::backquote($db); + $table = $GLOBALS['dbi']->escapeString($table); + $query = "SHOW TRIGGERS FROM $db WHERE `Table` = '$table'"; + $handle = $GLOBALS['dbi']->tryQuery($query); + if ($handle === false) { + break; + } + + $count = 0; + if ($GLOBALS['dbi']->dataSeek($handle, $pos)) { + while ($arr = $GLOBALS['dbi']->fetchArray($handle)) { + if ($count < $maxItems) { + $retval[] = $arr['Trigger']; + $count++; + } else { + break; + } + } + } + break; + default: + break; + } + + return $retval; + } + + /** + * Returns the type of the item represented by the node. + * + * @return string type of the item + */ + protected function getItemType() + { + return 'table'; + } + + /** + * Add an icon to navigation tree + * + * @param string $page Page name to redirect + * + * @return void + */ + private function addIcon($page) + { + if (empty($page)) { + return; + } + + switch ($page) { + case 'tbl_structure.php': + $this->icon[] = Util::getImage('b_props', __('Structure')); + break; + case 'tbl_select.php': + $this->icon[] = Util::getImage('b_search', __('Search')); + break; + case 'tbl_change.php': + $this->icon[] = Util::getImage('b_insrow', __('Insert')); + break; + case 'tbl_sql.php': + $this->icon[] = Util::getImage('b_sql', __('SQL')); + break; + case 'sql.php': + $this->icon[] = Util::getImage('b_browse', __('Browse')); + break; + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeTableContainer.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeTableContainer.php new file mode 100644 index 0000000..ec93203 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeTableContainer.php @@ -0,0 +1,54 @@ +icon = Util::getImage('b_browse', __('Tables')); + $this->links = [ + 'text' => 'db_structure.php?server=' . $GLOBALS['server'] + . '&db=%1$s&tbl_type=table', + 'icon' => 'db_structure.php?server=' . $GLOBALS['server'] + . '&db=%1$s&tbl_type=table', + ]; + $this->realName = 'tables'; + $this->classes = 'tableContainer subContainer'; + + $newLabel = _pgettext('Create new table', 'New'); + $new = NodeFactory::getInstance( + 'Node', + $newLabel + ); + $new->isNew = true; + $new->icon = Util::getImage('b_table_add', $newLabel); + $new->links = [ + 'text' => 'tbl_create.php?server=' . $GLOBALS['server'] + . '&db=%2$s', + 'icon' => 'tbl_create.php?server=' . $GLOBALS['server'] + . '&db=%2$s', + ]; + $new->classes = 'new_table italics'; + $this->addChild($new); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeTrigger.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeTrigger.php new file mode 100644 index 0000000..8ec4612 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeTrigger.php @@ -0,0 +1,41 @@ +icon = Util::getImage('b_triggers'); + $this->links = [ + 'text' => 'db_triggers.php?server=' . $GLOBALS['server'] + . '&db=%3$s&item_name=%1$s&edit_item=1', + 'icon' => 'db_triggers.php?server=' . $GLOBALS['server'] + . '&db=%3$s&item_name=%1$s&export_item=1', + ]; + $this->classes = 'trigger'; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeTriggerContainer.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeTriggerContainer.php new file mode 100644 index 0000000..a5aa3b7 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeTriggerContainer.php @@ -0,0 +1,52 @@ +icon = Util::getImage('b_triggers'); + $this->links = [ + 'text' => 'db_triggers.php?server=' . $GLOBALS['server'] + . '&db=%2$s&table=%1$s', + 'icon' => 'db_triggers.php?server=' . $GLOBALS['server'] + . '&db=%2$s&table=%1$s', + ]; + $this->realName = 'triggers'; + + $new = NodeFactory::getInstance( + 'Node', + _pgettext('Create new trigger', 'New') + ); + $new->isNew = true; + $new->icon = Util::getImage('b_trigger_add', ''); + $new->links = [ + 'text' => 'db_triggers.php?server=' . $GLOBALS['server'] + . '&db=%3$s&add_item=1', + 'icon' => 'db_triggers.php?server=' . $GLOBALS['server'] + . '&db=%3$s&add_item=1', + ]; + $new->classes = 'new_trigger italics'; + $this->addChild($new); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeView.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeView.php new file mode 100644 index 0000000..1ded149 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeView.php @@ -0,0 +1,51 @@ +icon = Util::getImage('b_props', __('View')); + $this->links = [ + 'text' => 'sql.php?server=' . $GLOBALS['server'] + . '&db=%2$s&table=%1$s&pos=0', + 'icon' => 'tbl_structure.php?server=' . $GLOBALS['server'] + . '&db=%2$s&table=%1$s', + ]; + $this->classes = 'view'; + } + + /** + * Returns the type of the item represented by the node. + * + * @return string type of the item + */ + protected function getItemType() + { + return 'view'; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeViewContainer.php b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeViewContainer.php new file mode 100644 index 0000000..f202904 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Nodes/NodeViewContainer.php @@ -0,0 +1,54 @@ +icon = Util::getImage('b_views', __('Views')); + $this->links = [ + 'text' => 'db_structure.php?server=' . $GLOBALS['server'] + . '&db=%1$s&tbl_type=view', + 'icon' => 'db_structure.php?server=' . $GLOBALS['server'] + . '&db=%1$s&tbl_type=view', + ]; + $this->classes = 'viewContainer subContainer'; + $this->realName = 'views'; + + $newLabel = _pgettext('Create new view', 'New'); + $new = NodeFactory::getInstance( + 'Node', + $newLabel + ); + $new->isNew = true; + $new->icon = Util::getImage('b_view_add', $newLabel); + $new->links = [ + 'text' => 'view_create.php?server=' . $GLOBALS['server'] + . '&db=%2$s', + 'icon' => 'view_create.php?server=' . $GLOBALS['server'] + . '&db=%2$s', + ]; + $new->classes = 'new_view italics'; + $this->addChild($new); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Normalization.php b/srcs/phpmyadmin/libraries/classes/Normalization.php new file mode 100644 index 0000000..6c3d73b --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Normalization.php @@ -0,0 +1,1105 @@ +dbi = $dbi; + $this->relation = $relation; + $this->transformations = $transformations; + $this->template = $template; + } + + /** + * build the html for columns of $colTypeCategory category + * in form of given $listType in a table + * + * @param string $db current database + * @param string $table current table + * @param string $colTypeCategory supported all|Numeric|String|Spatial + * |Date and time using the _pgettext() format + * @param string $listType type of list to build, supported dropdown|checkbox + * + * @return string HTML for list of columns in form of given list types + */ + public function getHtmlForColumnsList( + $db, + $table, + $colTypeCategory = 'all', + $listType = 'dropdown' + ) { + $columnTypeList = []; + if ($colTypeCategory != 'all') { + $types = $this->dbi->types->getColumns(); + $columnTypeList = $types[$colTypeCategory]; + } + $this->dbi->selectDb($db); + $columns = $this->dbi->getColumns( + $db, + $table, + null, + true + ); + $type = ""; + $selectColHtml = ""; + foreach ($columns as $column => $def) { + if (isset($def['Type'])) { + $extractedColumnSpec = Util::extractColumnSpec($def['Type']); + $type = $extractedColumnSpec['type']; + } + if (empty($columnTypeList) + || in_array(mb_strtoupper($type), $columnTypeList) + ) { + if ($listType == 'checkbox') { + $selectColHtml .= '' + . htmlspecialchars($column) . ' [ ' + . htmlspecialchars($def['Type']) . ' ]
    '; + } else { + $selectColHtml .= ''; + } + } + } + return $selectColHtml; + } + + /** + * get the html of the form to add the new column to given table + * + * @param integer $numFields number of columns to add + * @param string $db current database + * @param string $table current table + * @param array $columnMeta array containing default values for the fields + * + * @return string HTML + */ + public function getHtmlForCreateNewColumn( + $numFields, + $db, + $table, + array $columnMeta = [] + ) { + $cfgRelation = $this->relation->getRelationsParam(); + $contentCells = []; + $availableMime = []; + $mimeMap = []; + if ($cfgRelation['mimework'] && $GLOBALS['cfg']['BrowseMIME']) { + $mimeMap = $this->transformations->getMime($db, $table); + $availableMimeTypes = $this->transformations->getAvailableMimeTypes(); + if ($availableMimeTypes !== null) { + $availableMime = $availableMimeTypes; + } + } + $commentsMap = $this->relation->getComments($db, $table); + for ($columnNumber = 0; $columnNumber < $numFields; $columnNumber++) { + $contentCells[$columnNumber] = [ + 'column_number' => $columnNumber, + 'column_meta' => $columnMeta, + 'type_upper' => '', + 'length_values_input_size' => 8, + 'length' => '', + 'extracted_columnspec' => [], + 'submit_attribute' => null, + 'comments_map' => $commentsMap, + 'fields_meta' => null, + 'is_backup' => true, + 'move_columns' => [], + 'cfg_relation' => $cfgRelation, + 'available_mime' => $availableMime, + 'mime_map' => $mimeMap, + ]; + } + + $charsets = Charsets::getCharsets($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']); + $collations = Charsets::getCollations($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']); + $charsetsList = []; + /** @var Charset $charset */ + foreach ($charsets as $charset) { + $collationsList = []; + /** @var Collation $collation */ + foreach ($collations[$charset->getName()] as $collation) { + $collationsList[] = [ + 'name' => $collation->getName(), + 'description' => $collation->getDescription(), + ]; + } + $charsetsList[] = [ + 'name' => $charset->getName(), + 'description' => $charset->getDescription(), + 'collations' => $collationsList, + ]; + } + + return $this->template->render('columns_definitions/table_fields_definitions', [ + 'is_backup' => true, + 'fields_meta' => null, + 'mimework' => $cfgRelation['mimework'], + 'content_cells' => $contentCells, + 'change_column' => $_POST['change_column'], + 'is_virtual_columns_supported' => Util::isVirtualColumnsSupported(), + 'browse_mime' => $GLOBALS['cfg']['BrowseMIME'], + 'server_type' => Util::getServerType(), + 'max_rows' => intval($GLOBALS['cfg']['MaxRows']), + 'char_editing' => $GLOBALS['cfg']['CharEditing'], + 'attribute_types' => $this->dbi->types->getAttributes(), + 'privs_available' => $GLOBALS['col_priv'] && $GLOBALS['is_reload_priv'], + 'max_length' => $this->dbi->getVersion() >= 50503 ? 1024 : 255, + 'charsets' => $charsetsList, + ]); + } + + /** + * build the html for step 1.1 of normalization + * + * @param string $db current database + * @param string $table current table + * @param string $normalizedTo up to which step normalization will go, + * possible values 1nf|2nf|3nf + * + * @return string HTML for step 1.1 + */ + public function getHtmlFor1NFStep1($db, $table, $normalizedTo) + { + $step = 1; + $stepTxt = __('Make all columns atomic'); + $html = "

    " + . __('First step of normalization (1NF)') . "

    "; + $html .= "
    " . + "
    " . + "" . __('Step 1.') . $step . " " . $stepTxt . "" . + "

    " . __( + 'Do you have any column which can be split into more than' + . ' one column? ' + . 'For example: address can be split into street, city, country and zip.' + ) + . "
    ( " + . __( + 'Show me the central list of columns that are not already in this table' + ) . " )

    " + . "

    " . __( + 'Select a column which can be split into more ' + . 'than one (on select of \'no such column\', it\'ll move to next step).' + ) + . "

    " + . "
    " + . "" + . "" . __('split into ') + . "" + . "
    " + . "
    " + . "
    " + . "
    " + . "
    "; + return $html; + } + + /** + * build the html contents of various html elements in step 1.2 + * + * @param string $db current database + * @param string $table current table + * + * @return string[] HTML contents for step 1.2 + */ + public function getHtmlContentsFor1NFStep2($db, $table) + { + $step = 2; + $stepTxt = __('Have a primary key'); + $primary = Index::getPrimary($table, $db); + $hasPrimaryKey = "0"; + $legendText = __('Step 1.') . $step . " " . $stepTxt; + $extra = ''; + if ($primary) { + $headText = __("Primary key already exists."); + $subText = __("Taking you to next step…"); + $hasPrimaryKey = "1"; + } else { + $headText = __( + "There is no primary key; please add one.
    " + . "Hint: A primary key is a column " + . "(or combination of columns) that uniquely identify all rows." + ); + $subText = '' + . Util::getIcon( + 'b_index_add', + __( + 'Add a primary key on existing column(s)' + ) + ) + . ''; + $extra = __( + "If it's not possible to make existing " + . "column combinations as primary key" + ) . "
    " + . '' + . __('+ Add a new primary key column') . ''; + } + return [ + 'legendText' => $legendText, + 'headText' => $headText, + 'subText' => $subText, + 'hasPrimaryKey' => $hasPrimaryKey, + 'extra' => $extra, + ]; + } + + /** + * build the html contents of various html elements in step 1.4 + * + * @param string $db current database + * @param string $table current table + * + * @return string[] HTML contents for step 1.4 + */ + public function getHtmlContentsFor1NFStep4($db, $table) + { + $step = 4; + $stepTxt = __('Remove redundant columns'); + $legendText = __('Step 1.') . $step . " " . $stepTxt; + $headText = __( + "Do you have a group of columns which on combining gives an existing" + . " column? For example, if you have first_name, last_name and" + . " full_name then combining first_name and last_name gives full_name" + . " which is redundant." + ); + $subText = __( + "Check the columns which are redundant and click on remove. " + . "If no redundant column, click on 'No redundant column'" + ); + $extra = $this->getHtmlForColumnsList($db, $table, 'all', "checkbox") . "
    " + . '' + . ''; + return [ + 'legendText' => $legendText, + 'headText' => $headText, + 'subText' => $subText, + 'extra' => $extra, + ]; + } + + /** + * build the html contents of various html elements in step 1.3 + * + * @param string $db current database + * @param string $table current table + * + * @return string[] HTML contents for step 1.3 + */ + public function getHtmlContentsFor1NFStep3($db, $table) + { + $step = 3; + $stepTxt = __('Move repeating groups'); + $legendText = __('Step 1.') . $step . " " . $stepTxt; + $headText = __( + "Do you have a group of two or more columns that are closely " + . "related and are all repeating the same attribute? For example, " + . "a table that holds data on books might have columns such as book_id, " + . "author1, author2, author3 and so on which form a " + . "repeating group. In this case a new table (book_id, author) should " + . "be created." + ); + $subText = __( + "Check the columns which form a repeating group. " + . "If no such group, click on 'No repeating group'" + ); + $extra = $this->getHtmlForColumnsList($db, $table, 'all', "checkbox") . "
    " + . '' + . ''; + $primary = Index::getPrimary($table, $db); + $primarycols = $primary->getColumns(); + $pk = []; + foreach ($primarycols as $col) { + $pk[] = $col->getName(); + } + return [ + 'legendText' => $legendText, + 'headText' => $headText, + 'subText' => $subText, + 'extra' => $extra, + 'primary_key' => json_encode($pk), + ]; + } + + /** + * build html contents for 2NF step 2.1 + * + * @param string $db current database + * @param string $table current table + * + * @return string[] HTML contents for 2NF step 2.1 + */ + public function getHtmlFor2NFstep1($db, $table) + { + $legendText = __('Step 2.') . "1 " . __('Find partial dependencies'); + $primary = Index::getPrimary($table, $db); + $primarycols = $primary->getColumns(); + $pk = []; + $subText = ''; + $selectPkForm = ""; + $extra = ""; + foreach ($primarycols as $col) { + $pk[] = $col->getName(); + $selectPkForm .= '' + . htmlspecialchars($col->getName()); + } + $key = implode(', ', $pk); + if (count($primarycols) > 1) { + $this->dbi->selectDb($db); + $columns = (array) $this->dbi->getColumnNames( + $db, + $table + ); + if (count($pk) == count($columns)) { + $headText = sprintf( + __( + 'No partial dependencies possible as ' + . 'no non-primary column exists since primary key ( %1$s ) ' + . 'is composed of all the columns in the table.' + ), + htmlspecialchars($key) + ) . '
    '; + $extra = '

    ' . __('Table is already in second normal form.') + . '

    '; + } else { + $headText = sprintf( + __( + 'The primary key ( %1$s ) consists of more than one column ' + . 'so we need to find the partial dependencies.' + ), + htmlspecialchars($key) + ) . '
    ' . __( + 'Please answer the following question(s) ' + . 'carefully to obtain a correct normalization.' + ) + . '
    ' . __( + '+ Show me the possible partial dependencies ' + . 'based on data in the table' + ) . ''; + $subText = __( + 'For each column below, ' + . 'please select the minimal set of columns among given set ' + . 'whose values combined together are sufficient' + . ' to determine the value of the column.' + ); + $cnt = 0; + foreach ($columns as $column) { + if (! in_array($column, $pk)) { + $cnt++; + $extra .= "" . sprintf( + __('\'%1$s\' depends on:'), + htmlspecialchars($column) + ) . "
    "; + $extra .= '
    ' + . $selectPkForm . '


    '; + } + } + } + } else { + $headText = sprintf( + __( + 'No partial dependencies possible as the primary key' + . ' ( %1$s ) has just one column.' + ), + htmlspecialchars($key) + ) . '
    '; + $extra = '

    ' . __('Table is already in second normal form.') . '

    '; + } + return [ + 'legendText' => $legendText, + 'headText' => $headText, + 'subText' => $subText, + 'extra' => $extra, + 'primary_key' => $key, + ]; + } + + /** + * build the html for showing the tables to have in order to put current table in 2NF + * + * @param array $partialDependencies array containing all the dependencies + * @param string $table current table + * + * @return string HTML + */ + public function getHtmlForNewTables2NF(array $partialDependencies, $table) + { + $html = '

    ' . sprintf( + __( + 'In order to put the ' + . 'original table \'%1$s\' into Second normal form we need ' + . 'to create the following tables:' + ), + htmlspecialchars($table) + ) . '

    '; + $tableName = $table; + $i = 1; + foreach ($partialDependencies as $key => $dependents) { + $html .= '

    ' + . '( ' . htmlspecialchars($key) . '' + . (count($dependents) > 0 ? ', ' : '') + . htmlspecialchars(implode(', ', $dependents)) . ' )'; + $i++; + $tableName = 'table' . $i; + } + return $html; + } + + /** + * create/alter the tables needed for 2NF + * + * @param array $partialDependencies array containing all the partial dependencies + * @param object $tablesName name of new tables + * @param string $table current table + * @param string $db current database + * + * @return array + */ + public function createNewTablesFor2NF(array $partialDependencies, $tablesName, $table, $db) + { + $dropCols = false; + $nonPKCols = []; + $queries = []; + $error = false; + $headText = '

    ' . sprintf( + __('The second step of normalization is complete for table \'%1$s\'.'), + htmlspecialchars($table) + ) . '

    '; + if (count((array) $partialDependencies) === 1) { + return [ + 'legendText' => __('End of step'), + 'headText' => $headText, + 'queryError' => $error, + ]; + } + $message = ''; + $this->dbi->selectDb($db); + foreach ($partialDependencies as $key => $dependents) { + if ($tablesName->$key != $table) { + $backquotedKey = implode(', ', Util::backquote(explode(', ', $key))); + $queries[] = 'CREATE TABLE ' . Util::backquote($tablesName->$key) + . ' SELECT DISTINCT ' . $backquotedKey + . (count($dependents) > 0 ? ', ' : '') + . implode(',', Util::backquote($dependents)) + . ' FROM ' . Util::backquote($table) . ';'; + $queries[] = 'ALTER TABLE ' . Util::backquote($tablesName->$key) + . ' ADD PRIMARY KEY(' . $backquotedKey . ');'; + $nonPKCols = array_merge($nonPKCols, $dependents); + } else { + $dropCols = true; + } + } + + if ($dropCols) { + $query = 'ALTER TABLE ' . Util::backquote($table); + foreach ($nonPKCols as $col) { + $query .= ' DROP ' . Util::backquote($col) . ','; + } + $query = trim($query, ', '); + $query .= ';'; + $queries[] = $query; + } else { + $queries[] = 'DROP TABLE ' . Util::backquote($table); + } + foreach ($queries as $query) { + if (! $this->dbi->tryQuery($query)) { + $message = Message::error(__('Error in processing!')); + $message->addMessage( + Message::rawError( + $this->dbi->getError() + ), + '

    ' + ); + $error = true; + break; + } + } + return [ + 'legendText' => __('End of step'), + 'headText' => $headText, + 'queryError' => $error, + 'extra' => $message, + ]; + } + + /** + * build the html for showing the new tables to have in order + * to put given tables in 3NF + * + * @param object $dependencies containing all the dependencies + * @param array $tables tables formed after 2NF and need to convert to 3NF + * @param string $db current database + * + * @return array containing html and the list of new tables + */ + public function getHtmlForNewTables3NF($dependencies, array $tables, $db) + { + $html = ""; + $i = 1; + $newTables = []; + foreach ($tables as $table => $arrDependson) { + if (count(array_unique($arrDependson)) === 1) { + continue; + } + $primary = Index::getPrimary($table, $db); + $primarycols = $primary->getColumns(); + $pk = []; + foreach ($primarycols as $col) { + $pk[] = $col->getName(); + } + $html .= '

    ' . sprintf( + __( + 'In order to put the ' + . 'original table \'%1$s\' into Third normal form we need ' + . 'to create the following tables:' + ), + htmlspecialchars($table) + ) . '

    '; + $tableName = $table; + $columnList = []; + foreach ($arrDependson as $key) { + $dependents = $dependencies->$key; + if ($key == $table) { + $key = implode(', ', $pk); + } + $tmpTableCols = array_merge(explode(', ', $key), $dependents); + sort($tmpTableCols); + if (! in_array($tmpTableCols, $columnList)) { + $columnList[] = $tmpTableCols; + $html .= '

    ' + . '( ' . htmlspecialchars($key) . '' + . (count($dependents) > 0 ? ', ' : '') + . htmlspecialchars(implode(', ', $dependents)) . ' )'; + $newTables[$table][$tableName] = [ + "pk" => $key, + "nonpk" => implode(', ', $dependents), + ]; + $i++; + $tableName = 'table' . $i; + } + } + } + return [ + 'html' => $html, + 'newTables' => $newTables, + 'success' => true, + ]; + } + + /** + * create new tables or alter existing to get 3NF + * + * @param array $newTables list of new tables to be created + * @param string $db current database + * + * @return array + */ + public function createNewTablesFor3NF(array $newTables, $db) + { + $queries = []; + $dropCols = false; + $error = false; + $headText = '

    ' . + __('The third step of normalization is complete.') + . '

    '; + if (count((array) $newTables) === 0) { + return [ + 'legendText' => __('End of step'), + 'headText' => $headText, + 'queryError' => $error, + ]; + } + $message = ''; + $this->dbi->selectDb($db); + foreach ($newTables as $originalTable => $tablesList) { + foreach ($tablesList as $table => $cols) { + if ($table != $originalTable) { + $quotedPk = implode( + ', ', + Util::backquote(explode(', ', $cols->pk)) + ); + $quotedNonpk = implode( + ', ', + Util::backquote(explode(', ', $cols->nonpk)) + ); + $queries[] = 'CREATE TABLE ' . Util::backquote($table) + . ' SELECT DISTINCT ' . $quotedPk + . ', ' . $quotedNonpk + . ' FROM ' . Util::backquote($originalTable) . ';'; + $queries[] = 'ALTER TABLE ' . Util::backquote($table) + . ' ADD PRIMARY KEY(' . $quotedPk . ');'; + } else { + $dropCols = $cols; + } + } + if ($dropCols) { + $columns = (array) $this->dbi->getColumnNames( + $db, + $originalTable + ); + $colPresent = array_merge( + explode(', ', $dropCols->pk), + explode(', ', $dropCols->nonpk) + ); + $query = 'ALTER TABLE ' . Util::backquote($originalTable); + foreach ($columns as $col) { + if (! in_array($col, $colPresent)) { + $query .= ' DROP ' . Util::backquote($col) . ','; + } + } + $query = trim($query, ', '); + $query .= ';'; + $queries[] = $query; + } else { + $queries[] = 'DROP TABLE ' . Util::backquote($originalTable); + } + $dropCols = false; + } + foreach ($queries as $query) { + if (! $this->dbi->tryQuery($query)) { + $message = Message::error(__('Error in processing!')); + $message->addMessage( + Message::rawError( + $this->dbi->getError() + ), + '

    ' + ); + $error = true; + break; + } + } + return [ + 'legendText' => __('End of step'), + 'headText' => $headText, + 'queryError' => $error, + 'extra' => $message, + ]; + } + + /** + * move the repeating group of columns to a new table + * + * @param string $repeatingColumns comma separated list of repeating group columns + * @param string $primaryColumns comma separated list of column in primary key + * of $table + * @param string $newTable name of the new table to be created + * @param string $newColumn name of the new column in the new table + * @param string $table current table + * @param string $db current database + * + * @return array + */ + public function moveRepeatingGroup( + $repeatingColumns, + $primaryColumns, + $newTable, + $newColumn, + $table, + $db + ) { + $repeatingColumnsArr = (array) Util::backquote( + explode(', ', $repeatingColumns) + ); + $primaryColumns = implode( + ',', + Util::backquote(explode(',', $primaryColumns)) + ); + $query1 = 'CREATE TABLE ' . Util::backquote($newTable); + $query2 = 'ALTER TABLE ' . Util::backquote($table); + $message = Message::success( + sprintf( + __('Selected repeating group has been moved to the table \'%s\''), + htmlspecialchars($table) + ) + ); + $first = true; + $error = false; + foreach ($repeatingColumnsArr as $repeatingColumn) { + if (! $first) { + $query1 .= ' UNION '; + } + $first = false; + $query1 .= ' SELECT ' . $primaryColumns . ',' . $repeatingColumn + . ' as ' . Util::backquote($newColumn) + . ' FROM ' . Util::backquote($table); + $query2 .= ' DROP ' . $repeatingColumn . ','; + } + $query2 = trim($query2, ','); + $queries = [ + $query1, + $query2, + ]; + $this->dbi->selectDb($db); + foreach ($queries as $query) { + if (! $this->dbi->tryQuery($query)) { + $message = Message::error(__('Error in processing!')); + $message->addMessage( + Message::rawError( + $this->dbi->getError() + ), + '

    ' + ); + $error = true; + break; + } + } + return [ + 'queryError' => $error, + 'message' => $message, + ]; + } + + /** + * build html for 3NF step 1 to find the transitive dependencies + * + * @param string $db current database + * @param array $tables tables formed after 2NF and need to process for 3NF + * + * @return string[] + */ + public function getHtmlFor3NFstep1($db, array $tables) + { + $legendText = __('Step 3.') . "1 " . __('Find transitive dependencies'); + $extra = ""; + $headText = __( + 'Please answer the following question(s) ' + . 'carefully to obtain a correct normalization.' + ); + $subText = __( + 'For each column below, ' + . 'please select the minimal set of columns among given set ' + . 'whose values combined together are sufficient' + . ' to determine the value of the column.
    ' + . 'Note: A column may have no transitive dependency, ' + . 'in that case you don\'t have to select any.' + ); + $cnt = 0; + foreach ($tables as $table) { + $primary = Index::getPrimary($table, $db); + $primarycols = $primary->getColumns(); + $selectTdForm = ""; + $pk = []; + foreach ($primarycols as $col) { + $pk[] = $col->getName(); + } + $this->dbi->selectDb($db); + $columns = (array) $this->dbi->getColumnNames( + $db, + $table + ); + if (count($columns) - count($pk) <= 1) { + continue; + } + foreach ($columns as $column) { + if (! in_array($column, $pk)) { + $selectTdForm .= '' + . '' . htmlspecialchars($column) . ''; + } + } + foreach ($columns as $column) { + if (! in_array($column, $pk)) { + $cnt++; + $extra .= "" . sprintf( + __('\'%1$s\' depends on:'), + htmlspecialchars($column) + ) + . "
    "; + $extra .= '
    ' + . $selectTdForm + . '


    '; + } + } + } + if ($extra == "") { + $headText = __( + "No Transitive dependencies possible as the table " + . "doesn't have any non primary key columns" + ); + $subText = ""; + $extra = "

    " . __("Table is already in Third normal form!") . "

    "; + } + return [ + 'legendText' => $legendText, + 'headText' => $headText, + 'subText' => $subText, + 'extra' => $extra, + ]; + } + + /** + * get html for options to normalize table + * + * @return string HTML + */ + public function getHtmlForNormalizeTable() + { + $htmlOutput = '
    ' + . Url::getHiddenInputs($GLOBALS['db'], $GLOBALS['table']) + . ''; + $htmlOutput .= '
    '; + $htmlOutput .= '' + . __('Improve table structure (Normalization):') . ''; + $htmlOutput .= '

    ' . __('Select up to what step you want to normalize') + . '

    '; + $choices = [ + '1nf' => __('First step of normalization (1NF)'), + '2nf' => __('Second step of normalization (1NF+2NF)'), + '3nf' => __('Third step of normalization (1NF+2NF+3NF)'), + ]; + + $htmlOutput .= Util::getRadioFields( + 'normalizeTo', + $choices, + '1nf', + true + ); + $htmlOutput .= '
    ' + . "" . __( + 'Hint: Please follow the procedure carefully in order ' + . 'to obtain correct normalization' + ) . "" + . '' + . '
    ' + . '
    ' + . ''; + + return $htmlOutput; + } + + /** + * find all the possible partial dependencies based on data in the table. + * + * @param string $table current table + * @param string $db current database + * + * @return string HTML containing the list of all the possible partial dependencies + */ + public function findPartialDependencies($table, $db) + { + $dependencyList = []; + $this->dbi->selectDb($db); + $columns = (array) $this->dbi->getColumnNames( + $db, + $table + ); + $columns = (array) Util::backquote($columns); + $totalRowsRes = $this->dbi->fetchResult( + 'SELECT COUNT(*) FROM (SELECT * FROM ' + . Util::backquote($table) . ' LIMIT 500) as dt;' + ); + $totalRows = $totalRowsRes[0]; + $primary = Index::getPrimary($table, $db); + $primarycols = $primary->getColumns(); + $pk = []; + foreach ($primarycols as $col) { + $pk[] = Util::backquote($col->getName()); + } + $partialKeys = $this->getAllCombinationPartialKeys($pk); + $distinctValCount = $this->findDistinctValuesCount( + array_unique( + array_merge($columns, $partialKeys) + ), + $table + ); + foreach ($columns as $column) { + if (! in_array($column, $pk)) { + foreach ($partialKeys as $partialKey) { + if ($partialKey + && $this->checkPartialDependency( + $partialKey, + $column, + $table, + $distinctValCount[$partialKey], + $distinctValCount[$column], + $totalRows + ) + ) { + $dependencyList[$partialKey][] = $column; + } + } + } + } + + $html = __( + 'This list is based on a subset of the table\'s data ' + . 'and is not necessarily accurate. ' + ) + . '
    '; + foreach ($dependencyList as $dependon => $colList) { + $html .= '' + . '' + . '' + . htmlspecialchars(str_replace('`', '', $dependon)) . ' -> ' + . '' + . htmlspecialchars(str_replace('`', '', implode(', ', $colList))) + . '' + . ''; + } + if (empty($dependencyList)) { + $html .= '

    ' + . __('No partial dependencies found!') . '

    '; + } + $html .= '
    '; + return $html; + } + + /** + * check whether a particular column is dependent on given subset of primary key + * + * @param string $partialKey the partial key, subset of primary key, + * each column in key supposed to be backquoted + * @param string $column backquoted column on whose dependency being checked + * @param string $table current table + * @param integer $pkCnt distinct value count for given partial key + * @param integer $colCnt distinct value count for given column + * @param integer $totalRows total distinct rows count of the table + * + * @return boolean TRUE if $column is dependent on $partialKey, False otherwise + */ + private function checkPartialDependency( + $partialKey, + $column, + $table, + $pkCnt, + $colCnt, + $totalRows + ) { + $query = 'SELECT ' + . 'COUNT(DISTINCT ' . $partialKey . ',' . $column . ') as pkColCnt ' + . 'FROM (SELECT * FROM ' . Util::backquote($table) + . ' LIMIT 500) as dt;'; + $res = $this->dbi->fetchResult($query, null, null); + $pkColCnt = $res[0]; + if ($pkCnt && $pkCnt == $colCnt && $colCnt == $pkColCnt) { + return true; + } + if ($totalRows && $totalRows == $pkCnt) { + return true; + } + return false; + } + + /** + * function to get distinct values count of all the column in the array $columns + * + * @param array $columns array of backquoted columns whose distinct values + * need to be counted. + * @param string $table table to which these columns belong + * + * @return array associative array containing the count + */ + private function findDistinctValuesCount(array $columns, $table) + { + $result = []; + $query = 'SELECT '; + foreach ($columns as $column) { + if ($column) { //each column is already backquoted + $query .= 'COUNT(DISTINCT ' . $column . ') as \'' + . $column . '_cnt\', '; + } + } + $query = trim($query, ', '); + $query .= ' FROM (SELECT * FROM ' . Util::backquote($table) + . ' LIMIT 500) as dt;'; + $res = $this->dbi->fetchResult($query, null, null); + foreach ($columns as $column) { + if ($column) { + $result[$column] = $res[0][$column . '_cnt'] ?? null; + } + } + return $result; + } + + /** + * find all the possible partial keys + * + * @param array $primaryKey array containing all the column present in primary key + * + * @return array containing all the possible partial keys(subset of primary key) + */ + private function getAllCombinationPartialKeys(array $primaryKey) + { + $results = ['']; + foreach ($primaryKey as $element) { + foreach ($results as $combination) { + $results[] = trim($element . ',' . $combination, ','); + } + } + array_pop($results); //remove key which consist of all primary key columns + return $results; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/OpenDocument.php b/srcs/phpmyadmin/libraries/classes/OpenDocument.php new file mode 100644 index 0000000..40ebe15 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/OpenDocument.php @@ -0,0 +1,179 @@ +' + . '' + . '' + . 'phpMyAdmin ' . PMA_VERSION . '' + . 'phpMyAdmin ' . PMA_VERSION + . '' + . '' . strftime('%Y-%m-%dT%H:%M:%S') + . '' + . '' + . '', + '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '', + '' + . '' + . '' + . '' + . '' + . '' + . '', + ]; + + $name = [ + 'mimetype', + 'content.xml', + 'meta.xml', + 'styles.xml', + 'META-INF/manifest.xml', + ]; + + $zipExtension = new ZipExtension(); + return $zipExtension->createFile($data, $name); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Operations.php b/srcs/phpmyadmin/libraries/classes/Operations.php new file mode 100644 index 0000000..7cedd17 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Operations.php @@ -0,0 +1,2263 @@ +dbi = $dbi; + $this->relation = $relation; + } + + /** + * Get HTML output for database comment + * + * @param string $db database name + * + * @return string + */ + public function getHtmlForDatabaseComment($db) + { + $html_output = '
    ' + . '
    ' + . Url::getHiddenInputs($db) + . '
    ' + . ''; + if (Util::showIcons('ActionLinksMode')) { + $html_output .= Util::getImage('b_comment') . ' '; + } + $html_output .= __('Database comment'); + $html_output .= ''; + $html_output .= '' + . '
    '; + $html_output .= '
    ' + . '' + . '
    ' + . '
    ' + . '
    '; + + return $html_output; + } + + /** + * Get HTML output for rename database + * + * @param string $db database name + * @param string $db_collation dataset collation + * + * @return string + */ + public function getHtmlForRenameDatabase($db, $db_collation) + { + $html_output = '
    ' + . '
    '; + if ($db_collation !== null) { + $html_output .= '' . "\n"; + } + $html_output .= '' + . '' + . Url::getHiddenInputs($db) + . '
    ' + . ''; + + if (Util::showIcons('ActionLinksMode')) { + $html_output .= Util::getImage('b_edit') . ' '; + } + $html_output .= __('Rename database to') + . ''; + + $html_output .= ''; + $html_output .= '
    '; + + if ($GLOBALS['db_priv'] && $GLOBALS['table_priv'] + && $GLOBALS['col_priv'] && $GLOBALS['proc_priv'] + && $GLOBALS['is_reload_priv'] + ) { + $html_output .= ''; + } else { + $html_output .= ''; + } + + $html_output .= '
    '; + + $html_output .= '' + . '
    ' + . '
    ' + . '' + . '
    ' + . '
    ' + . '
    '; + + return $html_output; + } + + /** + * Get HTML for database drop link + * + * @param string $db database name + * + * @return string + */ + public function getHtmlForDropDatabaseLink($db) + { + $this_sql_query = 'DROP DATABASE ' . Util::backquote($db); + $this_url_params = [ + 'sql_query' => $this_sql_query, + 'back' => 'db_operations.php', + 'goto' => 'index.php', + 'reload' => '1', + 'purge' => '1', + 'message_to_show' => sprintf( + __('Database %s has been dropped.'), + htmlspecialchars(Util::backquote($db)) + ), + 'db' => null, + ]; + + $html_output = '
    ' + . '
    '; + $html_output .= ''; + if (Util::showIcons('ActionLinksMode')) { + $html_output .= Util::getImage('b_deltbl') . ' '; + } + $html_output .= __('Remove database') + . ''; + $html_output .= '
      '; + $html_output .= $this->getDeleteDataOrTablelink( + $this_url_params, + 'DROP_DATABASE', + __('Drop the database (DROP)'), + 'drop_db_anchor' + ); + $html_output .= '
    ' + . '
    '; + + return $html_output; + } + + /** + * Get HTML snippet for copy database + * + * @param string $db database name + * @param string $db_collation dataset collation + * + * @return string + */ + public function getHtmlForCopyDatabase($db, $db_collation) + { + $drop_clause = 'DROP TABLE / DROP VIEW'; + $choices = [ + 'structure' => __('Structure only'), + 'data' => __('Structure and data'), + 'dataonly' => __('Data only'), + ]; + + $pma_switch_to_new = isset($_SESSION['pma_switch_to_new']) && $_SESSION['pma_switch_to_new']; + + $html_output = '
    '; + $html_output .= '
    '; + + if ($db_collation !== null) { + $html_output .= '' . "\n"; + } + $html_output .= '' . "\n" + . Url::getHiddenInputs($db); + $html_output .= '
    ' + . ''; + + if (Util::showIcons('ActionLinksMode')) { + $html_output .= Util::getImage('b_edit') . ' '; + } + $html_output .= __('Copy database to') + . '' + . '
    ' + . Util::getRadioFields( + 'what', + $choices, + 'data', + true + ); + $html_output .= '
    '; + $html_output .= ''; + $html_output .= '
    '; + $html_output .= ''; + $html_output .= '
    '; + $html_output .= ''; + $html_output .= '
    '; + $html_output .= ''; + $html_output .= '
    '; + $html_output .= '
    '; + + if ($GLOBALS['db_priv'] && $GLOBALS['table_priv'] + && $GLOBALS['col_priv'] && $GLOBALS['proc_priv'] + && $GLOBALS['is_reload_priv'] + ) { + $html_output .= ''; + } else { + $html_output .= ''; + } + $html_output .= '
    '; + + $html_output .= ''; + $html_output .= '' + . '
    '; + $html_output .= '
    ' + . '' + . '
    ' + . '
    ' + . '
    '; + + return $html_output; + } + + /** + * Get HTML snippet for change database charset + * + * @param string $db database name + * @param string $db_collation dataset collation + * + * @return string + */ + public function getHtmlForChangeDatabaseCharset($db, $db_collation) + { + $html_output = '
    ' + . '
    '; + if (Util::showIcons('ActionLinksMode')) { + $html_output .= Util::getImage('s_asci') . ' '; + } + $html_output .= '' . "\n" + . '' . "\n"; + $html_output .= '' . "\n"; + $html_output .= '
    ' + . '' + . '' + . '
    ' + . '' + . '' + . '' + . '
    ' + . '' . "\n" + . '
    ' . "\n" + . '
    ' . "\n"; + + return $html_output; + } + + /** + * Run the Procedure definitions and function definitions + * + * to avoid selecting alternatively the current and new db + * we would need to modify the CREATE definitions to qualify + * the db name + * + * @param string $db database name + * + * @return void + */ + public function runProcedureAndFunctionDefinitions($db) + { + $procedure_names = $this->dbi->getProceduresOrFunctions($db, 'PROCEDURE'); + if ($procedure_names) { + foreach ($procedure_names as $procedure_name) { + $this->dbi->selectDb($db); + $tmp_query = $this->dbi->getDefinition( + $db, + 'PROCEDURE', + $procedure_name + ); + if ($tmp_query !== null) { + // collect for later display + $GLOBALS['sql_query'] .= "\n" . $tmp_query; + $this->dbi->selectDb($_POST['newname']); + $this->dbi->query($tmp_query); + } + } + } + + $function_names = $this->dbi->getProceduresOrFunctions($db, 'FUNCTION'); + if ($function_names) { + foreach ($function_names as $function_name) { + $this->dbi->selectDb($db); + $tmp_query = $this->dbi->getDefinition( + $db, + 'FUNCTION', + $function_name + ); + if ($tmp_query !== null) { + // collect for later display + $GLOBALS['sql_query'] .= "\n" . $tmp_query; + $this->dbi->selectDb($_POST['newname']); + $this->dbi->query($tmp_query); + } + } + } + } + + /** + * Create database before copy + * + * @return void + */ + public function createDbBeforeCopy() + { + $local_query = 'CREATE DATABASE IF NOT EXISTS ' + . Util::backquote($_POST['newname']); + if (isset($_POST['db_collation'])) { + $local_query .= ' DEFAULT' + . Util::getCharsetQueryPart($_POST['db_collation']); + } + $local_query .= ';'; + $GLOBALS['sql_query'] .= $local_query; + + // save the original db name because Tracker.php which + // may be called under $this->dbi->query() changes $GLOBALS['db'] + // for some statements, one of which being CREATE DATABASE + $original_db = $GLOBALS['db']; + $this->dbi->query($local_query); + $GLOBALS['db'] = $original_db; + + // Set the SQL mode to NO_AUTO_VALUE_ON_ZERO to prevent MySQL from creating + // export statements it cannot import + $sql_set_mode = "SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO'"; + $this->dbi->query($sql_set_mode); + + // rebuild the database list because Table::moveCopy + // checks in this list if the target db exists + $GLOBALS['dblist']->databases->build(); + } + + /** + * Get views as an array and create SQL view stand-in + * + * @param array $tables_full array of all tables in given db or dbs + * @param ExportSql $export_sql_plugin export plugin instance + * @param string $db database name + * + * @return array + */ + public function getViewsAndCreateSqlViewStandIn( + array $tables_full, + $export_sql_plugin, + $db + ) { + $views = []; + foreach ($tables_full as $each_table => $tmp) { + // to be able to rename a db containing views, + // first all the views are collected and a stand-in is created + // the real views are created after the tables + if ($this->dbi->getTable($db, (string) $each_table)->isView()) { + // If view exists, and 'add drop view' is selected: Drop it! + if ($_POST['what'] != 'nocopy' + && isset($_POST['drop_if_exists']) + && $_POST['drop_if_exists'] == 'true' + ) { + $drop_query = 'DROP VIEW IF EXISTS ' + . Util::backquote($_POST['newname']) . '.' + . Util::backquote($each_table); + $this->dbi->query($drop_query); + + $GLOBALS['sql_query'] .= "\n" . $drop_query . ';'; + } + + $views[] = $each_table; + // Create stand-in definition to resolve view dependencies + $sql_view_standin = $export_sql_plugin->getTableDefStandIn( + $db, + $each_table, + "\n" + ); + $this->dbi->selectDb($_POST['newname']); + $this->dbi->query($sql_view_standin); + $GLOBALS['sql_query'] .= "\n" . $sql_view_standin; + } + } + return $views; + } + + /** + * Get sql query for copy/rename table and boolean for whether copy/rename or not + * + * @param array $tables_full array of all tables in given db or dbs + * @param boolean $move whether database name is empty or not + * @param string $db database name + * + * @return array SQL queries for the constraints + */ + public function copyTables(array $tables_full, $move, $db) + { + $sqlContraints = []; + foreach ($tables_full as $each_table => $tmp) { + // skip the views; we have created stand-in definitions + if ($this->dbi->getTable($db, (string) $each_table)->isView()) { + continue; + } + + // value of $what for this table only + $this_what = $_POST['what']; + + // do not copy the data from a Merge table + // note: on the calling FORM, 'data' means 'structure and data' + if ($this->dbi->getTable($db, (string) $each_table)->isMerge()) { + if ($this_what == 'data') { + $this_what = 'structure'; + } + if ($this_what == 'dataonly') { + $this_what = 'nocopy'; + } + } + + if ($this_what != 'nocopy') { + // keep the triggers from the original db+table + // (third param is empty because delimiters are only intended + // for importing via the mysql client or our Import feature) + $triggers = $this->dbi->getTriggers($db, (string) $each_table, ''); + + if (! Table::moveCopy( + $db, + $each_table, + $_POST['newname'], + $each_table, + (isset($this_what) ? $this_what : 'data'), + $move, + 'db_copy' + )) { + $GLOBALS['_error'] = true; + break; + } + // apply the triggers to the destination db+table + if ($triggers) { + $this->dbi->selectDb($_POST['newname']); + foreach ($triggers as $trigger) { + $this->dbi->query($trigger['create']); + $GLOBALS['sql_query'] .= "\n" . $trigger['create'] . ';'; + } + } + + // this does not apply to a rename operation + if (isset($_POST['add_constraints']) + && ! empty($GLOBALS['sql_constraints_query']) + ) { + $sqlContraints[] = $GLOBALS['sql_constraints_query']; + unset($GLOBALS['sql_constraints_query']); + } + } + } + return $sqlContraints; + } + + /** + * Run the EVENT definition for selected database + * + * to avoid selecting alternatively the current and new db + * we would need to modify the CREATE definitions to qualify + * the db name + * + * @param string $db database name + * + * @return void + */ + public function runEventDefinitionsForDb($db) + { + $event_names = $this->dbi->fetchResult( + 'SELECT EVENT_NAME FROM information_schema.EVENTS WHERE EVENT_SCHEMA= \'' + . $this->dbi->escapeString($db) . '\';' + ); + if ($event_names) { + foreach ($event_names as $event_name) { + $this->dbi->selectDb($db); + $tmp_query = $this->dbi->getDefinition($db, 'EVENT', $event_name); + // collect for later display + $GLOBALS['sql_query'] .= "\n" . $tmp_query; + $this->dbi->selectDb($_POST['newname']); + $this->dbi->query($tmp_query); + } + } + } + + /** + * Handle the views, return the boolean value whether table rename/copy or not + * + * @param array $views views as an array + * @param boolean $move whether database name is empty or not + * @param string $db database name + * + * @return void + */ + public function handleTheViews(array $views, $move, $db) + { + // temporarily force to add DROP IF EXIST to CREATE VIEW query, + // to remove stand-in VIEW that was created earlier + // ( $_POST['drop_if_exists'] is used in moveCopy() ) + if (isset($_POST['drop_if_exists'])) { + $temp_drop_if_exists = $_POST['drop_if_exists']; + } + + $_POST['drop_if_exists'] = 'true'; + foreach ($views as $view) { + $copying_succeeded = Table::moveCopy( + $db, + $view, + $_POST['newname'], + $view, + 'structure', + $move, + 'db_copy' + ); + if (! $copying_succeeded) { + $GLOBALS['_error'] = true; + break; + } + } + unset($_POST['drop_if_exists']); + + if (isset($temp_drop_if_exists)) { + // restore previous value + $_POST['drop_if_exists'] = $temp_drop_if_exists; + } + } + + /** + * Adjust the privileges after Renaming the db + * + * @param string $oldDb Database name before renaming + * @param string $newname New Database name requested + * + * @return void + */ + public function adjustPrivilegesMoveDb($oldDb, $newname) + { + if ($GLOBALS['db_priv'] && $GLOBALS['table_priv'] + && $GLOBALS['col_priv'] && $GLOBALS['proc_priv'] + && $GLOBALS['is_reload_priv'] + ) { + $this->dbi->selectDb('mysql'); + $newname = str_replace("_", "\_", $newname); + $oldDb = str_replace("_", "\_", $oldDb); + + // For Db specific privileges + $query_db_specific = 'UPDATE ' . Util::backquote('db') + . 'SET Db = \'' . $this->dbi->escapeString($newname) + . '\' where Db = \'' . $this->dbi->escapeString($oldDb) . '\';'; + $this->dbi->query($query_db_specific); + + // For table specific privileges + $query_table_specific = 'UPDATE ' . Util::backquote('tables_priv') + . 'SET Db = \'' . $this->dbi->escapeString($newname) + . '\' where Db = \'' . $this->dbi->escapeString($oldDb) . '\';'; + $this->dbi->query($query_table_specific); + + // For column specific privileges + $query_col_specific = 'UPDATE ' . Util::backquote('columns_priv') + . 'SET Db = \'' . $this->dbi->escapeString($newname) + . '\' where Db = \'' . $this->dbi->escapeString($oldDb) . '\';'; + $this->dbi->query($query_col_specific); + + // For procedures specific privileges + $query_proc_specific = 'UPDATE ' . Util::backquote('procs_priv') + . 'SET Db = \'' . $this->dbi->escapeString($newname) + . '\' where Db = \'' . $this->dbi->escapeString($oldDb) . '\';'; + $this->dbi->query($query_proc_specific); + + // Finally FLUSH the new privileges + $flush_query = "FLUSH PRIVILEGES;"; + $this->dbi->query($flush_query); + } + } + + /** + * Adjust the privileges after Copying the db + * + * @param string $oldDb Database name before copying + * @param string $newname New Database name requested + * + * @return void + */ + public function adjustPrivilegesCopyDb($oldDb, $newname) + { + if ($GLOBALS['db_priv'] && $GLOBALS['table_priv'] + && $GLOBALS['col_priv'] && $GLOBALS['proc_priv'] + && $GLOBALS['is_reload_priv'] + ) { + $this->dbi->selectDb('mysql'); + $newname = str_replace("_", "\_", $newname); + $oldDb = str_replace("_", "\_", $oldDb); + + $query_db_specific_old = 'SELECT * FROM ' + . Util::backquote('db') . ' WHERE ' + . 'Db = "' . $oldDb . '";'; + + $old_privs_db = $this->dbi->fetchResult($query_db_specific_old, 0); + + foreach ($old_privs_db as $old_priv) { + $newDb_db_privs_query = 'INSERT INTO ' . Util::backquote('db') + . ' VALUES("' . $old_priv[0] . '", "' . $newname . '"'; + for ($i = 2; $i < count($old_priv); $i++) { + $newDb_db_privs_query .= ', "' . $old_priv[$i] . '"'; + } + $newDb_db_privs_query .= ')'; + + $this->dbi->query($newDb_db_privs_query); + } + + // For Table Specific privileges + $query_table_specific_old = 'SELECT * FROM ' + . Util::backquote('tables_priv') . ' WHERE ' + . 'Db = "' . $oldDb . '";'; + + $old_privs_table = $this->dbi->fetchResult( + $query_table_specific_old, + 0 + ); + + foreach ($old_privs_table as $old_priv) { + $newDb_table_privs_query = 'INSERT INTO ' . Util::backquote( + 'tables_priv' + ) . ' VALUES("' . $old_priv[0] . '", "' . $newname . '", "' + . $old_priv[2] . '", "' . $old_priv[3] . '", "' . $old_priv[4] + . '", "' . $old_priv[5] . '", "' . $old_priv[6] . '", "' + . $old_priv[7] . '");'; + + $this->dbi->query($newDb_table_privs_query); + } + + // For Column Specific privileges + $query_col_specific_old = 'SELECT * FROM ' + . Util::backquote('columns_priv') . ' WHERE ' + . 'Db = "' . $oldDb . '";'; + + $old_privs_col = $this->dbi->fetchResult( + $query_col_specific_old, + 0 + ); + + foreach ($old_privs_col as $old_priv) { + $newDb_col_privs_query = 'INSERT INTO ' . Util::backquote( + 'columns_priv' + ) . ' VALUES("' . $old_priv[0] . '", "' . $newname . '", "' + . $old_priv[2] . '", "' . $old_priv[3] . '", "' . $old_priv[4] + . '", "' . $old_priv[5] . '", "' . $old_priv[6] . '");'; + + $this->dbi->query($newDb_col_privs_query); + } + + // For Procedure Specific privileges + $query_proc_specific_old = 'SELECT * FROM ' + . Util::backquote('procs_priv') . ' WHERE ' + . 'Db = "' . $oldDb . '";'; + + $old_privs_proc = $this->dbi->fetchResult( + $query_proc_specific_old, + 0 + ); + + foreach ($old_privs_proc as $old_priv) { + $newDb_proc_privs_query = 'INSERT INTO ' . Util::backquote( + 'procs_priv' + ) . ' VALUES("' . $old_priv[0] . '", "' . $newname . '", "' + . $old_priv[2] . '", "' . $old_priv[3] . '", "' . $old_priv[4] + . '", "' . $old_priv[5] . '", "' . $old_priv[6] . '", "' + . $old_priv[7] . '");'; + + $this->dbi->query($newDb_proc_privs_query); + } + + // Finally FLUSH the new privileges + $flush_query = "FLUSH PRIVILEGES;"; + $this->dbi->query($flush_query); + } + } + + /** + * Create all accumulated constraints + * + * @param array $sqlConstratints array of sql constraints for the database + * + * @return void + */ + public function createAllAccumulatedConstraints(array $sqlConstratints) + { + $this->dbi->selectDb($_POST['newname']); + foreach ($sqlConstratints as $one_query) { + $this->dbi->query($one_query); + // and prepare to display them + $GLOBALS['sql_query'] .= "\n" . $one_query; + } + } + + /** + * Duplicate the bookmarks for the db (done once for each db) + * + * @param boolean $_error whether table rename/copy or not + * @param string $db database name + * + * @return void + */ + public function duplicateBookmarks($_error, $db) + { + if (! $_error && $db != $_POST['newname']) { + $get_fields = [ + 'user', + 'label', + 'query', + ]; + $where_fields = ['dbase' => $db]; + $new_fields = ['dbase' => $_POST['newname']]; + Table::duplicateInfo( + 'bookmarkwork', + 'bookmark', + $get_fields, + $where_fields, + $new_fields + ); + } + } + + /** + * Get the HTML snippet for order the table + * + * @param array $columns columns array + * + * @return string + */ + public function getHtmlForOrderTheTable(array $columns) + { + $html_output = '
    '; + $html_output .= '
    '; + $html_output .= Url::getHiddenInputs( + $GLOBALS['db'], + $GLOBALS['table'] + ); + $html_output .= '
    ' + . '' . __('Alter table order by') . '' + . ' ' . __('(singly)') . ' ' + . '
    ' + . '' + . '' + . '' + . '' + . '
    ' + . '
    ' + . '' + . '' + . '
    ' + . '
    ' + . '
    '; + + return $html_output; + } + + /** + * Get the HTML snippet for move table + * + * @return string + */ + public function getHtmlForMoveTable() + { + $html_output = '
    '; + $html_output .= '
    ' + . Url::getHiddenInputs($GLOBALS['db'], $GLOBALS['table']); + + $html_output .= '' + . '' + . '
    '; + + $html_output .= '' . __('Move table to (database.table)') + . ''; + + if (count($GLOBALS['dblist']->databases) > $GLOBALS['cfg']['MaxDbList']) { + $html_output .= ''; + } else { + $html_output .= ''; + } + $html_output .= ' . '; + $html_output .= '
    '; + + // starting with MySQL 5.0.24, SHOW CREATE TABLE includes the AUTO_INCREMENT + // next value but users can decide if they want it or not for the operation + + $html_output .= '' + . '
    '; + + if ($GLOBALS['table_priv'] && $GLOBALS['col_priv'] + && $GLOBALS['is_reload_priv'] + ) { + $html_output .= ''; + } else { + $html_output .= ''; + } + $html_output .= '
    '; + + $html_output .= '
    ' + . '' + . '
    ' + . '
    ' + . '
    '; + + return $html_output; + } + + /** + * Get the HTML div for Table option + * + * @param Table $pma_table Table object + * @param string $comment Comment + * @param string $tbl_collation table collation + * @param string $tbl_storage_engine table storage engine + * @param string $pack_keys pack keys + * @param string $auto_increment value of auto increment + * @param string $delay_key_write delay key write + * @param string $transactional value of transactional + * @param string $page_checksum value of page checksum + * @param string $checksum the checksum + * + * @return string + */ + public function getTableOptionDiv( + $pma_table, + $comment, + $tbl_collation, + $tbl_storage_engine, + $pack_keys, + $auto_increment, + $delay_key_write, + $transactional, + $page_checksum, + $checksum + ) { + $html_output = '
    '; + $html_output .= '
    getTableOptionFieldset( + $pma_table, + $comment, + $tbl_collation, + $tbl_storage_engine, + $pack_keys, + $delay_key_write, + $auto_increment, + $transactional, + $page_checksum, + $checksum + ); + + $html_output .= '
    ' + . '' + . '' + . '
    ' + . '
    ' + . '
    '; + + return $html_output; + } + + /** + * Get HTML for the rename table part of table options + * + * @return string + */ + private function getHtmlForRenameTable() + { + $html_output = '' . __('Rename table to') . '' + . '' + . '' + . '' + . ''; + + if ($GLOBALS['table_priv'] && $GLOBALS['col_priv'] + && $GLOBALS['is_reload_priv'] + ) { + $html_output .= ''; + } else { + $html_output .= ''; + } + $html_output .= ''; + + $html_output .= ''; + return $html_output; + } + + /** + * Get HTML for the table comments part of table options + * + * @param string $current_value of the table comments + * + * @return string + */ + private function getHtmlForTableComments($current_value) + { + $commentLength = $this->dbi->getVersion() >= 50503 ? 2048 : 60; + return '' . __('Table comments') . '' + . '' + . '' + . '' + . ''; + } + + /** + * Get HTML for the PACK KEYS part of table options + * + * @param string $current_value of the pack keys option + * + * @return string + */ + private function getHtmlForPackKeys($current_value) + { + $html_output = '' + . '' + . '' . "\n"; + $html_output .= '' . "\n"; + + $charsets = Charsets::getCharsets($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']); + $collations = Charsets::getCollations($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']); + /** @var Charset $charset */ + foreach ($charsets as $charset) { + $html_output .= '' . "\n"; + /** @var Collation $collation */ + foreach ($collations[$charset->getName()] as $collation) { + $html_output .= '' . "\n"; + } + $html_output .= '' . "\n"; + } + $html_output .= '' . "\n"; + $html_output .= '' + . ''; + + // Change all Column collations + $html_output .= '' + . '' + . '' + . ''; + + if ($pma_table->isEngine(['MYISAM', 'ARIA', 'ISAM'])) { + $html_output .= $this->getHtmlForPackKeys($pack_keys); + } // end if (MYISAM|ISAM) + + if ($pma_table->isEngine(['MYISAM', 'ARIA'])) { + $html_output .= $this->getHtmlForTableRow( + 'new_checksum', + 'CHECKSUM', + $checksum + ); + + $html_output .= $this->getHtmlForTableRow( + 'new_delay_key_write', + 'DELAY_KEY_WRITE', + $delay_key_write + ); + } // end if (MYISAM) + + if ($pma_table->isEngine('ARIA')) { + $html_output .= $this->getHtmlForTableRow( + 'new_transactional', + 'TRANSACTIONAL', + $transactional + ); + + $html_output .= $this->getHtmlForTableRow( + 'new_page_checksum', + 'PAGE_CHECKSUM', + $page_checksum + ); + } // end if (ARIA) + + if (strlen($auto_increment) > 0 + && $pma_table->isEngine(['MYISAM', 'ARIA', 'INNODB', 'PBXT', 'ROCKSDB']) + ) { + $html_output .= '' + . '' + . '' + . ' '; + } // end if (MYISAM|INNODB) + + $possible_row_formats = $this->getPossibleRowFormat(); + + // for MYISAM there is also COMPRESSED but it can be set only by the + // myisampack utility, so don't offer here the choice because if we + // try it inside an ALTER TABLE, MySQL (at least in 5.1.23-maria) + // does not return a warning + // (if the table was compressed, it can be seen on the Structure page) + + if (isset($possible_row_formats[$tbl_storage_engine])) { + $current_row_format + = mb_strtoupper($GLOBALS['showtable']['Row_format']); + $html_output .= '' + . '' + . ''; + $html_output .= Util::getDropdown( + 'new_row_format', + $possible_row_formats[$tbl_storage_engine], + $current_row_format, + 'new_row_format' + ); + $html_output .= ''; + } + $html_output .= '' + . ''; + + return $html_output; + } + + /** + * Get the common HTML table row (tr) for new_checksum, new_delay_key_write, + * new_transactional and new_page_checksum + * + * @param string $attribute class, name and id attribute + * @param string $label label value + * @param string $val checksum, delay_key_write, transactional, page_checksum + * + * @return string + */ + private function getHtmlForTableRow($attribute, $label, $val) + { + return '' + . '' + . '' + . '' + . '' + . '' + . '' + . ''; + } + + /** + * Get array of possible row formats + * + * @return array + */ + private function getPossibleRowFormat() + { + // the outer array is for engines, the inner array contains the dropdown + // option values as keys then the dropdown option labels + + $possible_row_formats = [ + 'ARCHIVE' => [ + 'COMPRESSED' => 'COMPRESSED', + ], + 'ARIA' => [ + 'FIXED' => 'FIXED', + 'DYNAMIC' => 'DYNAMIC', + 'PAGE' => 'PAGE', + ], + 'MARIA' => [ + 'FIXED' => 'FIXED', + 'DYNAMIC' => 'DYNAMIC', + 'PAGE' => 'PAGE', + ], + 'MYISAM' => [ + 'FIXED' => 'FIXED', + 'DYNAMIC' => 'DYNAMIC', + ], + 'PBXT' => [ + 'FIXED' => 'FIXED', + 'DYNAMIC' => 'DYNAMIC', + ], + 'INNODB' => [ + 'COMPACT' => 'COMPACT', + 'REDUNDANT' => 'REDUNDANT', + ], + ]; + + /** @var Innodb $innodbEnginePlugin */ + $innodbEnginePlugin = StorageEngine::getEngine('Innodb'); + $innodbPluginVersion = $innodbEnginePlugin->getInnodbPluginVersion(); + if (! empty($innodbPluginVersion)) { + $innodb_file_format = $innodbEnginePlugin->getInnodbFileFormat(); + } else { + $innodb_file_format = ''; + } + /** + * Newer MySQL/MariaDB always return empty a.k.a '' on $innodb_file_format otherwise + * old versions of MySQL/MariaDB must be returning something or not empty. + * This patch is to support newer MySQL/MariaDB while also for backward compatibilities. + */ + if (( ('Barracuda' == $innodb_file_format) || ($innodb_file_format == '') ) + && $innodbEnginePlugin->supportsFilePerTable() + ) { + $possible_row_formats['INNODB']['DYNAMIC'] = 'DYNAMIC'; + $possible_row_formats['INNODB']['COMPRESSED'] = 'COMPRESSED'; + } + + return $possible_row_formats; + } + + /** + * Get HTML div for copy table + * + * @return string + */ + public function getHtmlForCopytable() + { + $html_output = '
    '; + $html_output .= '
    ' + . Url::getHiddenInputs($GLOBALS['db'], $GLOBALS['table']) + . ''; + + $html_output .= '
    '; + $html_output .= '' + . __('Copy table to (database.table)') . ''; + + if (count($GLOBALS['dblist']->databases) > $GLOBALS['cfg']['MaxDbList']) { + $html_output .= ''; + } else { + $html_output .= ''; + } + $html_output .= ' . '; + $html_output .= '
    '; + + $choices = [ + 'structure' => __('Structure only'), + 'data' => __('Structure and data'), + 'dataonly' => __('Data only'), + ]; + + $html_output .= Util::getRadioFields( + 'what', + $choices, + 'data', + true + ); + $html_output .= '
    '; + + $html_output .= '' + . '
    ' + . '' + . '
    '; + + // display "Add constraints" choice only if there are + // foreign keys + if ($this->relation->getForeigners($GLOBALS['db'], $GLOBALS['table'], '', 'foreign')) { + $html_output .= ''; + $html_output .= '
    '; + } // endif + + $html_output .= '
    '; + + if ($GLOBALS['table_priv'] && $GLOBALS['col_priv'] + && $GLOBALS['is_reload_priv'] + ) { + $html_output .= ''; + } else { + $html_output .= ''; + } + $html_output .= '
    '; + + $pma_switch_to_new = isset($_SESSION['pma_switch_to_new']) && $_SESSION['pma_switch_to_new']; + + $html_output .= ''; + $html_output .= '' + . '
    '; + + $html_output .= '
    ' + . '' + . '
    ' + . '
    ' + . '
    '; + + return $html_output; + } + + /** + * Get HTML snippet for table maintenance + * + * @param Table $pma_table Table object + * @param array $url_params array of URL parameters + * + * @return string + */ + public function getHtmlForTableMaintenance($pma_table, array $url_params) + { + $html_output = '
    '; + $html_output .= '
    ' + . '' . __('Table maintenance') . ''; + $html_output .= '
      '; + + // Note: BERKELEY (BDB) is no longer supported, starting with MySQL 5.1 + $html_output .= $this->getListofMaintainActionLink($pma_table, $url_params); + + $html_output .= '
    ' + . '
    ' + . '
    '; + + return $html_output; + } + + /** + * Get HTML 'li' having a link of maintain action + * + * @param Table $pma_table Table object + * @param array $url_params Array of URL parameters + * + * @return string + */ + private function getListofMaintainActionLink($pma_table, array $url_params) + { + $html_output = ''; + + // analyze table + if ($pma_table->isEngine(['MYISAM', 'ARIA', 'INNODB', 'BERKELEYDB', 'TOKUDB'])) { + $params = [ + 'sql_query' => 'ANALYZE TABLE ' + . Util::backquote($GLOBALS['table']), + 'table_maintenance' => 'Go', + ]; + $html_output .= $this->getMaintainActionlink( + __('Analyze table'), + $params, + $url_params, + 'ANALYZE_TABLE' + ); + } + + // check table + if ($pma_table->isEngine(['MYISAM', 'ARIA', 'INNODB', 'TOKUDB'])) { + $params = [ + 'sql_query' => 'CHECK TABLE ' + . Util::backquote($GLOBALS['table']), + 'table_maintenance' => 'Go', + ]; + $html_output .= $this->getMaintainActionlink( + __('Check table'), + $params, + $url_params, + 'CHECK_TABLE' + ); + } + + // checksum table + $params = [ + 'sql_query' => 'CHECKSUM TABLE ' + . Util::backquote($GLOBALS['table']), + 'table_maintenance' => 'Go', + ]; + $html_output .= $this->getMaintainActionlink( + __('Checksum table'), + $params, + $url_params, + 'CHECKSUM_TABLE' + ); + + // defragment table + if ($pma_table->isEngine(['INNODB'])) { + $params = [ + 'sql_query' => 'ALTER TABLE ' + . Util::backquote($GLOBALS['table']) + . ' ENGINE = InnoDB;', + ]; + $html_output .= $this->getMaintainActionlink( + __('Defragment table'), + $params, + $url_params, + 'InnoDB_File_Defragmenting' + ); + } + + // flush table + $params = [ + 'sql_query' => 'FLUSH TABLE ' + . Util::backquote($GLOBALS['table']), + 'message_to_show' => sprintf( + __('Table %s has been flushed.'), + htmlspecialchars($GLOBALS['table']) + ), + 'reload' => 1, + ]; + $html_output .= $this->getMaintainActionlink( + __('Flush the table (FLUSH)'), + $params, + $url_params, + 'FLUSH' + ); + + // optimize table + if ($pma_table->isEngine(['MYISAM', 'ARIA', 'INNODB', 'BERKELEYDB', 'TOKUDB'])) { + $params = [ + 'sql_query' => 'OPTIMIZE TABLE ' + . Util::backquote($GLOBALS['table']), + 'table_maintenance' => 'Go', + ]; + $html_output .= $this->getMaintainActionlink( + __('Optimize table'), + $params, + $url_params, + 'OPTIMIZE_TABLE' + ); + } + + // repair table + if ($pma_table->isEngine(['MYISAM', 'ARIA'])) { + $params = [ + 'sql_query' => 'REPAIR TABLE ' + . Util::backquote($GLOBALS['table']), + 'table_maintenance' => 'Go', + ]; + $html_output .= $this->getMaintainActionlink( + __('Repair table'), + $params, + $url_params, + 'REPAIR_TABLE' + ); + } + + return $html_output; + } + + /** + * Get maintain action HTML link + * + * @param string $action_message action message + * @param array $params url parameters array + * @param array $url_params additional url parameters + * @param string $link contains name of page/anchor that is being linked + * + * @return string + */ + private function getMaintainActionlink($action_message, array $params, array $url_params, $link) + { + return '
  • ' + . Util::linkOrButton( + 'sql.php' . Url::getCommon(array_merge($url_params, $params)), + $action_message, + ['class' => 'maintain_action ajax'] + ) + . Util::showMySQLDocu($link) + . '
  • '; + } + + /** + * Get HTML for Delete data or table (truncate table, drop table) + * + * @param array $truncate_table_url_params url parameter array for truncate table + * @param array $dropTableUrlParams url parameter array for drop table + * + * @return string + */ + public function getHtmlForDeleteDataOrTable( + array $truncate_table_url_params, + array $dropTableUrlParams + ) { + $html_output = '
    ' + . '
    ' + . '' . __('Delete data or table') . ''; + + $html_output .= '
      '; + + if (! empty($truncate_table_url_params)) { + $html_output .= $this->getDeleteDataOrTablelink( + $truncate_table_url_params, + 'TRUNCATE_TABLE', + __('Empty the table (TRUNCATE)'), + 'truncate_tbl_anchor' + ); + } + if (! empty($dropTableUrlParams)) { + $html_output .= $this->getDeleteDataOrTablelink( + $dropTableUrlParams, + 'DROP_TABLE', + __('Delete the table (DROP)'), + 'drop_tbl_anchor' + ); + } + $html_output .= '
    '; + + return $html_output; + } + + /** + * Get the HTML link for Truncate table, Drop table and Drop db + * + * @param array $url_params url parameter array for delete data or table + * @param string $syntax TRUNCATE_TABLE or DROP_TABLE or DROP_DATABASE + * @param string $link link to be shown + * @param string $htmlId id of the link + * + * @return string html output + */ + public function getDeleteDataOrTablelink(array $url_params, $syntax, $link, $htmlId) + { + return '
  • ' . Util::linkOrButton( + 'sql.php' . Url::getCommon($url_params), + $link, + [ + 'id' => $htmlId, + 'class' => 'ajax', + ] + ) + . Util::showMySQLDocu($syntax) + . '
  • '; + } + + /** + * Get HTML snippet for partition maintenance + * + * @param array $partition_names array of partition names for a specific db/table + * @param array $url_params url parameters + * + * @return string + */ + public function getHtmlForPartitionMaintenance(array $partition_names, array $url_params) + { + $choices = [ + 'ANALYZE' => __('Analyze'), + 'CHECK' => __('Check'), + 'OPTIMIZE' => __('Optimize'), + 'REBUILD' => __('Rebuild'), + 'REPAIR' => __('Repair'), + 'TRUNCATE' => __('Truncate'), + ]; + + $partition_method = Partition::getPartitionMethod( + $GLOBALS['db'], + $GLOBALS['table'] + ); + // add COALESCE or DROP option to choices array depeding on Partition method + if ($partition_method == 'RANGE' + || $partition_method == 'RANGE COLUMNS' + || $partition_method == 'LIST' + || $partition_method == 'LIST COLUMNS' + ) { + $choices['DROP'] = __('Drop'); + } else { + $choices['COALESCE'] = __('Coalesce'); + } + + $html_output = '
    ' + . '
    ' + . Url::getHiddenInputs($GLOBALS['db'], $GLOBALS['table']) + . '
    ' + . '' + . __('Partition maintenance') + . Util::showMySQLDocu('partitioning_maintenance') + . ''; + + $html_select = '' . "\n"; + $html_output .= sprintf(__('Partition %s'), $html_select); + + $html_output .= '
    '; + $html_output .= Util::getRadioFields( + 'partition_operation', + $choices, + 'ANALYZE', + false, + true, + 'floatleft' + ); + $this_url_params = array_merge( + $url_params, + [ + 'sql_query' => 'ALTER TABLE ' + . Util::backquote($GLOBALS['table']) + . ' REMOVE PARTITIONING;', + ] + ); + $html_output .= '

    '; + + $html_output .= '' + . __('Remove partitioning') . ''; + + $html_output .= '
    ' + . '
    ' + . '' + . '' + . '
    ' + . '
    ' + . '
    '; + + return $html_output; + } + + /** + * Get the HTML for Referential Integrity check + * + * @param array $foreign all Relations to foreign tables for a given table + * or optionally a given column in a table + * @param array $url_params array of url parameters + * + * @return string + */ + public function getHtmlForReferentialIntegrityCheck(array $foreign, array $url_params) + { + $html_output = '
    ' + . '
    ' + . '' . __('Check referential integrity:') . ''; + + $html_output .= '
      '; + + foreach ($foreign as $master => $arr) { + $join_query = 'SELECT ' + . Util::backquote($GLOBALS['table']) . '.*' + . ' FROM ' . Util::backquote($GLOBALS['table']) + . ' LEFT JOIN ' + . Util::backquote($arr['foreign_db']) + . '.' + . Util::backquote($arr['foreign_table']); + if ($arr['foreign_table'] == $GLOBALS['table']) { + $foreign_table = $GLOBALS['table'] . '1'; + $join_query .= ' AS ' . Util::backquote($foreign_table); + } else { + $foreign_table = $arr['foreign_table']; + } + $join_query .= ' ON ' + . Util::backquote($GLOBALS['table']) . '.' + . Util::backquote($master) + . ' = ' + . Util::backquote($arr['foreign_db']) + . '.' + . Util::backquote($foreign_table) . '.' + . Util::backquote($arr['foreign_field']) + . ' WHERE ' + . Util::backquote($arr['foreign_db']) + . '.' + . Util::backquote($foreign_table) . '.' + . Util::backquote($arr['foreign_field']) + . ' IS NULL AND ' + . Util::backquote($GLOBALS['table']) . '.' + . Util::backquote($master) + . ' IS NOT NULL'; + $this_url_params = array_merge( + $url_params, + [ + 'sql_query' => $join_query, + 'sql_signature' => Core::signSqlQuery($join_query), + ] + ); + + $html_output .= '
    • ' + . '' + . $master . ' -> ' . $arr['foreign_db'] . '.' + . $arr['foreign_table'] . '.' . $arr['foreign_field'] + . '
    • ' . "\n"; + } // foreach $foreign + $html_output .= '
    '; + + return $html_output; + } + + /** + * Reorder table based on request params + * + * @return array SQL query and result + */ + public function getQueryAndResultForReorderingTable() + { + $sql_query = 'ALTER TABLE ' + . Util::backquote($GLOBALS['table']) + . ' ORDER BY ' + . Util::backquote(urldecode($_POST['order_field'])); + if (isset($_POST['order_order']) + && $_POST['order_order'] === 'desc' + ) { + $sql_query .= ' DESC'; + } else { + $sql_query .= ' ASC'; + } + $sql_query .= ';'; + $result = $this->dbi->query($sql_query); + + return [ + $sql_query, + $result, + ]; + } + + /** + * Get table alters array + * + * @param Table $pma_table The Table object + * @param string $pack_keys pack keys + * @param string $checksum value of checksum + * @param string $page_checksum value of page checksum + * @param string $delay_key_write delay key write + * @param string $row_format row format + * @param string $newTblStorageEngine table storage engine + * @param string $transactional value of transactional + * @param string $tbl_collation collation of the table + * + * @return array + */ + public function getTableAltersArray( + $pma_table, + $pack_keys, + $checksum, + $page_checksum, + $delay_key_write, + $row_format, + $newTblStorageEngine, + $transactional, + $tbl_collation + ) { + global $auto_increment; + + $table_alters = []; + + if (isset($_POST['comment']) + && urldecode($_POST['prev_comment']) !== $_POST['comment'] + ) { + $table_alters[] = 'COMMENT = \'' + . $this->dbi->escapeString($_POST['comment']) . '\''; + } + + if (! empty($newTblStorageEngine) + && mb_strtolower($newTblStorageEngine) !== mb_strtolower($GLOBALS['tbl_storage_engine']) + ) { + $table_alters[] = 'ENGINE = ' . $newTblStorageEngine; + } + if (! empty($_POST['tbl_collation']) + && $_POST['tbl_collation'] !== $tbl_collation + ) { + $table_alters[] = 'DEFAULT ' + . Util::getCharsetQueryPart($_POST['tbl_collation']); + } + + if ($pma_table->isEngine(['MYISAM', 'ARIA', 'ISAM']) + && isset($_POST['new_pack_keys']) + && $_POST['new_pack_keys'] != (string) $pack_keys + ) { + $table_alters[] = 'pack_keys = ' . $_POST['new_pack_keys']; + } + + $_POST['new_checksum'] = empty($_POST['new_checksum']) ? '0' : '1'; + if ($pma_table->isEngine(['MYISAM', 'ARIA']) + && $_POST['new_checksum'] !== $checksum + ) { + $table_alters[] = 'checksum = ' . $_POST['new_checksum']; + } + + $_POST['new_transactional'] + = empty($_POST['new_transactional']) ? '0' : '1'; + if ($pma_table->isEngine('ARIA') + && $_POST['new_transactional'] !== $transactional + ) { + $table_alters[] = 'TRANSACTIONAL = ' . $_POST['new_transactional']; + } + + $_POST['new_page_checksum'] + = empty($_POST['new_page_checksum']) ? '0' : '1'; + if ($pma_table->isEngine('ARIA') + && $_POST['new_page_checksum'] !== $page_checksum + ) { + $table_alters[] = 'PAGE_CHECKSUM = ' . $_POST['new_page_checksum']; + } + + $_POST['new_delay_key_write'] + = empty($_POST['new_delay_key_write']) ? '0' : '1'; + if ($pma_table->isEngine(['MYISAM', 'ARIA']) + && $_POST['new_delay_key_write'] !== $delay_key_write + ) { + $table_alters[] = 'delay_key_write = ' . $_POST['new_delay_key_write']; + } + + if ($pma_table->isEngine(['MYISAM', 'ARIA', 'INNODB', 'PBXT', 'ROCKSDB']) + && ! empty($_POST['new_auto_increment']) + && (! isset($auto_increment) + || $_POST['new_auto_increment'] !== $auto_increment) + ) { + $table_alters[] = 'auto_increment = ' + . $this->dbi->escapeString($_POST['new_auto_increment']); + } + + if (! empty($_POST['new_row_format'])) { + $newRowFormat = $_POST['new_row_format']; + $newRowFormatLower = mb_strtolower($newRowFormat); + if ($pma_table->isEngine(['MYISAM', 'ARIA', 'INNODB', 'PBXT']) + && (strlen($row_format) === 0 + || $newRowFormatLower !== mb_strtolower($row_format)) + ) { + $table_alters[] = 'ROW_FORMAT = ' + . $this->dbi->escapeString($newRowFormat); + } + } + + return $table_alters; + } + + /** + * Get warning messages array + * + * @return array + */ + public function getWarningMessagesArray() + { + $warning_messages = []; + foreach ($this->dbi->getWarnings() as $warning) { + // In MariaDB 5.1.44, when altering a table from Maria to MyISAM + // and if TRANSACTIONAL was set, the system reports an error; + // I discussed with a Maria developer and he agrees that this + // should not be reported with a Level of Error, so here + // I just ignore it. But there are other 1478 messages + // that it's better to show. + if (! (isset($_POST['new_tbl_storage_engine']) + && $_POST['new_tbl_storage_engine'] == 'MyISAM' + && $warning['Code'] == '1478' + && $warning['Level'] == 'Error') + ) { + $warning_messages[] = $warning['Level'] . ': #' . $warning['Code'] + . ' ' . $warning['Message']; + } + } + return $warning_messages; + } + + /** + * Get SQL query and result after ran this SQL query for a partition operation + * has been requested by the user + * + * @return array $sql_query, $result + */ + public function getQueryAndResultForPartition() + { + $sql_query = 'ALTER TABLE ' + . Util::backquote($GLOBALS['table']) . ' ' + . $_POST['partition_operation'] + . ' PARTITION '; + + if ($_POST['partition_operation'] == 'COALESCE') { + $sql_query .= count($_POST['partition_name']); + } else { + $sql_query .= implode(', ', $_POST['partition_name']) . ';'; + } + + $result = $this->dbi->query($sql_query); + + return [ + $sql_query, + $result, + ]; + } + + /** + * Adjust the privileges after renaming/moving a table + * + * @param string $oldDb Database name before table renaming/moving table + * @param string $oldTable Table name before table renaming/moving table + * @param string $newDb Database name after table renaming/ moving table + * @param string $newTable Table name after table renaming/moving table + * + * @return void + */ + public function adjustPrivilegesRenameOrMoveTable($oldDb, $oldTable, $newDb, $newTable) + { + if ($GLOBALS['table_priv'] && $GLOBALS['col_priv'] + && $GLOBALS['is_reload_priv'] + ) { + $this->dbi->selectDb('mysql'); + + // For table specific privileges + $query_table_specific = 'UPDATE ' . Util::backquote('tables_priv') + . 'SET Db = \'' . $this->dbi->escapeString($newDb) . '\', Table_name = \'' . $this->dbi->escapeString($newTable) + . '\' where Db = \'' . $this->dbi->escapeString($oldDb) . '\' AND Table_name = \'' . $this->dbi->escapeString($oldTable) + . '\';'; + $this->dbi->query($query_table_specific); + + // For column specific privileges + $query_col_specific = 'UPDATE ' . Util::backquote('columns_priv') + . 'SET Db = \'' . $this->dbi->escapeString($newDb) . '\', Table_name = \'' . $this->dbi->escapeString($newTable) + . '\' where Db = \'' . $this->dbi->escapeString($oldDb) . '\' AND Table_name = \'' . $this->dbi->escapeString($oldTable) + . '\';'; + $this->dbi->query($query_col_specific); + + // Finally FLUSH the new privileges + $flush_query = "FLUSH PRIVILEGES;"; + $this->dbi->query($flush_query); + } + } + + /** + * Adjust the privileges after copying a table + * + * @param string $oldDb Database name before table copying + * @param string $oldTable Table name before table copying + * @param string $newDb Database name after table copying + * @param string $newTable Table name after table copying + * + * @return void + */ + public function adjustPrivilegesCopyTable($oldDb, $oldTable, $newDb, $newTable) + { + if ($GLOBALS['table_priv'] && $GLOBALS['col_priv'] + && $GLOBALS['is_reload_priv'] + ) { + $this->dbi->selectDb('mysql'); + + // For Table Specific privileges + $query_table_specific_old = 'SELECT * FROM ' + . Util::backquote('tables_priv') . ' where ' + . 'Db = "' . $oldDb . '" AND Table_name = "' . $oldTable . '";'; + + $old_privs_table = $this->dbi->fetchResult( + $query_table_specific_old, + 0 + ); + + foreach ($old_privs_table as $old_priv) { + $newDb_table_privs_query = 'INSERT INTO ' + . Util::backquote('tables_priv') . ' VALUES("' + . $old_priv[0] . '", "' . $newDb . '", "' . $old_priv[2] . '", "' + . $newTable . '", "' . $old_priv[4] . '", "' . $old_priv[5] + . '", "' . $old_priv[6] . '", "' . $old_priv[7] . '");'; + + $this->dbi->query($newDb_table_privs_query); + } + + // For Column Specific privileges + $query_col_specific_old = 'SELECT * FROM ' + . Util::backquote('columns_priv') . ' WHERE ' + . 'Db = "' . $oldDb . '" AND Table_name = "' . $oldTable . '";'; + + $old_privs_col = $this->dbi->fetchResult( + $query_col_specific_old, + 0 + ); + + foreach ($old_privs_col as $old_priv) { + $newDb_col_privs_query = 'INSERT INTO ' + . Util::backquote('columns_priv') . ' VALUES("' + . $old_priv[0] . '", "' . $newDb . '", "' . $old_priv[2] . '", "' + . $newTable . '", "' . $old_priv[4] . '", "' . $old_priv[5] + . '", "' . $old_priv[6] . '");'; + + $this->dbi->query($newDb_col_privs_query); + } + + // Finally FLUSH the new privileges + $flush_query = "FLUSH PRIVILEGES;"; + $this->dbi->query($flush_query); + } + } + + /** + * Change all collations and character sets of all columns in table + * + * @param string $db Database name + * @param string $table Table name + * @param string $tbl_collation Collation Name + * + * @return void + */ + public function changeAllColumnsCollation($db, $table, $tbl_collation) + { + $this->dbi->selectDb($db); + + $change_all_collations_query = 'ALTER TABLE ' + . Util::backquote($table) + . ' CONVERT TO'; + + list($charset) = explode('_', $tbl_collation); + + $change_all_collations_query .= ' CHARACTER SET ' . $charset + . ($charset == $tbl_collation ? '' : ' COLLATE ' . $tbl_collation); + + $this->dbi->query($change_all_collations_query); + } + + /** + * Move or copy a table + * + * @param string $db current database name + * @param string $table current table name + * + * @return void + */ + public function moveOrCopyTable($db, $table) + { + /** + * Selects the database to work with + */ + $this->dbi->selectDb($db); + + /** + * $_POST['target_db'] could be empty in case we came from an input field + * (when there are many databases, no drop-down) + */ + if (empty($_POST['target_db'])) { + $_POST['target_db'] = $db; + } + + /** + * A target table name has been sent to this script -> do the work + */ + if (Core::isValid($_POST['new_name'])) { + if ($db == $_POST['target_db'] && $table == $_POST['new_name']) { + if (isset($_POST['submit_move'])) { + $message = Message::error(__('Can\'t move table to same one!')); + } else { + $message = Message::error(__('Can\'t copy table to same one!')); + } + } else { + Table::moveCopy( + $db, + $table, + $_POST['target_db'], + $_POST['new_name'], + $_POST['what'], + isset($_POST['submit_move']), + 'one_table' + ); + + if (isset($_POST['adjust_privileges']) + && ! empty($_POST['adjust_privileges']) + ) { + if (isset($_POST['submit_move'])) { + $this->adjustPrivilegesRenameOrMoveTable( + $db, + $table, + $_POST['target_db'], + $_POST['new_name'] + ); + } else { + $this->adjustPrivilegesCopyTable( + $db, + $table, + $_POST['target_db'], + $_POST['new_name'] + ); + } + + if (isset($_POST['submit_move'])) { + $message = Message::success( + __( + 'Table %s has been moved to %s. Privileges have been ' + . 'adjusted.' + ) + ); + } else { + $message = Message::success( + __( + 'Table %s has been copied to %s. Privileges have been ' + . 'adjusted.' + ) + ); + } + } else { + if (isset($_POST['submit_move'])) { + $message = Message::success( + __('Table %s has been moved to %s.') + ); + } else { + $message = Message::success( + __('Table %s has been copied to %s.') + ); + } + } + + $old = Util::backquote($db) . '.' + . Util::backquote($table); + $message->addParam($old); + + $new_name = $_POST['new_name']; + if ($this->dbi->getLowerCaseNames() === '1') { + $new_name = strtolower($new_name); + } + + $GLOBALS['table'] = $new_name; + + $new = Util::backquote($_POST['target_db']) . '.' + . Util::backquote($new_name); + $message->addParam($new); + } + } else { + /** + * No new name for the table! + */ + $message = Message::error(__('The table name is empty!')); + } + + $response = Response::getInstance(); + if ($response->isAjax()) { + $response->addJSON('message', $message); + if ($message->isSuccess()) { + $response->addJSON('db', $GLOBALS['db']); + } else { + $response->setRequestStatus(false); + } + exit; + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/OutputBuffering.php b/srcs/phpmyadmin/libraries/classes/OutputBuffering.php new file mode 100644 index 0000000..7714976 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/OutputBuffering.php @@ -0,0 +1,144 @@ +_mode = $this->_getMode(); + $this->_on = false; + } + + /** + * This function could be used eventually to support more modes. + * + * @return integer the output buffer mode + */ + private function _getMode() + { + $mode = 0; + if ($GLOBALS['cfg']['OBGzip'] && function_exists('ob_start')) { + if (ini_get('output_handler') == 'ob_gzhandler') { + // If a user sets the output_handler in php.ini to ob_gzhandler, then + // any right frame file in phpMyAdmin will not be handled properly by + // the browser. My fix was to check the ini file within the + // PMA_outBufferModeGet() function. + $mode = 0; + } elseif (function_exists('ob_get_level') && ob_get_level() > 0) { + // happens when php.ini's output_buffering is not Off + ob_end_clean(); + $mode = 1; + } else { + $mode = 1; + } + } + // Zero (0) is no mode or in other words output buffering is OFF. + // Follow 2^0, 2^1, 2^2, 2^3 type values for the modes. + // Useful if we ever decide to combine modes. Then a bitmask field of + // the sum of all modes will be the natural choice. + return $mode; + } + + /** + * Returns the singleton OutputBuffering object + * + * @return OutputBuffering object + */ + public static function getInstance() + { + if (empty(self::$_instance)) { + self::$_instance = new OutputBuffering(); + } + return self::$_instance; + } + + /** + * This function will need to run at the top of all pages if output + * output buffering is turned on. It also needs to be passed $mode from + * the PMA_outBufferModeGet() function or it will be useless. + * + * @return void + */ + public function start() + { + if (! $this->_on) { + if ($this->_mode && function_exists('ob_gzhandler')) { + ob_start('ob_gzhandler'); + } + ob_start(); + if (! defined('TESTSUITE')) { + header('X-ob_mode: ' . $this->_mode); + } + register_shutdown_function( + [ + OutputBuffering::class, + 'stop', + ] + ); + $this->_on = true; + } + } + + /** + * This function will need to run at the bottom of all pages if output + * buffering is turned on. It also needs to be passed $mode from the + * PMA_outBufferModeGet() function or it will be useless. + * + * @return void + */ + public static function stop() + { + $buffer = OutputBuffering::getInstance(); + if ($buffer->_on) { + $buffer->_on = false; + $buffer->_content = ob_get_contents(); + if (ob_get_length() > 0) { + ob_end_clean(); + } + } + } + + /** + * Gets buffer content + * + * @return string buffer content + */ + public function getContents() + { + return $this->_content; + } + + /** + * Flushes output buffer + * + * @return void + */ + public function flush() + { + if (ob_get_status() && $this->_mode) { + ob_flush(); + } else { + flush(); + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/ParseAnalyze.php b/srcs/phpmyadmin/libraries/classes/ParseAnalyze.php new file mode 100644 index 0000000..8fa8ff2 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/ParseAnalyze.php @@ -0,0 +1,84 @@ + 1) { + + /** + * @todo if there are more than one table name in the Select: + * - do not extract the first table name + * - do not show a table name in the page header + * - do not display the sub-pages links) + */ + $table = ''; + } else { + $table = $analyzed_sql_results['select_tables'][0][0]; + if (! empty($analyzed_sql_results['select_tables'][0][1])) { + $db = $analyzed_sql_results['select_tables'][0][1]; + } + } + // There is no point checking if a reload is required if we already decided + // to reload. Also, no reload is required for AJAX requests. + $response = Response::getInstance(); + if (empty($reload) && ! $response->isAjax()) { + // NOTE: Database names are case-insensitive. + $reload = strcasecmp($db, $prev_db) != 0; + } + + // Updating the array. + $analyzed_sql_results['reload'] = $reload; + } + + return [ + $analyzed_sql_results, + $db, + $table, + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Partition.php b/srcs/phpmyadmin/libraries/classes/Partition.php new file mode 100644 index 0000000..6727fad --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Partition.php @@ -0,0 +1,270 @@ +name = $row['PARTITION_NAME']; + $this->ordinal = $row['PARTITION_ORDINAL_POSITION']; + $this->method = $row['PARTITION_METHOD']; + $this->expression = $row['PARTITION_EXPRESSION']; + $this->description = $row['PARTITION_DESCRIPTION']; + // no sub partitions, load all data to this object + if (empty($row['SUBPARTITION_NAME'])) { + $this->loadCommonData($row); + } + } + + /** + * Returns the partiotion description + * + * @return string partition description + */ + public function getDescription() + { + return $this->description; + } + + /** + * Add a sub partition + * + * @param SubPartition $partition Sub partition + * + * @return void + */ + public function addSubPartition(SubPartition $partition) + { + $this->subPartitions[] = $partition; + } + + /** + * Whether there are sub partitions + * + * @return boolean + */ + public function hasSubPartitions() + { + return ! empty($this->subPartitions); + } + + /** + * Returns the number of data rows + * + * @return integer number of rows + */ + public function getRows() + { + if (empty($this->subPartitions)) { + return $this->rows; + } + + $rows = 0; + foreach ($this->subPartitions as $subPartition) { + $rows += $subPartition->rows; + } + return $rows; + } + + /** + * Returns the total data length + * + * @return integer data length + */ + public function getDataLength() + { + if (empty($this->subPartitions)) { + return $this->dataLength; + } + + $dataLength = 0; + foreach ($this->subPartitions as $subPartition) { + $dataLength += $subPartition->dataLength; + } + return $dataLength; + } + + /** + * Returns the tatal index length + * + * @return integer index length + */ + public function getIndexLength() + { + if (empty($this->subPartitions)) { + return $this->indexLength; + } + + $indexLength = 0; + foreach ($this->subPartitions as $subPartition) { + $indexLength += $subPartition->indexLength; + } + return $indexLength; + } + + /** + * Returns the list of sub partitions + * + * @return SubPartition[] + */ + public function getSubPartitions() + { + return $this->subPartitions; + } + + /** + * Returns array of partitions for a specific db/table + * + * @param string $db database name + * @param string $table table name + * + * @access public + * @return Partition[] + */ + public static function getPartitions($db, $table) + { + if (Partition::havePartitioning()) { + $result = $GLOBALS['dbi']->fetchResult( + "SELECT * FROM `information_schema`.`PARTITIONS`" + . " WHERE `TABLE_SCHEMA` = '" . $GLOBALS['dbi']->escapeString($db) + . "' AND `TABLE_NAME` = '" . $GLOBALS['dbi']->escapeString($table) . "'" + ); + if ($result) { + $partitionMap = []; + foreach ($result as $row) { + if (isset($partitionMap[$row['PARTITION_NAME']])) { + $partition = $partitionMap[$row['PARTITION_NAME']]; + } else { + $partition = new Partition($row); + $partitionMap[$row['PARTITION_NAME']] = $partition; + } + + if (! empty($row['SUBPARTITION_NAME'])) { + $parentPartition = $partition; + $partition = new SubPartition($row); + $parentPartition->addSubPartition($partition); + } + } + return array_values($partitionMap); + } + return []; + } + + return []; + } + + /** + * returns array of partition names for a specific db/table + * + * @param string $db database name + * @param string $table table name + * + * @access public + * @return array of partition names + */ + public static function getPartitionNames($db, $table) + { + if (Partition::havePartitioning()) { + return $GLOBALS['dbi']->fetchResult( + "SELECT DISTINCT `PARTITION_NAME` FROM `information_schema`.`PARTITIONS`" + . " WHERE `TABLE_SCHEMA` = '" . $GLOBALS['dbi']->escapeString($db) + . "' AND `TABLE_NAME` = '" . $GLOBALS['dbi']->escapeString($table) . "'" + ); + } + + return []; + } + + /** + * returns the partition method used by the table. + * + * @param string $db database name + * @param string $table table name + * + * @return string|null partition method + */ + public static function getPartitionMethod($db, $table) + { + if (Partition::havePartitioning()) { + $partition_method = $GLOBALS['dbi']->fetchResult( + "SELECT `PARTITION_METHOD` FROM `information_schema`.`PARTITIONS`" + . " WHERE `TABLE_SCHEMA` = '" . $GLOBALS['dbi']->escapeString($db) . "'" + . " AND `TABLE_NAME` = '" . $GLOBALS['dbi']->escapeString($table) . "'" + . " LIMIT 1" + ); + if (! empty($partition_method)) { + return $partition_method[0]; + } + } + return null; + } + + /** + * checks if MySQL server supports partitioning + * + * @static + * @staticvar boolean $have_partitioning + * @staticvar boolean $already_checked + * @access public + * @return boolean + */ + public static function havePartitioning() + { + static $have_partitioning = false; + static $already_checked = false; + + if (! $already_checked) { + if ($GLOBALS['dbi']->getVersion() < 50600) { + if ($GLOBALS['dbi']->fetchValue( + "SELECT @@have_partitioning;" + )) { + $have_partitioning = true; + } + } elseif ($GLOBALS['dbi']->getVersion() >= 80000) { + $have_partitioning = true; + } else { + // see https://dev.mysql.com/doc/refman/5.6/en/partitioning.html + $plugins = $GLOBALS['dbi']->fetchResult("SHOW PLUGINS"); + foreach ($plugins as $value) { + if ($value['Name'] == 'partition') { + $have_partitioning = true; + break; + } + } + } + $already_checked = true; + } + return $have_partitioning; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Pdf.php b/srcs/phpmyadmin/libraries/classes/Pdf.php new file mode 100644 index 0000000..3dacdb5 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Pdf.php @@ -0,0 +1,178 @@ +SetAuthor('phpMyAdmin ' . PMA_VERSION); + $this->AddFont('DejaVuSans', '', 'dejavusans.php'); + $this->AddFont('DejaVuSans', 'B', 'dejavusansb.php'); + $this->SetFont(Pdf::PMA_PDF_FONT, '', 14); + $this->setFooterFont([Pdf::PMA_PDF_FONT, '', 14]); + } + + /** + * This function must be named "Footer" to work with the TCPDF library + * + * @return void + */ + // @codingStandardsIgnoreLine + public function Footer() + { + // Check if footer for this page already exists + if (! isset($this->footerset[$this->page])) { + $this->SetY(-15); + $this->SetFont(Pdf::PMA_PDF_FONT, '', 14); + $this->Cell( + 0, + 6, + __('Page number:') . ' ' + . $this->getAliasNumPage() . '/' . $this->getAliasNbPages(), + 'T', + 0, + 'C' + ); + $this->Cell(0, 6, Util::localisedDate(), 0, 1, 'R'); + $this->SetY(20); + + // set footerset + $this->footerset[$this->page] = 1; + } + } + + /** + * Function to set alias which will be expanded on page rendering. + * + * @param string $name name of the alias + * @param string $value value of the alias + * + * @return void + */ + public function setAlias($name, $value) + { + $name = TCPDF_FONTS::UTF8ToUTF16BE( + $name, + false, + true, + $this->CurrentFont + ); + $this->Alias[$name] = TCPDF_FONTS::UTF8ToUTF16BE( + $value, + false, + true, + $this->CurrentFont + ); + } + + /** + * Improved with alias expanding. + * + * @return void + */ + public function _putpages() + { + if (count($this->Alias) > 0) { + $nbPages = count($this->pages); + for ($n = 1; $n <= $nbPages; $n++) { + $this->pages[$n] = strtr($this->pages[$n], $this->Alias); + } + } + parent::_putpages(); + } + + /** + * Displays an error message + * + * @param string $error_message the error message + * + * @return void + */ + // @codingStandardsIgnoreLine + public function Error($error_message = '') + { + Message::error( + __('Error while creating PDF:') . ' ' . $error_message + )->display(); + exit; + } + + /** + * Sends file as a download to user. + * + * @param string $filename file name + * + * @return void + */ + public function download($filename) + { + $pdfData = $this->getPDFData(); + Response::getInstance()->disable(); + Core::downloadHeader( + $filename, + 'application/pdf', + strlen($pdfData) + ); + echo $pdfData; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins.php b/srcs/phpmyadmin/libraries/classes/Plugins.php new file mode 100644 index 0000000..cd6f6c0 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins.php @@ -0,0 +1,633 @@ +getProperties()) { + $plugin_list[] = $plugin; + } + } + } + } + + usort($plugin_list, function ($cmp_name_1, $cmp_name_2) { + return strcasecmp( + $cmp_name_1->getProperties()->getText(), + $cmp_name_2->getProperties()->getText() + ); + }); + return $plugin_list; + } + + /** + * Returns locale string for $name or $name if no locale is found + * + * @param string $name for local string + * + * @return string locale string for $name + */ + public static function getString($name) + { + return isset($GLOBALS[$name]) ? $GLOBALS[$name] : $name; + } + + /** + * Returns html input tag option 'checked' if plugin $opt + * should be set by config or request + * + * @param string $section name of config section in + * $GLOBALS['cfg'][$section] for plugin + * @param string $opt name of option + * + * @return string html input tag option 'checked' + */ + public static function checkboxCheck($section, $opt) + { + // If the form is being repopulated using $_GET data, that is priority + if (isset($_GET[$opt]) + || ! isset($_GET['repopulate']) + && ((! empty($GLOBALS['timeout_passed']) && isset($_REQUEST[$opt])) + || ! empty($GLOBALS['cfg'][$section][$opt])) + ) { + return ' checked="checked"'; + } + return ''; + } + + /** + * Returns default value for option $opt + * + * @param string $section name of config section in + * $GLOBALS['cfg'][$section] for plugin + * @param string $opt name of option + * + * @return string default value for option $opt + */ + public static function getDefault($section, $opt) + { + if (isset($_GET[$opt])) { + // If the form is being repopulated using $_GET data, that is priority + return htmlspecialchars($_GET[$opt]); + } + + if (isset($GLOBALS['timeout_passed']) + && $GLOBALS['timeout_passed'] + && isset($_REQUEST[$opt]) + ) { + return htmlspecialchars($_REQUEST[$opt]); + } + + if (! isset($GLOBALS['cfg'][$section][$opt])) { + return ''; + } + + $matches = []; + /* Possibly replace localised texts */ + if (! preg_match_all( + '/(str[A-Z][A-Za-z0-9]*)/', + (string) $GLOBALS['cfg'][$section][$opt], + $matches + )) { + return htmlspecialchars((string) $GLOBALS['cfg'][$section][$opt]); + } + + $val = $GLOBALS['cfg'][$section][$opt]; + foreach ($matches[0] as $match) { + if (isset($GLOBALS[$match])) { + $val = str_replace($match, $GLOBALS[$match], $val); + } + } + return htmlspecialchars($val); + } + + /** + * Returns html select form element for plugin choice + * and hidden fields denoting whether each plugin must be exported as a file + * + * @param string $section name of config section in + * $GLOBALS['cfg'][$section] for plugin + * @param string $name name of select element + * @param array $list array with plugin instances + * @param string $cfgname name of config value, if none same as $name + * + * @return string html select tag + */ + public static function getChoice($section, $name, array $list, $cfgname = null) + { + if (! isset($cfgname)) { + $cfgname = $name; + } + $ret = '' . "\n"; + } + $ret .= '' . "\n" . $hidden; + + return $ret; + } + + /** + * Returns single option in a list element + * + * @param string $section name of config section in $GLOBALS['cfg'][$section] for plugin + * @param string $plugin_name unique plugin name + * @param OptionsPropertyItem $propertyGroup options property main group instance + * @param boolean $is_subgroup if this group is a subgroup + * + * @return string table row with option + */ + public static function getOneOption( + $section, + $plugin_name, + &$propertyGroup, + $is_subgroup = false + ) { + $ret = "\n"; + + $properties = null; + if (! $is_subgroup) { + // for subgroup headers + if (mb_strpos(get_class($propertyGroup), "PropertyItem")) { + $properties = [$propertyGroup]; + } else { + // for main groups + $ret .= '
    '; + + $text = null; + if (method_exists($propertyGroup, 'getText')) { + $text = $propertyGroup->getText(); + } + + if ($text != null) { + $ret .= '

    ' . self::getString($text) . '

    '; + } + $ret .= '
      '; + } + } + + if (! isset($properties)) { + $not_subgroup_header = true; + if (method_exists($propertyGroup, 'getProperties')) { + $properties = $propertyGroup->getProperties(); + } + } + + if (isset($properties)) { + /** @var OptionsPropertySubgroup $propertyItem */ + foreach ($properties as $propertyItem) { + $property_class = get_class($propertyItem); + // if the property is a subgroup, we deal with it recursively + if (mb_strpos($property_class, "Subgroup")) { + // for subgroups + // each subgroup can have a header, which may also be a form element + /** @var OptionsPropertyItem $subgroup_header */ + $subgroup_header = $propertyItem->getSubgroupHeader(); + if ($subgroup_header !== null) { + $ret .= self::getOneOption( + $section, + $plugin_name, + $subgroup_header + ); + } + + $ret .= '
    • getName() . '">'; + } else { + $ret .= '>'; + } + + $ret .= self::getOneOption( + $section, + $plugin_name, + $propertyItem, + true + ); + continue; + } + + // single property item + $ret .= self::getHtmlForProperty( + $section, + $plugin_name, + $propertyItem + ); + } + } + + if ($is_subgroup) { + // end subgroup + $ret .= '
    '; + } else { + // end main group + if (! empty($not_subgroup_header)) { + $ret .= '
    '; + } + } + + if (method_exists($propertyGroup, "getDoc")) { + $doc = $propertyGroup->getDoc(); + if ($doc != null) { + if (count($doc) === 3) { + $ret .= Util::showMySQLDocu( + $doc[1], + false, + null, + null, + $doc[2] + ); + } elseif (count($doc) === 1) { + $ret .= Util::showDocu('faq', $doc[0]); + } else { + $ret .= Util::showMySQLDocu( + $doc[1] + ); + } + } + } + + // Close the list element after $doc link is displayed + if (isset($property_class)) { + if ($property_class == 'PhpMyAdmin\Properties\Options\Items\BoolPropertyItem' + || $property_class == 'PhpMyAdmin\Properties\Options\Items\MessageOnlyPropertyItem' + || $property_class == 'PhpMyAdmin\Properties\Options\Items\SelectPropertyItem' + || $property_class == 'PhpMyAdmin\Properties\Options\Items\TextPropertyItem' + ) { + $ret .= ''; + } + } + $ret .= "\n"; + return $ret; + } + + /** + * Get HTML for properties items + * + * @param string $section name of config section in + * $GLOBALS['cfg'][$section] for plugin + * @param string $plugin_name unique plugin name + * @param OptionsPropertyItem $propertyItem Property item + * + * @return string + */ + public static function getHtmlForProperty( + $section, + $plugin_name, + $propertyItem + ) { + $ret = null; + $property_class = get_class($propertyItem); + switch ($property_class) { + case BoolPropertyItem::class: + $ret .= '
  • ' . "\n"; + $ret .= 'getName() + ); + + if ($propertyItem->getForce() != null) { + // Same code is also few lines lower, update both if needed + $ret .= ' onclick="if (!this.checked && ' + . '(!document.getElementById(\'checkbox_' . $plugin_name + . '_' . $propertyItem->getForce() . '\') ' + . '|| !document.getElementById(\'checkbox_' + . $plugin_name . '_' . $propertyItem->getForce() + . '\').checked)) ' + . 'return false; else return true;"'; + } + $ret .= '>'; + $ret .= ''; + break; + case DocPropertyItem::class: + echo DocPropertyItem::class; + break; + case HiddenPropertyItem::class: + $ret .= '
  • '; + break; + case MessageOnlyPropertyItem::class: + $ret .= '
  • ' . "\n"; + $ret .= '

    ' . self::getString($propertyItem->getText()) . '

    '; + break; + case RadioPropertyItem::class: + /** + * @var RadioPropertyItem $pitem + */ + $pitem = $propertyItem; + + $default = self::getDefault( + $section, + $plugin_name . '_' . $pitem->getName() + ); + + foreach ($pitem->getValues() as $key => $val) { + $ret .= '
  • getName() . '_' . $key . '">' + . self::getString($val) . '
  • '; + } + break; + case SelectPropertyItem::class: + /** + * @var SelectPropertyItem $pitem + */ + $pitem = $propertyItem; + $ret .= '
  • ' . "\n"; + $ret .= ''; + $ret .= ''; + break; + case TextPropertyItem::class: + /** + * @var TextPropertyItem $pitem + */ + $pitem = $propertyItem; + $ret .= '
  • ' . "\n"; + $ret .= ''; + $ret .= 'getSize() != null + ? ' size="' . $pitem->getSize() . '"' + : '') + . ($pitem->getLen() != null + ? ' maxlength="' . $pitem->getLen() . '"' + : '') + . '>'; + break; + case NumberPropertyItem::class: + $ret .= '
  • ' . "\n"; + $ret .= ''; + $ret .= ''; + break; + default: + break; + } + return $ret; + } + + /** + * Returns html div with editable options for plugin + * + * @param string $section name of config section in $GLOBALS['cfg'][$section] + * @param array $list array with plugin instances + * + * @return string html fieldset with plugin options + */ + public static function getOptions($section, array $list) + { + $ret = ''; + // Options for plugins that support them + foreach ($list as $plugin) { + $properties = $plugin->getProperties(); + $text = null; + $options = null; + if ($properties != null) { + $text = $properties->getText(); + $options = $properties->getOptions(); + } + + $elem = explode('\\', get_class($plugin)); + $plugin_name = array_pop($elem); + unset($elem); + $plugin_name = mb_strtolower( + mb_substr( + $plugin_name, + mb_strlen($section) + ) + ); + + $ret .= '
    '; + $ret .= '

    ' . self::getString($text) . '

    '; + + $no_options = true; + if ($options !== null && count($options) > 0) { + foreach ($options->getProperties() as $propertyMainGroup) { + // check for hidden properties + $no_options = true; + foreach ($propertyMainGroup->getProperties() as $propertyItem) { + if (strcmp(HiddenPropertyItem::class, get_class($propertyItem))) { + $no_options = false; + break; + } + } + + $ret .= self::getOneOption( + $section, + $plugin_name, + $propertyMainGroup + ); + } + } + + if ($no_options) { + $ret .= '

    ' . __('This format has no options') . '

    '; + } + $ret .= '
    '; + } + return $ret; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationConfig.php b/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationConfig.php new file mode 100644 index 0000000..7ebd1ae --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationConfig.php @@ -0,0 +1,172 @@ +isAjax()) { + $response->setRequestStatus(false); + // reload_flag removes the token parameter from the URL and reloads + $response->addJSON('reload_flag', '1'); + if (defined('TESTSUITE')) { + return true; + } else { + exit; + } + } + + return true; + } + + /** + * Gets authentication credentials + * + * @return boolean always true + */ + public function readCredentials() + { + if ($GLOBALS['token_provided'] && $GLOBALS['token_mismatch']) { + return false; + } + + $this->user = $GLOBALS['cfg']['Server']['user']; + $this->password = $GLOBALS['cfg']['Server']['password']; + + return true; + } + + /** + * User is not allowed to login to MySQL -> authentication failed + * + * @param string $failure String describing why authentication has failed + * + * @return void + */ + public function showFailure($failure) + { + parent::showFailure($failure); + $conn_error = $GLOBALS['dbi']->getError(); + if (! $conn_error) { + $conn_error = __('Cannot connect: invalid settings.'); + } + + /* HTML header */ + $response = Response::getInstance(); + $response->getFooter() + ->setMinimal(); + $header = $response->getHeader(); + $header->setBodyId('loginform'); + $header->setTitle(__('Access denied!')); + $header->disableMenuAndConsole(); + echo '

    +
    +

    '; + echo sprintf(__('Welcome to %s'), ' phpMyAdmin '); + echo '

    +
    +
    + + + + + + + ' , "\n"; + if (count($GLOBALS['cfg']['Servers']) > 1) { + // offer a chance to login to other servers if the current one failed + echo '' , "\n"; + echo ' ' , "\n"; + echo '' , "\n"; + } + echo '
    '; + if (isset($GLOBALS['allowDeny_forbidden']) + && $GLOBALS['allowDeny_forbidden'] + ) { + trigger_error(__('Access denied!'), E_USER_NOTICE); + } else { + // Check whether user has configured something + if ($GLOBALS['PMA_Config']->source_mtime == 0) { + echo '

    ' , sprintf( + __( + 'You probably did not create a configuration file.' + . ' You might want to use the %1$ssetup script%2$s to' + . ' create one.' + ), + '', + '' + ) , '

    ' , "\n"; + } elseif (! isset($GLOBALS['errno']) + || (isset($GLOBALS['errno']) && $GLOBALS['errno'] != 2002) + && $GLOBALS['errno'] != 2003 + ) { + // if we display the "Server not responding" error, do not confuse + // users by telling them they have a settings problem + // (note: it's true that they could have a badly typed host name, + // but anyway the current message tells that the server + // rejected the connection, which is not really what happened) + // 2002 is the error given by mysqli + // 2003 is the error given by mysql + trigger_error( + __( + 'phpMyAdmin tried to connect to the MySQL server, and the' + . ' server rejected the connection. You should check the' + . ' host, username and password in your configuration and' + . ' make sure that they correspond to the information given' + . ' by the administrator of the MySQL server.' + ), + E_USER_WARNING + ); + } + echo Util::mysqlDie( + $conn_error, + '', + true, + '', + false + ); + } + $GLOBALS['error_handler']->dispUserErrors(); + echo '
    ' , "\n"; + echo '' + , __('Retry to connect') + , '' , "\n"; + echo '
    ' , "\n"; + echo Select::render(true, true); + echo '
    ' , "\n"; + if (! defined('TESTSUITE')) { + exit; + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationCookie.php b/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationCookie.php new file mode 100644 index 0000000..7a794d0 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationCookie.php @@ -0,0 +1,964 @@ +_use_openssl = ! class_exists(Random::class); + } + + /** + * Forces (not)using of openSSL + * + * @param boolean $use The flag + * + * @return void + */ + public function setUseOpenSSL($use) + { + $this->_use_openssl = $use; + } + + /** + * Displays authentication form + * + * this function MUST exit/quit the application + * + * @global string $conn_error the last connection error + * + * @return boolean|void + */ + public function showLoginForm() + { + global $conn_error; + + $response = Response::getInstance(); + + // When sending login modal after session has expired, send the new token explicitly with the response to update the token in all the forms having a hidden token. + $session_expired = isset($_REQUEST['check_timeout']) || isset($_REQUEST['session_timedout']); + if (! $session_expired && $response->loginPage()) { + if (defined('TESTSUITE')) { + return true; + } else { + exit; + } + } + + // When sending login modal after session has expired, send the new token explicitly with the response to update the token in all the forms having a hidden token. + if ($session_expired) { + $response->setRequestStatus(false); + $response->addJSON( + 'new_token', + $_SESSION[' PMA_token '] + ); + } + + // logged_in response parameter is used to check if the login, using the modal was successful after session expiration + if (isset($_REQUEST['session_timedout'])) { + $response->addJSON( + 'logged_in', + 0 + ); + } + + // No recall if blowfish secret is not configured as it would produce + // garbage + if ($GLOBALS['cfg']['LoginCookieRecall'] + && ! empty($GLOBALS['cfg']['blowfish_secret']) + ) { + $default_user = $this->user; + $default_server = $GLOBALS['pma_auth_server']; + $autocomplete = ''; + } else { + $default_user = ''; + $default_server = ''; + // skip the IE autocomplete feature. + $autocomplete = ' autocomplete="off"'; + } + + // wrap the login form in a div which overlays the whole page. + if ($session_expired) { + echo $this->template->render('login/header', [ + 'theme' => $GLOBALS['PMA_Theme'], + 'add_class' => ' modal_form', + 'session_expired' => 1, + ]); + } else { + echo $this->template->render('login/header', [ + 'theme' => $GLOBALS['PMA_Theme'], + 'add_class' => '', + 'session_expired' => 0, + ]); + } + + if ($GLOBALS['cfg']['DBG']['demo']) { + echo '
    '; + echo '' , __('phpMyAdmin Demo Server') , ''; + printf( + __( + 'You are using the demo server. You can do anything here, but ' + . 'please do not change root, debian-sys-maint and pma users. ' + . 'More information is available at %s.' + ), + 'demo.phpmyadmin.net' + ); + echo '
    '; + } + + // Show error message + if (! empty($conn_error)) { + Message::rawError((string) $conn_error)->display(); + } elseif (isset($_GET['session_expired']) + && intval($_GET['session_expired']) == 1 + ) { + Message::rawError( + __('Your session has expired. Please log in again.') + )->display(); + } + + // Displays the languages form + $language_manager = LanguageManager::getInstance(); + if (empty($GLOBALS['cfg']['Lang']) && $language_manager->hasChoice()) { + echo "
    "; + // use fieldset, don't show doc link + echo $language_manager->getSelectorDisplay(new Template(), true, false); + echo '
    '; + } + echo ' +
    + +
    +
    + '; + echo ''; + + // Add a hidden element session_timedout which is used to check if the user requested login after session expiration + if ($session_expired) { + echo ''; + } + echo __('Log in'); + echo Util::showDocu('index'); + echo ''; + if ($GLOBALS['cfg']['AllowArbitraryServer']) { + echo ' +
    + + +
    '; + } + echo '
    + + +
    +
    + + +
    '; + if (count($GLOBALS['cfg']['Servers']) > 1) { + echo '
    + +
    '; + } else { + echo ' '; + } // end if (server choice) + + echo '
    '; + + // binds input field with invisible reCaptcha if enabled + if (empty($GLOBALS['cfg']['CaptchaLoginPrivateKey']) + && empty($GLOBALS['cfg']['CaptchaLoginPublicKey']) + ) { + echo ''; + } else { + echo ''; + echo ''; + } + $_form_params = []; + if (! empty($GLOBALS['target'])) { + $_form_params['target'] = $GLOBALS['target']; + } + if (strlen($GLOBALS['db'])) { + $_form_params['db'] = $GLOBALS['db']; + } + if (strlen($GLOBALS['table'])) { + $_form_params['table'] = $GLOBALS['table']; + } + // do not generate a "server" hidden field as we want the "server" + // drop-down to have priority + echo Url::getHiddenInputs($_form_params, '', 0, 'server'); + echo '
    +
    '; + + if ($GLOBALS['error_handler']->hasDisplayErrors()) { + echo '
    '; + $GLOBALS['error_handler']->dispErrors(); + echo '
    '; + } + + // close the wrapping div tag, if the request is after session timeout + if ($session_expired) { + echo $this->template->render('login/footer', ['session_expired' => 1]); + } else { + echo $this->template->render('login/footer', ['session_expired' => 0]); + } + + echo Config::renderFooter(); + + if (! defined('TESTSUITE')) { + exit; + } else { + return true; + } + } + + /** + * Gets authentication credentials + * + * this function DOES NOT check authentication - it just checks/provides + * authentication credentials required to connect to the MySQL server + * usually with $GLOBALS['dbi']->connect() + * + * it returns false if something is missing - which usually leads to + * showLoginForm() which displays login form + * + * it returns true if all seems ok which usually leads to auth_set_user() + * + * it directly switches to showFailure() if user inactivity timeout is reached + * + * @return boolean whether we get authentication settings or not + */ + public function readCredentials() + { + global $conn_error; + + // Initialization + /** + * @global $GLOBALS['pma_auth_server'] the user provided server to + * connect to + */ + $GLOBALS['pma_auth_server'] = ''; + + $this->user = $this->password = ''; + $GLOBALS['from_cookie'] = false; + + if (isset($_POST['pma_username']) && strlen($_POST['pma_username']) > 0) { + // Verify Captcha if it is required. + if (! empty($GLOBALS['cfg']['CaptchaLoginPrivateKey']) + && ! empty($GLOBALS['cfg']['CaptchaLoginPublicKey']) + ) { + if (! empty($_POST["g-recaptcha-response"])) { + if (function_exists('curl_init')) { + $reCaptcha = new ReCaptcha\ReCaptcha( + $GLOBALS['cfg']['CaptchaLoginPrivateKey'], + new ReCaptcha\RequestMethod\CurlPost() + ); + } elseif (ini_get('allow_url_fopen')) { + $reCaptcha = new ReCaptcha\ReCaptcha( + $GLOBALS['cfg']['CaptchaLoginPrivateKey'], + new ReCaptcha\RequestMethod\Post() + ); + } else { + $reCaptcha = new ReCaptcha\ReCaptcha( + $GLOBALS['cfg']['CaptchaLoginPrivateKey'], + new ReCaptcha\RequestMethod\SocketPost() + ); + } + + // verify captcha status. + $resp = $reCaptcha->verify( + $_POST["g-recaptcha-response"], + Core::getIp() + ); + + // Check if the captcha entered is valid, if not stop the login. + if ($resp == null || ! $resp->isSuccess()) { + $codes = $resp->getErrorCodes(); + + if (in_array('invalid-json', $codes)) { + $conn_error = __('Failed to connect to the reCAPTCHA service!'); + } else { + $conn_error = __('Entered captcha is wrong, try again!'); + } + return false; + } + } else { + $conn_error = __('Missing reCAPTCHA verification, maybe it has been blocked by adblock?'); + return false; + } + } + + // The user just logged in + $this->user = Core::sanitizeMySQLUser($_POST['pma_username']); + $this->password = isset($_POST['pma_password']) ? $_POST['pma_password'] : ''; + if ($GLOBALS['cfg']['AllowArbitraryServer'] + && isset($_REQUEST['pma_servername']) + ) { + if ($GLOBALS['cfg']['ArbitraryServerRegexp']) { + $parts = explode(' ', $_REQUEST['pma_servername']); + if (count($parts) === 2) { + $tmp_host = $parts[0]; + } else { + $tmp_host = $_REQUEST['pma_servername']; + } + + $match = preg_match( + $GLOBALS['cfg']['ArbitraryServerRegexp'], + $tmp_host + ); + if (! $match) { + $conn_error = __( + 'You are not allowed to log in to this MySQL server!' + ); + return false; + } + } + $GLOBALS['pma_auth_server'] = Core::sanitizeMySQLHost($_REQUEST['pma_servername']); + } + /* Secure current session on login to avoid session fixation */ + Session::secure(); + return true; + } + + // At the end, try to set the $this->user + // and $this->password variables from cookies + + // check cookies + $serverCookie = $GLOBALS['PMA_Config']->getCookie('pmaUser-' . $GLOBALS['server']); + if (empty($serverCookie)) { + return false; + } + + $value = $this->cookieDecrypt( + $serverCookie, + $this->_getEncryptionSecret() + ); + + if ($value === false) { + return false; + } + + $this->user = $value; + // user was never logged in since session start + if (empty($_SESSION['browser_access_time'])) { + return false; + } + + // User inactive too long + $last_access_time = time() - $GLOBALS['cfg']['LoginCookieValidity']; + foreach ($_SESSION['browser_access_time'] as $key => $value) { + if ($value < $last_access_time) { + unset($_SESSION['browser_access_time'][$key]); + } + } + // All sessions expired + if (empty($_SESSION['browser_access_time'])) { + Util::cacheUnset('is_create_db_priv'); + Util::cacheUnset('is_reload_priv'); + Util::cacheUnset('db_to_create'); + Util::cacheUnset('dbs_where_create_table_allowed'); + Util::cacheUnset('dbs_to_test'); + Util::cacheUnset('db_priv'); + Util::cacheUnset('col_priv'); + Util::cacheUnset('table_priv'); + Util::cacheUnset('proc_priv'); + + $this->showFailure('no-activity'); + if (! defined('TESTSUITE')) { + exit; + } else { + return false; + } + } + + // check password cookie + $serverCookie = $GLOBALS['PMA_Config']->getCookie('pmaAuth-' . $GLOBALS['server']); + + if (empty($serverCookie)) { + return false; + } + $value = $this->cookieDecrypt( + $serverCookie, + $this->_getSessionEncryptionSecret() + ); + if ($value === false) { + return false; + } + + $auth_data = json_decode($value, true); + + if (! is_array($auth_data) || ! isset($auth_data['password'])) { + return false; + } + $this->password = $auth_data['password']; + if ($GLOBALS['cfg']['AllowArbitraryServer'] && ! empty($auth_data['server'])) { + $GLOBALS['pma_auth_server'] = $auth_data['server']; + } + + $GLOBALS['from_cookie'] = true; + + return true; + } + + /** + * Set the user and password after last checkings if required + * + * @return boolean always true + */ + public function storeCredentials() + { + global $cfg; + + if ($GLOBALS['cfg']['AllowArbitraryServer'] + && ! empty($GLOBALS['pma_auth_server']) + ) { + /* Allow to specify 'host port' */ + $parts = explode(' ', $GLOBALS['pma_auth_server']); + if (count($parts) === 2) { + $tmp_host = $parts[0]; + $tmp_port = $parts[1]; + } else { + $tmp_host = $GLOBALS['pma_auth_server']; + $tmp_port = ''; + } + if ($cfg['Server']['host'] != $GLOBALS['pma_auth_server']) { + $cfg['Server']['host'] = $tmp_host; + if (! empty($tmp_port)) { + $cfg['Server']['port'] = $tmp_port; + } + } + unset($tmp_host, $tmp_port, $parts); + } + + return parent::storeCredentials(); + } + + /** + * Stores user credentials after successful login. + * + * @return void|bool + */ + public function rememberCredentials() + { + // Name and password cookies need to be refreshed each time + // Duration = one month for username + + $this->storeUsernameCookie($this->user); + + // Duration = as configured + // Do not store password cookie on password change as we will + // set the cookie again after password has been changed + if (! isset($_POST['change_pw'])) { + $this->storePasswordCookie($this->password); + } + // URL where to go: + $redirect_url = './index.php'; + + // any parameters to pass? + $url_params = []; + if (strlen($GLOBALS['db']) > 0) { + $url_params['db'] = $GLOBALS['db']; + } + if (strlen($GLOBALS['table']) > 0) { + $url_params['table'] = $GLOBALS['table']; + } + // any target to pass? + if (! empty($GLOBALS['target']) + && $GLOBALS['target'] != 'index.php' + ) { + $url_params['target'] = $GLOBALS['target']; + } + + // user logged in successfully after session expiration + if (isset($_REQUEST['session_timedout'])) { + $response = Response::getInstance(); + $response->addJSON( + 'logged_in', + 1 + ); + $response->addJSON( + 'success', + 1 + ); + $response->addJSON( + 'new_token', + $_SESSION[' PMA_token '] + ); + + if (! defined('TESTSUITE')) { + exit; + } else { + return false; + } + } + // Set server cookies if required (once per session) and, in this case, + // force reload to ensure the client accepts cookies + if (! $GLOBALS['from_cookie']) { + + /** + * Clear user cache. + */ + Util::clearUserCache(); + + Response::getInstance() + ->disable(); + + Core::sendHeaderLocation( + $redirect_url . Url::getCommonRaw($url_params), + true + ); + if (! defined('TESTSUITE')) { + exit; + } else { + return false; + } + } // end if + + return true; + } + + /** + * Stores username in a cookie. + * + * @param string $username User name + * + * @return void + */ + public function storeUsernameCookie($username) + { + // Name and password cookies need to be refreshed each time + // Duration = one month for username + $GLOBALS['PMA_Config']->setCookie( + 'pmaUser-' . $GLOBALS['server'], + $this->cookieEncrypt( + $username, + $this->_getEncryptionSecret() + ) + ); + } + + /** + * Stores password in a cookie. + * + * @param string $password Password + * + * @return void + */ + public function storePasswordCookie($password) + { + $payload = ['password' => $password]; + if ($GLOBALS['cfg']['AllowArbitraryServer'] && ! empty($GLOBALS['pma_auth_server'])) { + $payload['server'] = $GLOBALS['pma_auth_server']; + } + // Duration = as configured + $GLOBALS['PMA_Config']->setCookie( + 'pmaAuth-' . $GLOBALS['server'], + $this->cookieEncrypt( + json_encode($payload), + $this->_getSessionEncryptionSecret() + ), + null, + (int) $GLOBALS['cfg']['LoginCookieStore'] + ); + } + + /** + * User is not allowed to login to MySQL -> authentication failed + * + * prepares error message and switches to showLoginForm() which display the error + * and the login form + * + * this function MUST exit/quit the application, + * currently done by call to showLoginForm() + * + * @param string $failure String describing why authentication has failed + * + * @return void + */ + public function showFailure($failure) + { + global $conn_error; + + parent::showFailure($failure); + + // Deletes password cookie and displays the login form + $GLOBALS['PMA_Config']->removeCookie('pmaAuth-' . $GLOBALS['server']); + + $conn_error = $this->getErrorMessage($failure); + + $response = Response::getInstance(); + + // needed for PHP-CGI (not need for FastCGI or mod-php) + $response->header('Cache-Control: no-store, no-cache, must-revalidate'); + $response->header('Pragma: no-cache'); + + $this->showLoginForm(); + } + + /** + * Returns blowfish secret or generates one if needed. + * + * @return string + */ + private function _getEncryptionSecret() + { + if (empty($GLOBALS['cfg']['blowfish_secret'])) { + return $this->_getSessionEncryptionSecret(); + } + + return $GLOBALS['cfg']['blowfish_secret']; + } + + /** + * Returns blowfish secret or generates one if needed. + * + * @return string + */ + private function _getSessionEncryptionSecret() + { + if (empty($_SESSION['encryption_key'])) { + if ($this->_use_openssl) { + $_SESSION['encryption_key'] = openssl_random_pseudo_bytes(32); + } else { + $_SESSION['encryption_key'] = Crypt\Random::string(32); + } + } + return $_SESSION['encryption_key']; + } + + /** + * Concatenates secret in order to make it 16 bytes log + * + * This doesn't add any security, just ensures the secret + * is long enough by copying it. + * + * @param string $secret Original secret + * + * @return string + */ + public function enlargeSecret($secret) + { + while (strlen($secret) < 16) { + $secret .= $secret; + } + return substr($secret, 0, 16); + } + + /** + * Derives MAC secret from encryption secret. + * + * @param string $secret the secret + * + * @return string the MAC secret + */ + public function getMACSecret($secret) + { + // Grab first part, up to 16 chars + // The MAC and AES secrets can overlap if original secret is short + $length = strlen($secret); + if ($length > 16) { + return substr($secret, 0, 16); + } + return $this->enlargeSecret( + $length == 1 ? $secret : substr($secret, 0, -1) + ); + } + + /** + * Derives AES secret from encryption secret. + * + * @param string $secret the secret + * + * @return string the AES secret + */ + public function getAESSecret($secret) + { + // Grab second part, up to 16 chars + // The MAC and AES secrets can overlap if original secret is short + $length = strlen($secret); + if ($length > 16) { + return substr($secret, -16); + } + return $this->enlargeSecret( + $length == 1 ? $secret : substr($secret, 1) + ); + } + + /** + * Cleans any SSL errors + * + * This can happen from corrupted cookies, by invalid encryption + * parameters used in older phpMyAdmin versions or by wrong openSSL + * configuration. + * + * In neither case the error is useful to user, but we need to clear + * the error buffer as otherwise the errors would pop up later, for + * example during MySQL SSL setup. + * + * @return void + */ + public function cleanSSLErrors() + { + if (function_exists('openssl_error_string')) { + do { + $hasSslErrors = openssl_error_string(); + } while ($hasSslErrors !== false); + } + } + + /** + * Encryption using openssl's AES or phpseclib's AES + * (phpseclib uses mcrypt when it is available) + * + * @param string $data original data + * @param string $secret the secret + * + * @return string the encrypted result + */ + public function cookieEncrypt($data, $secret) + { + $mac_secret = $this->getMACSecret($secret); + $aes_secret = $this->getAESSecret($secret); + $iv = $this->createIV(); + if ($this->_use_openssl) { + $result = openssl_encrypt( + $data, + 'AES-128-CBC', + $aes_secret, + 0, + $iv + ); + } else { + $cipher = new Crypt\AES(Crypt\Base::MODE_CBC); + $cipher->setIV($iv); + $cipher->setKey($aes_secret); + $result = base64_encode($cipher->encrypt($data)); + } + $this->cleanSSLErrors(); + $iv = base64_encode($iv); + return json_encode( + [ + 'iv' => $iv, + 'mac' => hash_hmac('sha1', $iv . $result, $mac_secret), + 'payload' => $result, + ] + ); + } + + /** + * Decryption using openssl's AES or phpseclib's AES + * (phpseclib uses mcrypt when it is available) + * + * @param string $encdata encrypted data + * @param string $secret the secret + * + * @return string|false original data, false on error + */ + public function cookieDecrypt($encdata, $secret) + { + $data = json_decode($encdata, true); + + if (! is_array($data) || ! isset($data['mac']) || ! isset($data['iv']) || ! isset($data['payload']) + || ! is_string($data['mac']) || ! is_string($data['iv']) || ! is_string($data['payload']) + ) { + return false; + } + + $mac_secret = $this->getMACSecret($secret); + $aes_secret = $this->getAESSecret($secret); + $newmac = hash_hmac('sha1', $data['iv'] . $data['payload'], $mac_secret); + + if (! hash_equals($data['mac'], $newmac)) { + return false; + } + + if ($this->_use_openssl) { + $result = openssl_decrypt( + $data['payload'], + 'AES-128-CBC', + $aes_secret, + 0, + base64_decode($data['iv']) + ); + } else { + $cipher = new Crypt\AES(Crypt\Base::MODE_CBC); + $cipher->setIV(base64_decode($data['iv'])); + $cipher->setKey($aes_secret); + $result = $cipher->decrypt(base64_decode($data['payload'])); + } + $this->cleanSSLErrors(); + return $result; + } + + /** + * Returns size of IV for encryption. + * + * @return int + */ + public function getIVSize() + { + if ($this->_use_openssl) { + return openssl_cipher_iv_length('AES-128-CBC'); + } + return (new Crypt\AES(Crypt\Base::MODE_CBC))->block_size; + } + + /** + * Initialization + * Store the initialization vector because it will be needed for + * further decryption. I don't think necessary to have one iv + * per server so I don't put the server number in the cookie name. + * + * @return string + */ + public function createIV() + { + /* Testsuite shortcut only to allow predictable IV */ + if ($this->_cookie_iv !== null) { + return $this->_cookie_iv; + } + if ($this->_use_openssl) { + return openssl_random_pseudo_bytes( + $this->getIVSize() + ); + } + + return Crypt\Random::string( + $this->getIVSize() + ); + } + + /** + * Sets encryption IV to use + * + * This is for testing only! + * + * @param string $vector The IV + * + * @return void + */ + public function setIV($vector) + { + $this->_cookie_iv = $vector; + } + + /** + * Callback when user changes password. + * + * @param string $password New password to set + * + * @return void + */ + public function handlePasswordChange($password) + { + $this->storePasswordCookie($password); + } + + /** + * Perform logout + * + * @return void + */ + public function logOut() + { + /** @var Config $PMA_Config */ + global $PMA_Config; + + // -> delete password cookie(s) + if ($GLOBALS['cfg']['LoginCookieDeleteAll']) { + foreach ($GLOBALS['cfg']['Servers'] as $key => $val) { + $PMA_Config->removeCookie('pmaAuth-' . $key); + if ($PMA_Config->issetCookie('pmaAuth-' . $key)) { + $PMA_Config->removeCookie('pmaAuth-' . $key); + } + } + } else { + $cookieName = 'pmaAuth-' . $GLOBALS['server']; + $PMA_Config->removeCookie($cookieName); + if ($PMA_Config->issetCookie($cookieName)) { + $PMA_Config->removeCookie($cookieName); + } + } + parent::logOut(); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationHttp.php b/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationHttp.php new file mode 100644 index 0000000..6d735e8 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationHttp.php @@ -0,0 +1,214 @@ +isAjax()) { + $response->setRequestStatus(false); + // reload_flag removes the token parameter from the URL and reloads + $response->addJSON('reload_flag', '1'); + if (defined('TESTSUITE')) { + return true; + } else { + exit; + } + } + + return $this->authForm(); + } + + /** + * Displays authentication form + * + * @return boolean + */ + public function authForm() + { + if (empty($GLOBALS['cfg']['Server']['auth_http_realm'])) { + if (empty($GLOBALS['cfg']['Server']['verbose'])) { + $server_message = $GLOBALS['cfg']['Server']['host']; + } else { + $server_message = $GLOBALS['cfg']['Server']['verbose']; + } + $realm_message = 'phpMyAdmin ' . $server_message; + } else { + $realm_message = $GLOBALS['cfg']['Server']['auth_http_realm']; + } + + $response = Response::getInstance(); + + // remove non US-ASCII to respect RFC2616 + $realm_message = preg_replace('/[^\x20-\x7e]/i', '', $realm_message); + $response->header('WWW-Authenticate: Basic realm="' . $realm_message . '"'); + $response->setHttpResponseCode(401); + + /* HTML header */ + $footer = $response->getFooter(); + $footer->setMinimal(); + $header = $response->getHeader(); + $header->setTitle(__('Access denied!')); + $header->disableMenuAndConsole(); + $header->setBodyId('loginform'); + + $response->addHTML('

    '); + $response->addHTML(sprintf(__('Welcome to %s'), ' phpMyAdmin')); + $response->addHTML('

    '); + $response->addHTML('

    '); + $response->addHTML( + Message::error( + __('Wrong username/password. Access denied.') + ) + ); + $response->addHTML('

    '); + + $response->addHTML(Config::renderFooter()); + + if (! defined('TESTSUITE')) { + exit; + } else { + return false; + } + } + + /** + * Gets authentication credentials + * + * @return boolean whether we get authentication settings or not + */ + public function readCredentials() + { + // Grabs the $PHP_AUTH_USER variable + if (isset($GLOBALS['PHP_AUTH_USER'])) { + $this->user = $GLOBALS['PHP_AUTH_USER']; + } + if (empty($this->user)) { + if (Core::getenv('PHP_AUTH_USER')) { + $this->user = Core::getenv('PHP_AUTH_USER'); + } elseif (Core::getenv('REMOTE_USER')) { + // CGI, might be encoded, see below + $this->user = Core::getenv('REMOTE_USER'); + } elseif (Core::getenv('REDIRECT_REMOTE_USER')) { + // CGI, might be encoded, see below + $this->user = Core::getenv('REDIRECT_REMOTE_USER'); + } elseif (Core::getenv('AUTH_USER')) { + // WebSite Professional + $this->user = Core::getenv('AUTH_USER'); + } elseif (Core::getenv('HTTP_AUTHORIZATION')) { + // IIS, might be encoded, see below + $this->user = Core::getenv('HTTP_AUTHORIZATION'); + } elseif (Core::getenv('Authorization')) { + // FastCGI, might be encoded, see below + $this->user = Core::getenv('Authorization'); + } + } + // Grabs the $PHP_AUTH_PW variable + if (isset($GLOBALS['PHP_AUTH_PW'])) { + $this->password = $GLOBALS['PHP_AUTH_PW']; + } + if (empty($this->password)) { + if (Core::getenv('PHP_AUTH_PW')) { + $this->password = Core::getenv('PHP_AUTH_PW'); + } elseif (Core::getenv('REMOTE_PASSWORD')) { + // Apache/CGI + $this->password = Core::getenv('REMOTE_PASSWORD'); + } elseif (Core::getenv('AUTH_PASSWORD')) { + // WebSite Professional + $this->password = Core::getenv('AUTH_PASSWORD'); + } + } + // Sanitize empty password login + if ($this->password === null) { + $this->password = ''; + } + + // Avoid showing the password in phpinfo()'s output + unset($GLOBALS['PHP_AUTH_PW']); + unset($_SERVER['PHP_AUTH_PW']); + + // Decode possibly encoded information (used by IIS/CGI/FastCGI) + // (do not use explode() because a user might have a colon in his password + if (strcmp(substr($this->user, 0, 6), 'Basic ') == 0) { + $usr_pass = base64_decode(substr($this->user, 6)); + if (! empty($usr_pass)) { + $colon = strpos($usr_pass, ':'); + if ($colon) { + $this->user = substr($usr_pass, 0, $colon); + $this->password = substr($usr_pass, $colon + 1); + } + unset($colon); + } + unset($usr_pass); + } + + // sanitize username + $this->user = Core::sanitizeMySQLUser($this->user); + + // User logged out -> ensure the new username is not the same + $old_usr = isset($_REQUEST['old_usr']) ? $_REQUEST['old_usr'] : ''; + if (! empty($old_usr) + && (isset($this->user) && hash_equals($old_usr, $this->user)) + ) { + $this->user = ''; + } + + // Returns whether we get authentication settings or not + return ! empty($this->user); + } + + /** + * User is not allowed to login to MySQL -> authentication failed + * + * @param string $failure String describing why authentication has failed + * + * @return void + */ + public function showFailure($failure) + { + parent::showFailure($failure); + $error = $GLOBALS['dbi']->getError(); + if ($error && $GLOBALS['errno'] != 1045) { + Core::fatalError($error); + } else { + $this->authForm(); + } + } + + /** + * Returns URL for login form. + * + * @return string + */ + public function getLoginFormURL() + { + return './index.php?old_usr=' . $this->user; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationSignon.php b/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationSignon.php new file mode 100644 index 0000000..36b1d66 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Auth/AuthenticationSignon.php @@ -0,0 +1,282 @@ +=')) { + session_set_cookie_params($sessionCookieParams); + } + + session_set_cookie_params( + $sessionCookieParams['lifetime'], + $sessionCookieParams['path'], + $sessionCookieParams['domain'], + $sessionCookieParams['secure'], + $sessionCookieParams['httponly'] + ); + } + + /** + * Gets authentication credentials + * + * @return boolean whether we get authentication settings or not + */ + public function readCredentials() + { + /* Check if we're using same signon server */ + $signon_url = $GLOBALS['cfg']['Server']['SignonURL']; + if (isset($_SESSION['LAST_SIGNON_URL']) + && $_SESSION['LAST_SIGNON_URL'] != $signon_url + ) { + return false; + } + + /* Script name */ + $script_name = $GLOBALS['cfg']['Server']['SignonScript']; + + /* Session name */ + $session_name = $GLOBALS['cfg']['Server']['SignonSession']; + + /* Login URL */ + $signon_url = $GLOBALS['cfg']['Server']['SignonURL']; + + /* Current host */ + $single_signon_host = $GLOBALS['cfg']['Server']['host']; + + /* Current port */ + $single_signon_port = $GLOBALS['cfg']['Server']['port']; + + /* No configuration updates */ + $single_signon_cfgupdate = []; + + /* Handle script based auth */ + if (! empty($script_name)) { + if (! @file_exists($script_name)) { + Core::fatalError( + __('Can not find signon authentication script:') + . ' ' . $script_name + ); + } + include $script_name; + + list ($this->user, $this->password) + = get_login_credentials($GLOBALS['cfg']['Server']['user']); + } elseif (isset($_COOKIE[$session_name])) { /* Does session exist? */ + /* End current session */ + $old_session = session_name(); + $old_id = session_id(); + $oldCookieParams = session_get_cookie_params(); + if (! defined('TESTSUITE')) { + session_write_close(); + } + /* Load single signon session */ + if (! defined('TESTSUITE')) { + $this->setCookieParams(); + session_name($session_name); + session_id($_COOKIE[$session_name]); + session_start(); + } + + /* Clear error message */ + unset($_SESSION['PMA_single_signon_error_message']); + + /* Grab credentials if they exist */ + if (isset($_SESSION['PMA_single_signon_user'])) { + $this->user = $_SESSION['PMA_single_signon_user']; + } + if (isset($_SESSION['PMA_single_signon_password'])) { + $this->password = $_SESSION['PMA_single_signon_password']; + } + if (isset($_SESSION['PMA_single_signon_host'])) { + $single_signon_host = $_SESSION['PMA_single_signon_host']; + } + + if (isset($_SESSION['PMA_single_signon_port'])) { + $single_signon_port = $_SESSION['PMA_single_signon_port']; + } + + if (isset($_SESSION['PMA_single_signon_cfgupdate'])) { + $single_signon_cfgupdate = $_SESSION['PMA_single_signon_cfgupdate']; + } + + /* Also get token as it is needed to access subpages */ + if (isset($_SESSION['PMA_single_signon_token'])) { + /* No need to care about token on logout */ + $pma_token = $_SESSION['PMA_single_signon_token']; + } + + /* End single signon session */ + if (! defined('TESTSUITE')) { + session_write_close(); + } + + /* Restart phpMyAdmin session */ + if (! defined('TESTSUITE')) { + $this->setCookieParams($oldCookieParams); + session_name($old_session); + if (! empty($old_id)) { + session_id($old_id); + } + session_start(); + } + + /* Set the single signon host */ + $GLOBALS['cfg']['Server']['host'] = $single_signon_host; + + /* Set the single signon port */ + $GLOBALS['cfg']['Server']['port'] = $single_signon_port; + + /* Configuration update */ + $GLOBALS['cfg']['Server'] = array_merge( + $GLOBALS['cfg']['Server'], + $single_signon_cfgupdate + ); + + /* Restore our token */ + if (! empty($pma_token)) { + $_SESSION[' PMA_token '] = $pma_token; + $_SESSION[' HMAC_secret '] = Util::generateRandom(16); + } + + /** + * Clear user cache. + */ + Util::clearUserCache(); + } + + // Returns whether we get authentication settings or not + if (empty($this->user)) { + unset($_SESSION['LAST_SIGNON_URL']); + + return false; + } + + $_SESSION['LAST_SIGNON_URL'] = $GLOBALS['cfg']['Server']['SignonURL']; + + return true; + } + + /** + * User is not allowed to login to MySQL -> authentication failed + * + * @param string $failure String describing why authentication has failed + * + * @return void + */ + public function showFailure($failure) + { + parent::showFailure($failure); + + /* Session name */ + $session_name = $GLOBALS['cfg']['Server']['SignonSession']; + + /* Does session exist? */ + if (isset($_COOKIE[$session_name])) { + if (! defined('TESTSUITE')) { + /* End current session */ + session_write_close(); + + /* Load single signon session */ + $this->setCookieParams(); + session_name($session_name); + session_id($_COOKIE[$session_name]); + session_start(); + } + + /* Set error message */ + $_SESSION['PMA_single_signon_error_message'] = $this->getErrorMessage($failure); + } + $this->showLoginForm(); + } + + /** + * Returns URL for login form. + * + * @return string + */ + public function getLoginFormURL() + { + return $GLOBALS['cfg']['Server']['SignonURL']; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/AuthenticationPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/AuthenticationPlugin.php new file mode 100644 index 0000000..a275f49 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/AuthenticationPlugin.php @@ -0,0 +1,371 @@ +ipAllowDeny = new IpAllowDeny(); + $this->template = new Template(); + } + + /** + * Displays authentication form + * + * @return boolean + */ + abstract public function showLoginForm(); + + /** + * Gets authentication credentials + * + * @return boolean + */ + abstract public function readCredentials(); + + /** + * Set the user and password after last checkings if required + * + * @return boolean + */ + public function storeCredentials() + { + global $cfg; + + $this->setSessionAccessTime(); + + $cfg['Server']['user'] = $this->user; + $cfg['Server']['password'] = $this->password; + + return true; + } + + /** + * Stores user credentials after successful login. + * + * @return void + */ + public function rememberCredentials() + { + } + + /** + * User is not allowed to login to MySQL -> authentication failed + * + * @param string $failure String describing why authentication has failed + * + * @return void + */ + public function showFailure($failure) + { + Logging::logUser($this->user, $failure); + } + + /** + * Perform logout + * + * @return void + */ + public function logOut() + { + /** @var Config $PMA_Config */ + global $PMA_Config; + + /* Obtain redirect URL (before doing logout) */ + if (! empty($GLOBALS['cfg']['Server']['LogoutURL'])) { + $redirect_url = $GLOBALS['cfg']['Server']['LogoutURL']; + } else { + $redirect_url = $this->getLoginFormURL(); + } + + /* Clear credentials */ + $this->user = ''; + $this->password = ''; + + /* + * Get a logged-in server count in case of LoginCookieDeleteAll is disabled. + */ + $server = 0; + if ($GLOBALS['cfg']['LoginCookieDeleteAll'] === false + && $GLOBALS['cfg']['Server']['auth_type'] == 'cookie' + ) { + foreach ($GLOBALS['cfg']['Servers'] as $key => $val) { + if ($PMA_Config->issetCookie('pmaAuth-' . $key)) { + $server = $key; + } + } + } + + if ($server === 0) { + /* delete user's choices that were stored in session */ + if (! defined('TESTSUITE')) { + session_unset(); + session_destroy(); + } + + /* Redirect to login form (or configured URL) */ + Core::sendHeaderLocation($redirect_url); + } else { + /* Redirect to other autenticated server */ + $_SESSION['partial_logout'] = true; + Core::sendHeaderLocation( + './index.php' . Url::getCommonRaw(['server' => $server]) + ); + } + } + + /** + * Returns URL for login form. + * + * @return string + */ + public function getLoginFormURL() + { + return './index.php'; + } + + /** + * Returns error message for failed authentication. + * + * @param string $failure String describing why authentication has failed + * + * @return string + */ + public function getErrorMessage($failure) + { + if ($failure == 'empty-denied') { + return __( + 'Login without a password is forbidden by configuration' + . ' (see AllowNoPassword)' + ); + } elseif ($failure == 'root-denied' || $failure == 'allow-denied') { + return __('Access denied!'); + } elseif ($failure == 'no-activity') { + return sprintf( + __('No activity within %s seconds; please log in again.'), + intval($GLOBALS['cfg']['LoginCookieValidity']) + ); + } + + $dbi_error = $GLOBALS['dbi']->getError(); + if (! empty($dbi_error)) { + return htmlspecialchars($dbi_error); + } elseif (isset($GLOBALS['errno'])) { + return '#' . $GLOBALS['errno'] . ' ' + . __('Cannot log in to the MySQL server'); + } + + return __('Cannot log in to the MySQL server'); + } + + /** + * Callback when user changes password. + * + * @param string $password New password to set + * + * @return void + */ + public function handlePasswordChange($password) + { + } + + /** + * Store session access time in session. + * + * Tries to workaround PHP 5 session garbage collection which + * looks at the session file's last modified time + * + * @return void + */ + public function setSessionAccessTime() + { + if (isset($_REQUEST['guid'])) { + $guid = (string) $_REQUEST['guid']; + } else { + $guid = 'default'; + } + if (isset($_REQUEST['access_time'])) { + // Ensure access_time is in range <0, LoginCookieValidity + 1> + // to avoid excessive extension of validity. + // + // Negative values can cause session expiry extension + // Too big values can cause overflow and lead to same + $time = time() - min(max(0, intval($_REQUEST['access_time'])), $GLOBALS['cfg']['LoginCookieValidity'] + 1); + } else { + $time = time(); + } + $_SESSION['browser_access_time'][$guid] = $time; + } + + /** + * High level authentication interface + * + * Gets the credentials or shows login form if necessary + * + * @return void + */ + public function authenticate() + { + $success = $this->readCredentials(); + + /* Show login form (this exits) */ + if (! $success) { + /* Force generating of new session */ + Session::secure(); + $this->showLoginForm(); + } + + /* Store credentials (eg. in cookies) */ + $this->storeCredentials(); + /* Check allow/deny rules */ + $this->checkRules(); + } + + /** + * Check configuration defined restrictions for authentication + * + * @return void + */ + public function checkRules() + { + global $cfg; + + // Check IP-based Allow/Deny rules as soon as possible to reject the + // user based on mod_access in Apache + if (isset($cfg['Server']['AllowDeny']) + && isset($cfg['Server']['AllowDeny']['order']) + ) { + $allowDeny_forbidden = false; // default + if ($cfg['Server']['AllowDeny']['order'] == 'allow,deny') { + $allowDeny_forbidden = true; + if ($this->ipAllowDeny->allow()) { + $allowDeny_forbidden = false; + } + if ($this->ipAllowDeny->deny()) { + $allowDeny_forbidden = true; + } + } elseif ($cfg['Server']['AllowDeny']['order'] == 'deny,allow') { + if ($this->ipAllowDeny->deny()) { + $allowDeny_forbidden = true; + } + if ($this->ipAllowDeny->allow()) { + $allowDeny_forbidden = false; + } + } elseif ($cfg['Server']['AllowDeny']['order'] == 'explicit') { + if ($this->ipAllowDeny->allow() && ! $this->ipAllowDeny->deny()) { + $allowDeny_forbidden = false; + } else { + $allowDeny_forbidden = true; + } + } // end if ... elseif ... elseif + + // Ejects the user if banished + if ($allowDeny_forbidden) { + $this->showFailure('allow-denied'); + } + } // end if + + // is root allowed? + if (! $cfg['Server']['AllowRoot'] && $cfg['Server']['user'] == 'root') { + $this->showFailure('root-denied'); + } + + // is a login without password allowed? + if (! $cfg['Server']['AllowNoPassword'] + && $cfg['Server']['password'] === '' + ) { + $this->showFailure('empty-denied'); + } + } + + /** + * Checks whether two factor authentication is active + * for given user and performs it. + * + * @return boolean|void + */ + public function checkTwoFactor() + { + $twofactor = new TwoFactor($this->user); + + /* Do we need to show the form? */ + if ($twofactor->check()) { + return; + } + + $response = Response::getInstance(); + if ($response->loginPage()) { + if (defined('TESTSUITE')) { + return; + } else { + exit; + } + } + echo $this->template->render('login/header', ['theme' => $GLOBALS['PMA_Theme']]); + Message::rawNotice( + __('You have enabled two factor authentication, please confirm your login.') + )->display(); + echo $this->template->render('login/twofactor', [ + 'form' => $twofactor->render(), + 'show_submit' => $twofactor->showSubmit, + ]); + echo $this->template->render('login/footer'); + echo Config::renderFooter(); + if (! defined('TESTSUITE')) { + exit; + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportCodegen.php b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportCodegen.php new file mode 100644 index 0000000..8a29538 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportCodegen.php @@ -0,0 +1,447 @@ +initSpecificVariables(); + $this->setProperties(); + } + + /** + * Initialize the local variables that are used for export CodeGen + * + * @return void + */ + protected function initSpecificVariables() + { + $this->_setCgFormats( + [ + "NHibernate C# DO", + "NHibernate XML", + ] + ); + + $this->_setCgHandlers( + [ + "_handleNHibernateCSBody", + "_handleNHibernateXMLBody", + ] + ); + } + + /** + * Sets the export CodeGen properties + * + * @return void + */ + protected function setProperties() + { + $exportPluginProperties = new ExportPluginProperties(); + $exportPluginProperties->setText('CodeGen'); + $exportPluginProperties->setExtension('cs'); + $exportPluginProperties->setMimeType('text/cs'); + $exportPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $exportPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new OptionsPropertyMainGroup("general_opts"); + // create primary items and add them to the group + $leaf = new HiddenPropertyItem("structure_or_data"); + $generalOptions->addProperty($leaf); + $leaf = new SelectPropertyItem( + "format", + __('Format:') + ); + $leaf->setValues($this->_getCgFormats()); + $generalOptions->addProperty($leaf); + // add the main group to the root group + $exportSpecificOptions->addProperty($generalOptions); + + // set the options for the export plugin property item + $exportPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $exportPluginProperties; + } + + /** + * Outputs export header + * + * @return bool Whether it succeeded + */ + public function exportHeader() + { + return true; + } + + /** + * Outputs export footer + * + * @return bool Whether it succeeded + */ + public function exportFooter() + { + return true; + } + + /** + * Outputs database header + * + * @param string $db Database name + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBHeader($db, $db_alias = '') + { + return true; + } + + /** + * Outputs database footer + * + * @param string $db Database name + * + * @return bool Whether it succeeded + */ + public function exportDBFooter($db) + { + return true; + } + + /** + * Outputs CREATE DATABASE statement + * + * @param string $db Database name + * @param string $export_type 'server', 'database', 'table' + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBCreate($db, $export_type, $db_alias = '') + { + return true; + } + + /** + * Outputs the content of a table in NHibernate format + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $sql_query SQL query for obtaining data + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportData( + $db, + $table, + $crlf, + $error_url, + $sql_query, + array $aliases = [] + ) { + $CG_FORMATS = $this->_getCgFormats(); + $CG_HANDLERS = $this->_getCgHandlers(); + + $format = $GLOBALS['codegen_format']; + if (isset($CG_FORMATS[$format])) { + $method = $CG_HANDLERS[$format]; + + return $this->export->outputHandler( + $this->$method($db, $table, $crlf, $aliases) + ); + } + + return $this->export->outputHandler(sprintf("%s is not supported.", $format)); + } + + /** + * Used to make identifiers (from table or database names) + * + * @param string $str name to be converted + * @param bool $ucfirst whether to make the first character uppercase + * + * @return string identifier + */ + public static function cgMakeIdentifier($str, $ucfirst = true) + { + // remove unsafe characters + $str = preg_replace('/[^\p{L}\p{Nl}_]/u', '', $str); + // make sure first character is a letter or _ + if (! preg_match('/^\pL/u', $str)) { + $str = '_' . $str; + } + if ($ucfirst) { + $str = ucfirst($str); + } + + return $str; + } + + /** + * C# Handler + * + * @param string $db database name + * @param string $table table name + * @param string $crlf line separator + * @param array $aliases Aliases of db/table/columns + * + * @return string containing C# code lines, separated by "\n" + */ + private function _handleNHibernateCSBody($db, $table, $crlf, array $aliases = []) + { + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + $lines = []; + + $result = $GLOBALS['dbi']->query( + sprintf( + 'DESC %s.%s', + Util::backquote($db), + Util::backquote($table) + ) + ); + if ($result) { + /** @var TableProperty[] $tableProperties */ + $tableProperties = []; + while ($row = $GLOBALS['dbi']->fetchRow($result)) { + $col_as = $this->getAlias($aliases, $row[0], 'col', $db, $table); + if (! empty($col_as)) { + $row[0] = $col_as; + } + $tableProperties[] = new TableProperty($row); + } + $GLOBALS['dbi']->freeResult($result); + $lines[] = 'using System;'; + $lines[] = 'using System.Collections;'; + $lines[] = 'using System.Collections.Generic;'; + $lines[] = 'using System.Text;'; + $lines[] = 'namespace ' . ExportCodegen::cgMakeIdentifier($db_alias); + $lines[] = '{'; + $lines[] = ' #region ' + . ExportCodegen::cgMakeIdentifier($table_alias); + $lines[] = ' public class ' + . ExportCodegen::cgMakeIdentifier($table_alias); + $lines[] = ' {'; + $lines[] = ' #region Member Variables'; + foreach ($tableProperties as $tableProperty) { + $lines[] = $tableProperty->formatCs( + ' protected #dotNetPrimitiveType# _#name#;' + ); + } + $lines[] = ' #endregion'; + $lines[] = ' #region Constructors'; + $lines[] = ' public ' + . ExportCodegen::cgMakeIdentifier($table_alias) . '() { }'; + $temp = []; + foreach ($tableProperties as $tableProperty) { + if (! $tableProperty->isPK()) { + $temp[] = $tableProperty->formatCs( + '#dotNetPrimitiveType# #name#' + ); + } + } + $lines[] = ' public ' + . ExportCodegen::cgMakeIdentifier($table_alias) + . '(' + . implode(', ', $temp) + . ')'; + $lines[] = ' {'; + foreach ($tableProperties as $tableProperty) { + if (! $tableProperty->isPK()) { + $lines[] = $tableProperty->formatCs( + ' this._#name#=#name#;' + ); + } + } + $lines[] = ' }'; + $lines[] = ' #endregion'; + $lines[] = ' #region Public Properties'; + foreach ($tableProperties as $tableProperty) { + $lines[] = $tableProperty->formatCs( + ' public virtual #dotNetPrimitiveType# #ucfirstName#' + . "\n" + . ' {' . "\n" + . ' get {return _#name#;}' . "\n" + . ' set {_#name#=value;}' . "\n" + . ' }' + ); + } + $lines[] = ' #endregion'; + $lines[] = ' }'; + $lines[] = ' #endregion'; + $lines[] = '}'; + } + + return implode($crlf, $lines); + } + + /** + * XML Handler + * + * @param string $db database name + * @param string $table table name + * @param string $crlf line separator + * @param array $aliases Aliases of db/table/columns + * + * @return string containing XML code lines, separated by "\n" + */ + private function _handleNHibernateXMLBody( + $db, + $table, + $crlf, + array $aliases = [] + ) { + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + $lines = []; + $lines[] = ''; + $lines[] = ''; + $lines[] = ' '; + $result = $GLOBALS['dbi']->query( + sprintf( + "DESC %s.%s", + Util::backquote($db), + Util::backquote($table) + ) + ); + if ($result) { + while ($row = $GLOBALS['dbi']->fetchRow($result)) { + $col_as = $this->getAlias($aliases, $row[0], 'col', $db, $table); + if (! empty($col_as)) { + $row[0] = $col_as; + } + $tableProperty = new TableProperty($row); + if ($tableProperty->isPK()) { + $lines[] = $tableProperty->formatXml( + ' ' . "\n" + . ' ' . "\n" + . ' ' . "\n" + . ' ' + ); + } else { + $lines[] = $tableProperty->formatXml( + ' ' . "\n" + . ' ' . "\n" + . ' ' + ); + } + } + $GLOBALS['dbi']->freeResult($result); + } + $lines[] = ' '; + $lines[] = ''; + + return implode($crlf, $lines); + } + + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Getter for CodeGen formats + * + * @return array + */ + private function _getCgFormats() + { + return $this->_cgFormats; + } + + /** + * Setter for CodeGen formats + * + * @param array $CG_FORMATS contains CodeGen Formats + * + * @return void + */ + private function _setCgFormats(array $CG_FORMATS) + { + $this->_cgFormats = $CG_FORMATS; + } + + /** + * Getter for CodeGen handlers + * + * @return array + */ + private function _getCgHandlers() + { + return $this->_cgHandlers; + } + + /** + * Setter for CodeGen handlers + * + * @param array $CG_HANDLERS contains CodeGen handler methods + * + * @return void + */ + private function _setCgHandlers(array $CG_HANDLERS) + { + $this->_cgHandlers = $CG_HANDLERS; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportCsv.php b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportCsv.php new file mode 100644 index 0000000..8e5a319 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportCsv.php @@ -0,0 +1,347 @@ +setProperties(); + } + + /** + * Sets the export CSV properties + * + * @return void + */ + protected function setProperties() + { + $exportPluginProperties = new ExportPluginProperties(); + $exportPluginProperties->setText('CSV'); + $exportPluginProperties->setExtension('csv'); + $exportPluginProperties->setMimeType('text/comma-separated-values'); + $exportPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $exportPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new OptionsPropertyMainGroup("general_opts"); + // create leaf items and add them to the group + $leaf = new TextPropertyItem( + "separator", + __('Columns separated with:') + ); + $generalOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + "enclosed", + __('Columns enclosed with:') + ); + $generalOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + "escaped", + __('Columns escaped with:') + ); + $generalOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + "terminated", + __('Lines terminated with:') + ); + $generalOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + 'null', + __('Replace NULL with:') + ); + $generalOptions->addProperty($leaf); + $leaf = new BoolPropertyItem( + 'removeCRLF', + __('Remove carriage return/line feed characters within columns') + ); + $generalOptions->addProperty($leaf); + $leaf = new BoolPropertyItem( + 'columns', + __('Put columns names in the first row') + ); + $generalOptions->addProperty($leaf); + $leaf = new HiddenPropertyItem( + 'structure_or_data' + ); + $generalOptions->addProperty($leaf); + // add the main group to the root group + $exportSpecificOptions->addProperty($generalOptions); + + // set the options for the export plugin property item + $exportPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $exportPluginProperties; + } + + /** + * Outputs export header + * + * @return bool Whether it succeeded + */ + public function exportHeader() + { + global $what, $csv_terminated, $csv_separator, $csv_enclosed, $csv_escaped; + //Enable columns names by default for CSV + if ($what == 'csv') { + $GLOBALS['csv_columns'] = 'yes'; + } + // Here we just prepare some values for export + if ($what == 'excel') { + $csv_terminated = "\015\012"; + switch ($GLOBALS['excel_edition']) { + case 'win': + // as tested on Windows with Excel 2002 and Excel 2007 + $csv_separator = ';'; + break; + case 'mac_excel2003': + $csv_separator = ';'; + break; + case 'mac_excel2008': + $csv_separator = ','; + break; + } + $csv_enclosed = '"'; + $csv_escaped = '"'; + if (isset($GLOBALS['excel_columns'])) { + $GLOBALS['csv_columns'] = 'yes'; + } + } else { + if (empty($csv_terminated) + || mb_strtolower($csv_terminated) == 'auto' + ) { + $csv_terminated = $GLOBALS['crlf']; + } else { + $csv_terminated = str_replace( + [ + '\\r', + '\\n', + '\\t', + ], + [ + "\015", + "\012", + "\011", + ], + $csv_terminated + ); + } // end if + $csv_separator = str_replace('\\t', "\011", $csv_separator); + } + + return true; + } + + /** + * Outputs export footer + * + * @return bool Whether it succeeded + */ + public function exportFooter() + { + return true; + } + + /** + * Outputs database header + * + * @param string $db Database name + * @param string $db_alias Alias of db + * + * @return bool Whether it succeeded + */ + public function exportDBHeader($db, $db_alias = '') + { + return true; + } + + /** + * Outputs database footer + * + * @param string $db Database name + * + * @return bool Whether it succeeded + */ + public function exportDBFooter($db) + { + return true; + } + + /** + * Outputs CREATE DATABASE statement + * + * @param string $db Database name + * @param string $export_type 'server', 'database', 'table' + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBCreate($db, $export_type, $db_alias = '') + { + return true; + } + + /** + * Outputs the content of a table in CSV format + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $sql_query SQL query for obtaining data + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportData( + $db, + $table, + $crlf, + $error_url, + $sql_query, + array $aliases = [] + ) { + global $what, $csv_terminated, $csv_separator, $csv_enclosed, $csv_escaped; + + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + + // Gets the data from the database + $result = $GLOBALS['dbi']->query( + $sql_query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_UNBUFFERED + ); + $fields_cnt = $GLOBALS['dbi']->numFields($result); + + // If required, get fields name at the first line + if (isset($GLOBALS['csv_columns'])) { + $schema_insert = ''; + for ($i = 0; $i < $fields_cnt; $i++) { + $col_as = $GLOBALS['dbi']->fieldName($result, $i); + if (! empty($aliases[$db]['tables'][$table]['columns'][$col_as])) { + $col_as = $aliases[$db]['tables'][$table]['columns'][$col_as]; + } + $col_as = stripslashes($col_as); + if ($csv_enclosed == '') { + $schema_insert .= $col_as; + } else { + $schema_insert .= $csv_enclosed + . str_replace( + $csv_enclosed, + $csv_escaped . $csv_enclosed, + $col_as + ) + . $csv_enclosed; + } + $schema_insert .= $csv_separator; + } // end for + $schema_insert = trim(mb_substr($schema_insert, 0, -1)); + if (! $this->export->outputHandler($schema_insert . $csv_terminated)) { + return false; + } + } // end if + + // Format the data + while ($row = $GLOBALS['dbi']->fetchRow($result)) { + $schema_insert = ''; + for ($j = 0; $j < $fields_cnt; $j++) { + if (! isset($row[$j]) || $row[$j] === null) { + $schema_insert .= $GLOBALS[$what . '_null']; + } elseif ($row[$j] == '0' || $row[$j] != '') { + // always enclose fields + if ($what == 'excel') { + $row[$j] = preg_replace("/\015(\012)?/", "\012", $row[$j]); + } + // remove CRLF characters within field + if (isset($GLOBALS[$what . '_removeCRLF']) + && $GLOBALS[$what . '_removeCRLF'] + ) { + $row[$j] = str_replace( + [ + "\r", + "\n", + ], + "", + $row[$j] + ); + } + if ($csv_enclosed == '') { + $schema_insert .= $row[$j]; + } else { + // also double the escape string if found in the data + if ($csv_escaped != $csv_enclosed) { + $schema_insert .= $csv_enclosed + . str_replace( + $csv_enclosed, + $csv_escaped . $csv_enclosed, + str_replace( + $csv_escaped, + $csv_escaped . $csv_escaped, + $row[$j] + ) + ) + . $csv_enclosed; + } else { + // avoid a problem when escape string equals enclose + $schema_insert .= $csv_enclosed + . str_replace( + $csv_enclosed, + $csv_escaped . $csv_enclosed, + $row[$j] + ) + . $csv_enclosed; + } + } + } else { + $schema_insert .= ''; + } + if ($j < $fields_cnt - 1) { + $schema_insert .= $csv_separator; + } + } // end for + + if (! $this->export->outputHandler($schema_insert . $csv_terminated)) { + return false; + } + } // end while + $GLOBALS['dbi']->freeResult($result); + + return true; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportExcel.php b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportExcel.php new file mode 100644 index 0000000..e778d20 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportExcel.php @@ -0,0 +1,90 @@ +setText('CSV for MS Excel'); + $exportPluginProperties->setExtension('csv'); + $exportPluginProperties->setMimeType('text/comma-separated-values'); + $exportPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $exportPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new OptionsPropertyMainGroup("general_opts"); + // create primary items and add them to the group + $leaf = new TextPropertyItem( + 'null', + __('Replace NULL with:') + ); + $generalOptions->addProperty($leaf); + $leaf = new BoolPropertyItem( + 'removeCRLF', + __('Remove carriage return/line feed characters within columns') + ); + $generalOptions->addProperty($leaf); + $leaf = new BoolPropertyItem( + 'columns', + __('Put columns names in the first row') + ); + $generalOptions->addProperty($leaf); + $leaf = new SelectPropertyItem( + 'edition', + __('Excel edition:') + ); + $leaf->setValues( + [ + 'win' => 'Windows', + 'mac_excel2003' => 'Excel 2003 / Macintosh', + 'mac_excel2008' => 'Excel 2008 / Macintosh', + ] + ); + $generalOptions->addProperty($leaf); + $leaf = new HiddenPropertyItem( + 'structure_or_data' + ); + $generalOptions->addProperty($leaf); + // add the main group to the root group + $exportSpecificOptions->addProperty($generalOptions); + + // set the options for the export plugin property item + $exportPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $exportPluginProperties; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportHtmlword.php b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportHtmlword.php new file mode 100644 index 0000000..cbf4fce --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportHtmlword.php @@ -0,0 +1,670 @@ +setProperties(); + } + + /** + * Sets the export HTML-Word properties + * + * @return void + */ + protected function setProperties() + { + $exportPluginProperties = new ExportPluginProperties(); + $exportPluginProperties->setText('Microsoft Word 2000'); + $exportPluginProperties->setExtension('doc'); + $exportPluginProperties->setMimeType('application/vnd.ms-word'); + $exportPluginProperties->setForceFile(true); + $exportPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $exportPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // what to dump (structure/data/both) + $dumpWhat = new OptionsPropertyMainGroup( + "dump_what", + __('Dump table') + ); + // create primary items and add them to the group + $leaf = new RadioPropertyItem("structure_or_data"); + $leaf->setValues( + [ + 'structure' => __('structure'), + 'data' => __('data'), + 'structure_and_data' => __('structure and data'), + ] + ); + $dumpWhat->addProperty($leaf); + // add the main group to the root group + $exportSpecificOptions->addProperty($dumpWhat); + + // data options main group + $dataOptions = new OptionsPropertyMainGroup( + "dump_what", + __('Data dump options') + ); + $dataOptions->setForce('structure'); + // create primary items and add them to the group + $leaf = new TextPropertyItem( + "null", + __('Replace NULL with:') + ); + $dataOptions->addProperty($leaf); + $leaf = new BoolPropertyItem( + "columns", + __('Put columns names in the first row') + ); + $dataOptions->addProperty($leaf); + // add the main group to the root group + $exportSpecificOptions->addProperty($dataOptions); + + // set the options for the export plugin property item + $exportPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $exportPluginProperties; + } + + /** + * Outputs export header + * + * @return bool Whether it succeeded + */ + public function exportHeader() + { + global $charset; + + return $this->export->outputHandler( + ' + + + + + + + ' + ); + } + + /** + * Outputs export footer + * + * @return bool Whether it succeeded + */ + public function exportFooter() + { + return $this->export->outputHandler(''); + } + + /** + * Outputs database header + * + * @param string $db Database name + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBHeader($db, $db_alias = '') + { + if (empty($db_alias)) { + $db_alias = $db; + } + + return $this->export->outputHandler( + '

    ' . __('Database') . ' ' . htmlspecialchars($db_alias) . '

    ' + ); + } + + /** + * Outputs database footer + * + * @param string $db Database name + * + * @return bool Whether it succeeded + */ + public function exportDBFooter($db) + { + return true; + } + + /** + * Outputs CREATE DATABASE statement + * + * @param string $db Database name + * @param string $export_type 'server', 'database', 'table' + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBCreate($db, $export_type, $db_alias = '') + { + return true; + } + + /** + * Outputs the content of a table in HTML-Word format + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $sql_query SQL query for obtaining data + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportData( + $db, + $table, + $crlf, + $error_url, + $sql_query, + array $aliases = [] + ) { + global $what; + + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + + if (! $this->export->outputHandler( + '

    ' + . __('Dumping data for table') . ' ' . htmlspecialchars($table_alias) + . '

    ' + ) + ) { + return false; + } + if (! $this->export->outputHandler( + '' + ) + ) { + return false; + } + + // Gets the data from the database + $result = $GLOBALS['dbi']->query( + $sql_query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_UNBUFFERED + ); + $fields_cnt = $GLOBALS['dbi']->numFields($result); + + // If required, get fields name at the first line + if (isset($GLOBALS['htmlword_columns'])) { + $schema_insert = ''; + for ($i = 0; $i < $fields_cnt; $i++) { + $col_as = $GLOBALS['dbi']->fieldName($result, $i); + if (! empty($aliases[$db]['tables'][$table]['columns'][$col_as])) { + $col_as = $aliases[$db]['tables'][$table]['columns'][$col_as]; + } + $col_as = stripslashes($col_as); + $schema_insert .= ''; + } // end for + $schema_insert .= ''; + if (! $this->export->outputHandler($schema_insert)) { + return false; + } + } // end if + + // Format the data + while ($row = $GLOBALS['dbi']->fetchRow($result)) { + $schema_insert = ''; + for ($j = 0; $j < $fields_cnt; $j++) { + if (! isset($row[$j]) || $row[$j] === null) { + $value = $GLOBALS[$what . '_null']; + } elseif ($row[$j] == '0' || $row[$j] != '') { + $value = $row[$j]; + } else { + $value = ''; + } + $schema_insert .= ''; + } // end for + $schema_insert .= ''; + if (! $this->export->outputHandler($schema_insert)) { + return false; + } + } // end while + $GLOBALS['dbi']->freeResult($result); + + return $this->export->outputHandler('
    '); + } + + /** + * Returns a stand-in CREATE definition to resolve view dependencies + * + * @param string $db the database name + * @param string $view the view name + * @param string $crlf the end of line sequence + * @param array $aliases Aliases of db/table/columns + * + * @return string resulting definition + */ + public function getTableDefStandIn($db, $view, $crlf, $aliases = []) + { + $schema_insert = '' + . '' + . '' + . '' + . '' + . '' + . ''; + + /** + * Get the unique keys in the view + */ + $unique_keys = []; + $keys = $GLOBALS['dbi']->getTableIndexes($db, $view); + foreach ($keys as $key) { + if ($key['Non_unique'] == 0) { + $unique_keys[] = $key['Column_name']; + } + } + + $columns = $GLOBALS['dbi']->getColumns($db, $view); + foreach ($columns as $column) { + $col_as = $column['Field']; + if (! empty($aliases[$db]['tables'][$view]['columns'][$col_as])) { + $col_as = $aliases[$db]['tables'][$view]['columns'][$col_as]; + } + $schema_insert .= $this->formatOneColumnDefinition( + $column, + $unique_keys, + $col_as + ); + $schema_insert .= ''; + } + + $schema_insert .= '
    '; + + return $schema_insert; + } + + /** + * Returns $table's CREATE definition + * + * @param string $db the database name + * @param string $table the table name + * @param bool $do_relation whether to include relation comments + * @param bool $do_comments whether to include the pmadb-style column + * comments as comments in the structure; + * this is deprecated but the parameter is + * left here because export.php calls + * PMA_exportStructure() also for other + * export types which use this parameter + * @param bool $do_mime whether to include mime comments + * at the end + * @param bool $view whether we're handling a view + * @param array $aliases Aliases of db/table/columns + * + * @return string resulting schema + */ + public function getTableDef( + $db, + $table, + $do_relation, + $do_comments, + $do_mime, + $view = false, + array $aliases = [] + ) { + // set $cfgRelation here, because there is a chance that it's modified + // since the class initialization + global $cfgRelation; + + $schema_insert = ''; + + /** + * Gets fields properties + */ + $GLOBALS['dbi']->selectDb($db); + + // Check if we can use Relations + list($res_rel, $have_rel) = $this->relation->getRelationsAndStatus( + $do_relation && ! empty($cfgRelation['relation']), + $db, + $table + ); + + /** + * Displays the table structure + */ + $schema_insert .= ''; + + $schema_insert .= ''; + $schema_insert .= ''; + $schema_insert .= ''; + $schema_insert .= ''; + $schema_insert .= ''; + if ($do_relation && $have_rel) { + $schema_insert .= ''; + } + if ($do_comments) { + $schema_insert .= ''; + $comments = $this->relation->getComments($db, $table); + } + if ($do_mime && $cfgRelation['mimework']) { + $schema_insert .= ''; + $mime_map = $this->transformations->getMime($db, $table, true); + } + $schema_insert .= ''; + + $columns = $GLOBALS['dbi']->getColumns($db, $table); + /** + * Get the unique keys in the table + */ + $unique_keys = []; + $keys = $GLOBALS['dbi']->getTableIndexes($db, $table); + foreach ($keys as $key) { + if ($key['Non_unique'] == 0) { + $unique_keys[] = $key['Column_name']; + } + } + foreach ($columns as $column) { + $col_as = $column['Field']; + if (! empty($aliases[$db]['tables'][$table]['columns'][$col_as])) { + $col_as = $aliases[$db]['tables'][$table]['columns'][$col_as]; + } + $schema_insert .= $this->formatOneColumnDefinition( + $column, + $unique_keys, + $col_as + ); + $field_name = $column['Field']; + if ($do_relation && $have_rel) { + $schema_insert .= ''; + } + if ($do_comments && $cfgRelation['commwork']) { + $schema_insert .= ''; + } + if ($do_mime && $cfgRelation['mimework']) { + $schema_insert .= ''; + } + + $schema_insert .= ''; + } // end foreach + + $schema_insert .= '
    ' + . htmlspecialchars( + $this->getRelationString( + $res_rel, + $field_name, + $db, + $aliases + ) + ) + . '' + . (isset($comments[$field_name]) + ? htmlspecialchars($comments[$field_name]) + : '') . '' + . (isset($mime_map[$field_name]) ? + htmlspecialchars( + str_replace('_', '/', $mime_map[$field_name]['mimetype']) + ) + : '') . '
    '; + + return $schema_insert; + } + + /** + * Outputs triggers + * + * @param string $db database name + * @param string $table table name + * + * @return string Formatted triggers list + */ + protected function getTriggers($db, $table) + { + $dump = ''; + $dump .= ''; + $dump .= ''; + $dump .= ''; + $dump .= ''; + $dump .= ''; + $dump .= ''; + + $triggers = $GLOBALS['dbi']->getTriggers($db, $table); + + foreach ($triggers as $trigger) { + $dump .= ''; + $dump .= '' + . '' + . '' + . '' + . ''; + } + + $dump .= '
    '; + + return $dump; + } + + /** + * Outputs table's structure + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $export_mode 'create_table', 'triggers', 'create_view', + * 'stand_in' + * @param string $export_type 'server', 'database', 'table' + * @param bool $do_relation whether to include relation comments + * @param bool $do_comments whether to include the pmadb-style column + * comments as comments in the structure; + * this is deprecated but the parameter is + * left here because export.php calls + * PMA_exportStructure() also for other + * export types which use this parameter + * @param bool $do_mime whether to include mime comments + * @param bool $dates whether to include creation/update/check dates + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportStructure( + $db, + $table, + $crlf, + $error_url, + $export_mode, + $export_type, + $do_relation = false, + $do_comments = false, + $do_mime = false, + $dates = false, + array $aliases = [] + ) { + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + + $dump = ''; + + switch ($export_mode) { + case 'create_table': + $dump .= '

    ' + . __('Table structure for table') . ' ' + . htmlspecialchars($table_alias) + . '

    '; + $dump .= $this->getTableDef( + $db, + $table, + $do_relation, + $do_comments, + $do_mime, + false, + $aliases + ); + break; + case 'triggers': + $dump = ''; + $triggers = $GLOBALS['dbi']->getTriggers($db, $table); + if ($triggers) { + $dump .= '

    ' + . __('Triggers') . ' ' . htmlspecialchars($table_alias) + . '

    '; + $dump .= $this->getTriggers($db, $table); + } + break; + case 'create_view': + $dump .= '

    ' + . __('Structure for view') . ' ' . htmlspecialchars($table_alias) + . '

    '; + $dump .= $this->getTableDef( + $db, + $table, + $do_relation, + $do_comments, + $do_mime, + true, + $aliases + ); + break; + case 'stand_in': + $dump .= '

    ' + . __('Stand-in structure for view') . ' ' + . htmlspecialchars($table_alias) + . '

    '; + // export a stand-in definition to resolve view dependencies + $dump .= $this->getTableDefStandIn($db, $table, $crlf, $aliases); + } // end switch + + return $this->export->outputHandler($dump); + } + + /** + * Formats the definition for one column + * + * @param array $column info about this column + * @param array $unique_keys unique keys of the table + * @param string $col_alias Column Alias + * + * @return string Formatted column definition + */ + protected function formatOneColumnDefinition( + array $column, + array $unique_keys, + $col_alias = '' + ) { + if (empty($col_alias)) { + $col_alias = $column['Field']; + } + $definition = ''; + + $extracted_columnspec = Util::extractColumnSpec($column['Type']); + + $type = htmlspecialchars($extracted_columnspec['print_type']); + if (empty($type)) { + $type = ' '; + } + + if (! isset($column['Default'])) { + if ($column['Null'] != 'NO') { + $column['Default'] = 'NULL'; + } + } + + $fmt_pre = ''; + $fmt_post = ''; + if (in_array($column['Field'], $unique_keys)) { + $fmt_pre = '' . $fmt_pre; + $fmt_post .= ''; + } + if ($column['Key'] == 'PRI') { + $fmt_pre = '' . $fmt_pre; + $fmt_post .= ''; + } + $definition .= '' . $fmt_pre + . htmlspecialchars($col_alias) . $fmt_post . ''; + $definition .= '' . htmlspecialchars($type) . ''; + $definition .= '' + . (($column['Null'] == '' || $column['Null'] == 'NO') + ? __('No') + : __('Yes')) + . ''; + $definition .= '' + . htmlspecialchars(isset($column['Default']) ? $column['Default'] : '') + . ''; + + return $definition; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportJson.php b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportJson.php new file mode 100644 index 0000000..4ebfbbe --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportJson.php @@ -0,0 +1,295 @@ +setProperties(); + } + + /** + * Encodes the data into JSON + * + * @param mixed $data Data to encode + * + * @return string + */ + public function encode($data) + { + $options = 0; + if (isset($GLOBALS['json_pretty_print']) + && $GLOBALS['json_pretty_print'] + ) { + $options |= JSON_PRETTY_PRINT; + } + if (isset($GLOBALS['json_unicode']) + && $GLOBALS['json_unicode'] + ) { + $options |= JSON_UNESCAPED_UNICODE; + } + return json_encode($data, $options); + } + + /** + * Sets the export JSON properties + * + * @return void + */ + protected function setProperties() + { + $exportPluginProperties = new ExportPluginProperties(); + $exportPluginProperties->setText('JSON'); + $exportPluginProperties->setExtension('json'); + $exportPluginProperties->setMimeType('text/plain'); + $exportPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $exportPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new OptionsPropertyMainGroup("general_opts"); + // create primary items and add them to the group + $leaf = new HiddenPropertyItem("structure_or_data"); + $generalOptions->addProperty($leaf); + + $leaf = new BoolPropertyItem( + 'pretty_print', + __('Output pretty-printed JSON (Use human-readable formatting)') + ); + $generalOptions->addProperty($leaf); + + $leaf = new BoolPropertyItem( + 'unicode', + __('Output unicode characters unescaped') + ); + $generalOptions->addProperty($leaf); + + // add the main group to the root group + $exportSpecificOptions->addProperty($generalOptions); + + // set the options for the export plugin property item + $exportPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $exportPluginProperties; + } + + /** + * Outputs export header + * + * @return bool Whether it succeeded + */ + public function exportHeader() + { + global $crlf; + + $meta = [ + 'type' => 'header', + 'version' => PMA_VERSION, + 'comment' => 'Export to JSON plugin for PHPMyAdmin', + ]; + + return $this->export->outputHandler( + '[' . $crlf . $this->encode($meta) . ',' . $crlf + ); + } + + /** + * Outputs export footer + * + * @return bool Whether it succeeded + */ + public function exportFooter() + { + global $crlf; + + return $this->export->outputHandler(']' . $crlf); + } + + /** + * Outputs database header + * + * @param string $db Database name + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBHeader($db, $db_alias = '') + { + global $crlf; + + if (empty($db_alias)) { + $db_alias = $db; + } + + $meta = [ + 'type' => 'database', + 'name' => $db_alias, + ]; + + return $this->export->outputHandler( + $this->encode($meta) . ',' . $crlf + ); + } + + /** + * Outputs database footer + * + * @param string $db Database name + * + * @return bool Whether it succeeded + */ + public function exportDBFooter($db) + { + return true; + } + + /** + * Outputs CREATE DATABASE statement + * + * @param string $db Database name + * @param string $export_type 'server', 'database', 'table' + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBCreate($db, $export_type, $db_alias = '') + { + return true; + } + + /** + * Outputs the content of a table in JSON format + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $sql_query SQL query for obtaining data + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportData( + $db, + $table, + $crlf, + $error_url, + $sql_query, + array $aliases = [] + ) { + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + + if (! $this->first) { + if (! $this->export->outputHandler(',')) { + return false; + } + } else { + $this->first = false; + } + + $buffer = $this->encode( + [ + 'type' => 'table', + 'name' => $table_alias, + 'database' => $db_alias, + 'data' => "@@DATA@@", + ] + ); + list($header, $footer) = explode('"@@DATA@@"', $buffer); + + if (! $this->export->outputHandler($header . $crlf . '[' . $crlf)) { + return false; + } + + $result = $GLOBALS['dbi']->query( + $sql_query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_UNBUFFERED + ); + $columns_cnt = $GLOBALS['dbi']->numFields($result); + $fieldsMeta = $GLOBALS['dbi']->getFieldsMeta($result); + + $columns = []; + for ($i = 0; $i < $columns_cnt; $i++) { + $col_as = $GLOBALS['dbi']->fieldName($result, $i); + if (! empty($aliases[$db]['tables'][$table]['columns'][$col_as])) { + $col_as = $aliases[$db]['tables'][$table]['columns'][$col_as]; + } + $columns[$i] = stripslashes($col_as); + } + + $record_cnt = 0; + while ($record = $GLOBALS['dbi']->fetchRow($result)) { + $record_cnt++; + + // Output table name as comment if this is the first record of the table + if ($record_cnt > 1) { + if (! $this->export->outputHandler(',' . $crlf)) { + return false; + } + } + + $data = []; + + for ($i = 0; $i < $columns_cnt; $i++) { + if ($fieldsMeta[$i]->type === 'geometry') { + // export GIS types as hex + $record[$i] = '0x' . bin2hex($record[$i]); + } + $data[$columns[$i]] = $record[$i]; + } + + $encodedData = $this->encode($data); + if (! $encodedData) { + return false; + } + if (! $this->export->outputHandler($encodedData)) { + return false; + } + } + + if (! $this->export->outputHandler($crlf . ']' . $crlf . $footer . $crlf)) { + return false; + } + + $GLOBALS['dbi']->freeResult($result); + + return true; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportLatex.php b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportLatex.php new file mode 100644 index 0000000..e215300 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportLatex.php @@ -0,0 +1,709 @@ +initSpecificVariables(); + $this->setProperties(); + } + + /** + * Initialize the local variables that are used for export Latex + * + * @return void + */ + protected function initSpecificVariables() + { + /* Messages used in default captions */ + $GLOBALS['strLatexContent'] = __('Content of table @TABLE@'); + $GLOBALS['strLatexContinued'] = __('(continued)'); + $GLOBALS['strLatexStructure'] = __('Structure of table @TABLE@'); + } + + /** + * Sets the export Latex properties + * + * @return void + */ + protected function setProperties() + { + global $plugin_param; + $hide_structure = false; + if ($plugin_param['export_type'] == 'table' + && ! $plugin_param['single_table'] + ) { + $hide_structure = true; + } + + $exportPluginProperties = new ExportPluginProperties(); + $exportPluginProperties->setText('LaTeX'); + $exportPluginProperties->setExtension('tex'); + $exportPluginProperties->setMimeType('application/x-tex'); + $exportPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $exportPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new OptionsPropertyMainGroup("general_opts"); + // create primary items and add them to the group + $leaf = new BoolPropertyItem( + "caption", + __('Include table caption') + ); + $generalOptions->addProperty($leaf); + // add the main group to the root group + $exportSpecificOptions->addProperty($generalOptions); + + // what to dump (structure/data/both) main group + $dumpWhat = new OptionsPropertyMainGroup( + "dump_what", + __('Dump table') + ); + // create primary items and add them to the group + $leaf = new RadioPropertyItem("structure_or_data"); + $leaf->setValues( + [ + 'structure' => __('structure'), + 'data' => __('data'), + 'structure_and_data' => __('structure and data'), + ] + ); + $dumpWhat->addProperty($leaf); + // add the main group to the root group + $exportSpecificOptions->addProperty($dumpWhat); + + // structure options main group + if (! $hide_structure) { + $structureOptions = new OptionsPropertyMainGroup( + "structure", + __('Object creation options') + ); + $structureOptions->setForce('data'); + // create primary items and add them to the group + $leaf = new TextPropertyItem( + "structure_caption", + __('Table caption:') + ); + $leaf->setDoc('faq6-27'); + $structureOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + "structure_continued_caption", + __('Table caption (continued):') + ); + $leaf->setDoc('faq6-27'); + $structureOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + "structure_label", + __('Label key:') + ); + $leaf->setDoc('faq6-27'); + $structureOptions->addProperty($leaf); + if (! empty($GLOBALS['cfgRelation']['relation'])) { + $leaf = new BoolPropertyItem( + "relation", + __('Display foreign key relationships') + ); + $structureOptions->addProperty($leaf); + } + $leaf = new BoolPropertyItem( + "comments", + __('Display comments') + ); + $structureOptions->addProperty($leaf); + if (! empty($GLOBALS['cfgRelation']['mimework'])) { + $leaf = new BoolPropertyItem( + "mime", + __('Display media (MIME) types') + ); + $structureOptions->addProperty($leaf); + } + // add the main group to the root group + $exportSpecificOptions->addProperty($structureOptions); + } + + // data options main group + $dataOptions = new OptionsPropertyMainGroup( + "data", + __('Data dump options') + ); + $dataOptions->setForce('structure'); + // create primary items and add them to the group + $leaf = new BoolPropertyItem( + "columns", + __('Put columns names in the first row:') + ); + $dataOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + "data_caption", + __('Table caption:') + ); + $leaf->setDoc('faq6-27'); + $dataOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + "data_continued_caption", + __('Table caption (continued):') + ); + $leaf->setDoc('faq6-27'); + $dataOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + "data_label", + __('Label key:') + ); + $leaf->setDoc('faq6-27'); + $dataOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + 'null', + __('Replace NULL with:') + ); + $dataOptions->addProperty($leaf); + // add the main group to the root group + $exportSpecificOptions->addProperty($dataOptions); + + // set the options for the export plugin property item + $exportPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $exportPluginProperties; + } + + /** + * Outputs export header + * + * @return bool Whether it succeeded + */ + public function exportHeader() + { + global $crlf; + global $cfg; + + $head = '% phpMyAdmin LaTeX Dump' . $crlf + . '% version ' . PMA_VERSION . $crlf + . '% https://www.phpmyadmin.net/' . $crlf + . '%' . $crlf + . '% ' . __('Host:') . ' ' . $cfg['Server']['host']; + if (! empty($cfg['Server']['port'])) { + $head .= ':' . $cfg['Server']['port']; + } + $head .= $crlf + . '% ' . __('Generation Time:') . ' ' + . Util::localisedDate() . $crlf + . '% ' . __('Server version:') . ' ' . $GLOBALS['dbi']->getVersionString() . $crlf + . '% ' . __('PHP Version:') . ' ' . PHP_VERSION . $crlf; + + return $this->export->outputHandler($head); + } + + /** + * Outputs export footer + * + * @return bool Whether it succeeded + */ + public function exportFooter() + { + return true; + } + + /** + * Outputs database header + * + * @param string $db Database name + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBHeader($db, $db_alias = '') + { + if (empty($db_alias)) { + $db_alias = $db; + } + global $crlf; + $head = '% ' . $crlf + . '% ' . __('Database:') . ' \'' . $db_alias . '\'' . $crlf + . '% ' . $crlf; + + return $this->export->outputHandler($head); + } + + /** + * Outputs database footer + * + * @param string $db Database name + * + * @return bool Whether it succeeded + */ + public function exportDBFooter($db) + { + return true; + } + + /** + * Outputs CREATE DATABASE statement + * + * @param string $db Database name + * @param string $export_type 'server', 'database', 'table' + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBCreate($db, $export_type, $db_alias = '') + { + return true; + } + + /** + * Outputs the content of a table in JSON format + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $sql_query SQL query for obtaining data + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportData( + $db, + $table, + $crlf, + $error_url, + $sql_query, + array $aliases = [] + ) { + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + + $result = $GLOBALS['dbi']->tryQuery( + $sql_query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_UNBUFFERED + ); + + $columns_cnt = $GLOBALS['dbi']->numFields($result); + $columns = []; + $columns_alias = []; + for ($i = 0; $i < $columns_cnt; $i++) { + $columns[$i] = $col_as = $GLOBALS['dbi']->fieldName($result, $i); + if (! empty($aliases[$db]['tables'][$table]['columns'][$col_as])) { + $col_as = $aliases[$db]['tables'][$table]['columns'][$col_as]; + } + $columns_alias[$i] = $col_as; + } + + $buffer = $crlf . '%' . $crlf . '% ' . __('Data:') . ' ' . $table_alias + . $crlf . '%' . $crlf . ' \\begin{longtable}{|'; + + for ($index = 0; $index < $columns_cnt; $index++) { + $buffer .= 'l|'; + } + $buffer .= '} ' . $crlf; + + $buffer .= ' \\hline \\endhead \\hline \\endfoot \\hline ' . $crlf; + if (isset($GLOBALS['latex_caption'])) { + $buffer .= ' \\caption{' + . Util::expandUserString( + $GLOBALS['latex_data_caption'], + [ + 'texEscape', + static::class, + ], + [ + 'table' => $table_alias, + 'database' => $db_alias, + ] + ) + . '} \\label{' + . Util::expandUserString( + $GLOBALS['latex_data_label'], + null, + [ + 'table' => $table_alias, + 'database' => $db_alias, + ] + ) + . '} \\\\'; + } + if (! $this->export->outputHandler($buffer)) { + return false; + } + + // show column names + if (isset($GLOBALS['latex_columns'])) { + $buffer = '\\hline '; + for ($i = 0; $i < $columns_cnt; $i++) { + $buffer .= '\\multicolumn{1}{|c|}{\\textbf{' + . self::texEscape(stripslashes($columns_alias[$i])) . '}} & '; + } + + $buffer = mb_substr($buffer, 0, -2) . '\\\\ \\hline \hline '; + if (! $this->export->outputHandler($buffer . ' \\endfirsthead ' . $crlf)) { + return false; + } + if (isset($GLOBALS['latex_caption'])) { + if (! $this->export->outputHandler( + '\\caption{' + . Util::expandUserString( + $GLOBALS['latex_data_continued_caption'], + [ + 'texEscape', + static::class, + ], + [ + 'table' => $table_alias, + 'database' => $db_alias, + ] + ) + . '} \\\\ ' + ) + ) { + return false; + } + } + if (! $this->export->outputHandler($buffer . '\\endhead \\endfoot' . $crlf)) { + return false; + } + } else { + if (! $this->export->outputHandler('\\\\ \hline')) { + return false; + } + } + + // print the whole table + while ($record = $GLOBALS['dbi']->fetchAssoc($result)) { + $buffer = ''; + // print each row + for ($i = 0; $i < $columns_cnt; $i++) { + if ($record[$columns[$i]] !== null + && isset($record[$columns[$i]]) + ) { + $column_value = self::texEscape( + stripslashes($record[$columns[$i]]) + ); + } else { + $column_value = $GLOBALS['latex_null']; + } + + // last column ... no need for & character + if ($i == ($columns_cnt - 1)) { + $buffer .= $column_value; + } else { + $buffer .= $column_value . " & "; + } + } + $buffer .= ' \\\\ \\hline ' . $crlf; + if (! $this->export->outputHandler($buffer)) { + return false; + } + } + + $buffer = ' \\end{longtable}' . $crlf; + if (! $this->export->outputHandler($buffer)) { + return false; + } + + $GLOBALS['dbi']->freeResult($result); + + return true; + } // end getTableLaTeX + + /** + * Outputs table's structure + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $export_mode 'create_table', 'triggers', 'create_view', + * 'stand_in' + * @param string $export_type 'server', 'database', 'table' + * @param bool $do_relation whether to include relation comments + * @param bool $do_comments whether to include the pmadb-style column + * comments as comments in the structure; + * this is deprecated but the parameter is + * left here because export.php calls + * exportStructure() also for other + * export types which use this parameter + * @param bool $do_mime whether to include mime comments + * @param bool $dates whether to include creation/update/check dates + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportStructure( + $db, + $table, + $crlf, + $error_url, + $export_mode, + $export_type, + $do_relation = false, + $do_comments = false, + $do_mime = false, + $dates = false, + array $aliases = [] + ) { + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + + global $cfgRelation; + + /* We do not export triggers */ + if ($export_mode == 'triggers') { + return true; + } + + /** + * Get the unique keys in the table + */ + $unique_keys = []; + $keys = $GLOBALS['dbi']->getTableIndexes($db, $table); + foreach ($keys as $key) { + if ($key['Non_unique'] == 0) { + $unique_keys[] = $key['Column_name']; + } + } + + /** + * Gets fields properties + */ + $GLOBALS['dbi']->selectDb($db); + + // Check if we can use Relations + list($res_rel, $have_rel) = $this->relation->getRelationsAndStatus( + $do_relation && ! empty($cfgRelation['relation']), + $db, + $table + ); + /** + * Displays the table structure + */ + $buffer = $crlf . '%' . $crlf . '% ' . __('Structure:') . ' ' + . $table_alias . $crlf . '%' . $crlf . ' \\begin{longtable}{'; + if (! $this->export->outputHandler($buffer)) { + return false; + } + + $alignment = '|l|c|c|c|'; + if ($do_relation && $have_rel) { + $alignment .= 'l|'; + } + if ($do_comments) { + $alignment .= 'l|'; + } + if ($do_mime && $cfgRelation['mimework']) { + $alignment .= 'l|'; + } + $buffer = $alignment . '} ' . $crlf; + + $header = ' \\hline '; + $header .= '\\multicolumn{1}{|c|}{\\textbf{' . __('Column') + . '}} & \\multicolumn{1}{|c|}{\\textbf{' . __('Type') + . '}} & \\multicolumn{1}{|c|}{\\textbf{' . __('Null') + . '}} & \\multicolumn{1}{|c|}{\\textbf{' . __('Default') . '}}'; + if ($do_relation && $have_rel) { + $header .= ' & \\multicolumn{1}{|c|}{\\textbf{' . __('Links to') . '}}'; + } + if ($do_comments) { + $header .= ' & \\multicolumn{1}{|c|}{\\textbf{' . __('Comments') . '}}'; + $comments = $this->relation->getComments($db, $table); + } + if ($do_mime && $cfgRelation['mimework']) { + $header .= ' & \\multicolumn{1}{|c|}{\\textbf{MIME}}'; + $mime_map = $this->transformations->getMime($db, $table, true); + } + + // Table caption for first page and label + if (isset($GLOBALS['latex_caption'])) { + $buffer .= ' \\caption{' + . Util::expandUserString( + $GLOBALS['latex_structure_caption'], + [ + 'texEscape', + static::class, + ], + [ + 'table' => $table_alias, + 'database' => $db_alias, + ] + ) + . '} \\label{' + . Util::expandUserString( + $GLOBALS['latex_structure_label'], + null, + [ + 'table' => $table_alias, + 'database' => $db_alias, + ] + ) + . '} \\\\' . $crlf; + } + $buffer .= $header . ' \\\\ \\hline \\hline' . $crlf + . '\\endfirsthead' . $crlf; + // Table caption on next pages + if (isset($GLOBALS['latex_caption'])) { + $buffer .= ' \\caption{' + . Util::expandUserString( + $GLOBALS['latex_structure_continued_caption'], + [ + 'texEscape', + static::class, + ], + [ + 'table' => $table_alias, + 'database' => $db_alias, + ] + ) + . '} \\\\ ' . $crlf; + } + $buffer .= $header . ' \\\\ \\hline \\hline \\endhead \\endfoot ' . $crlf; + + if (! $this->export->outputHandler($buffer)) { + return false; + } + + $fields = $GLOBALS['dbi']->getColumns($db, $table); + foreach ($fields as $row) { + $extracted_columnspec = Util::extractColumnSpec($row['Type']); + $type = $extracted_columnspec['print_type']; + if (empty($type)) { + $type = ' '; + } + + if (! isset($row['Default'])) { + if ($row['Null'] != 'NO') { + $row['Default'] = 'NULL'; + } + } + + $field_name = $col_as = $row['Field']; + if (! empty($aliases[$db]['tables'][$table]['columns'][$col_as])) { + $col_as = $aliases[$db]['tables'][$table]['columns'][$col_as]; + } + + $local_buffer = $col_as . "\000" . $type . "\000" + . (($row['Null'] == '' || $row['Null'] == 'NO') + ? __('No') : __('Yes')) + . "\000" . (isset($row['Default']) ? $row['Default'] : ''); + + if ($do_relation && $have_rel) { + $local_buffer .= "\000"; + $local_buffer .= $this->getRelationString( + $res_rel, + $field_name, + $db, + $aliases + ); + } + if ($do_comments && $cfgRelation['commwork']) { + $local_buffer .= "\000"; + if (isset($comments[$field_name])) { + $local_buffer .= $comments[$field_name]; + } + } + if ($do_mime && $cfgRelation['mimework']) { + $local_buffer .= "\000"; + if (isset($mime_map[$field_name])) { + $local_buffer .= str_replace( + '_', + '/', + $mime_map[$field_name]['mimetype'] + ); + } + } + $local_buffer = self::texEscape($local_buffer); + if ($row['Key'] == 'PRI') { + $pos = mb_strpos($local_buffer, "\000"); + $local_buffer = '\\textit{' + . + mb_substr($local_buffer, 0, $pos) + . '}' . + mb_substr($local_buffer, $pos); + } + if (in_array($field_name, $unique_keys)) { + $pos = mb_strpos($local_buffer, "\000"); + $local_buffer = '\\textbf{' + . + mb_substr($local_buffer, 0, $pos) + . '}' . + mb_substr($local_buffer, $pos); + } + $buffer = str_replace("\000", ' & ', $local_buffer); + $buffer .= ' \\\\ \\hline ' . $crlf; + + if (! $this->export->outputHandler($buffer)) { + return false; + } + } // end while + + $buffer = ' \\end{longtable}' . $crlf; + + return $this->export->outputHandler($buffer); + } // end of the 'exportStructure' method + + /** + * Escapes some special characters for use in TeX/LaTeX + * + * @param string $string the string to convert + * + * @return string the converted string with escape codes + */ + public static function texEscape($string) + { + $escape = [ + '$', + '%', + '{', + '}', + '&', + '#', + '_', + '^', + ]; + $cnt_escape = count($escape); + for ($k = 0; $k < $cnt_escape; $k++) { + $string = str_replace($escape[$k], '\\' . $escape[$k], $string); + } + + return $string; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportMediawiki.php b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportMediawiki.php new file mode 100644 index 0000000..9d971cb --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportMediawiki.php @@ -0,0 +1,386 @@ +setProperties(); + } + + /** + * Sets the export MediaWiki properties + * + * @return void + */ + protected function setProperties() + { + $exportPluginProperties = new ExportPluginProperties(); + $exportPluginProperties->setText('MediaWiki Table'); + $exportPluginProperties->setExtension('mediawiki'); + $exportPluginProperties->setMimeType('text/plain'); + $exportPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $exportPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new OptionsPropertyMainGroup( + "general_opts", + __('Dump table') + ); + + // what to dump (structure/data/both) + $subgroup = new OptionsPropertySubgroup( + "dump_table", + __("Dump table") + ); + $leaf = new RadioPropertyItem('structure_or_data'); + $leaf->setValues( + [ + 'structure' => __('structure'), + 'data' => __('data'), + 'structure_and_data' => __('structure and data'), + ] + ); + $subgroup->setSubgroupHeader($leaf); + $generalOptions->addProperty($subgroup); + + // export table name + $leaf = new BoolPropertyItem( + "caption", + __('Export table names') + ); + $generalOptions->addProperty($leaf); + + // export table headers + $leaf = new BoolPropertyItem( + "headers", + __('Export table headers') + ); + $generalOptions->addProperty($leaf); + //add the main group to the root group + $exportSpecificOptions->addProperty($generalOptions); + + // set the options for the export plugin property item + $exportPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $exportPluginProperties; + } + + /** + * Outputs export header + * + * @return bool Whether it succeeded + */ + public function exportHeader() + { + return true; + } + + /** + * Outputs export footer + * + * @return bool Whether it succeeded + */ + public function exportFooter() + { + return true; + } + + /** + * Outputs database header + * + * @param string $db Database name + * @param string $db_alias Alias of db + * + * @return bool Whether it succeeded + */ + public function exportDBHeader($db, $db_alias = '') + { + return true; + } + + /** + * Outputs database footer + * + * @param string $db Database name + * + * @return bool Whether it succeeded + */ + public function exportDBFooter($db) + { + return true; + } + + /** + * Outputs CREATE DATABASE statement + * + * @param string $db Database name + * @param string $export_type 'server', 'database', 'table' + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBCreate($db, $export_type, $db_alias = '') + { + return true; + } + + /** + * Outputs table's structure + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $export_mode 'create_table','triggers','create_view', + * 'stand_in' + * @param string $export_type 'server', 'database', 'table' + * @param bool $do_relation whether to include relation comments + * @param bool $do_comments whether to include the pmadb-style column + * comments as comments in the structure; this is + * deprecated but the parameter is left here + * because export.php calls exportStructure() + * also for other export types which use this + * parameter + * @param bool $do_mime whether to include mime comments + * @param bool $dates whether to include creation/update/check dates + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportStructure( + $db, + $table, + $crlf, + $error_url, + $export_mode, + $export_type, + $do_relation = false, + $do_comments = false, + $do_mime = false, + $dates = false, + array $aliases = [] + ) { + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + + $output = ''; + switch ($export_mode) { + case 'create_table': + $columns = $GLOBALS['dbi']->getColumns($db, $table); + $columns = array_values($columns); + $row_cnt = count($columns); + + // Print structure comment + $output = $this->_exportComment( + "Table structure for " + . Util::backquote($table_alias) + ); + + // Begin the table construction + $output .= "{| class=\"wikitable\" style=\"text-align:center;\"" + . $this->_exportCRLF(); + + // Add the table name + if (isset($GLOBALS['mediawiki_caption'])) { + $output .= "|+'''" . $table_alias . "'''" . $this->_exportCRLF(); + } + + // Add the table headers + if (isset($GLOBALS['mediawiki_headers'])) { + $output .= "|- style=\"background:#ffdead;\"" . $this->_exportCRLF(); + $output .= "! style=\"background:#ffffff\" | " + . $this->_exportCRLF(); + for ($i = 0; $i < $row_cnt; ++$i) { + $col_as = $columns[$i]['Field']; + if (! empty($aliases[$db]['tables'][$table]['columns'][$col_as]) + ) { + $col_as + = $aliases[$db]['tables'][$table]['columns'][$col_as]; + } + $output .= " | " . $col_as . $this->_exportCRLF(); + } + } + + // Add the table structure + $output .= "|-" . $this->_exportCRLF(); + $output .= "! Type" . $this->_exportCRLF(); + for ($i = 0; $i < $row_cnt; ++$i) { + $output .= " | " . $columns[$i]['Type'] . $this->_exportCRLF(); + } + + $output .= "|-" . $this->_exportCRLF(); + $output .= "! Null" . $this->_exportCRLF(); + for ($i = 0; $i < $row_cnt; ++$i) { + $output .= " | " . $columns[$i]['Null'] . $this->_exportCRLF(); + } + + $output .= "|-" . $this->_exportCRLF(); + $output .= "! Default" . $this->_exportCRLF(); + for ($i = 0; $i < $row_cnt; ++$i) { + $output .= " | " . $columns[$i]['Default'] . $this->_exportCRLF(); + } + + $output .= "|-" . $this->_exportCRLF(); + $output .= "! Extra" . $this->_exportCRLF(); + for ($i = 0; $i < $row_cnt; ++$i) { + $output .= " | " . $columns[$i]['Extra'] . $this->_exportCRLF(); + } + + $output .= "|}" . str_repeat($this->_exportCRLF(), 2); + break; + } // end switch + + return $this->export->outputHandler($output); + } + + /** + * Outputs the content of a table in MediaWiki format + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $sql_query SQL query for obtaining data + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportData( + $db, + $table, + $crlf, + $error_url, + $sql_query, + array $aliases = [] + ) { + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + + // Print data comment + $output = $this->_exportComment( + "Table data for " . Util::backquote($table_alias) + ); + + // Begin the table construction + // Use the "wikitable" class for style + // Use the "sortable" class for allowing tables to be sorted by column + $output .= "{| class=\"wikitable sortable\" style=\"text-align:center;\"" + . $this->_exportCRLF(); + + // Add the table name + if (isset($GLOBALS['mediawiki_caption'])) { + $output .= "|+'''" . $table_alias . "'''" . $this->_exportCRLF(); + } + + // Add the table headers + if (isset($GLOBALS['mediawiki_headers'])) { + // Get column names + $column_names = $GLOBALS['dbi']->getColumnNames($db, $table); + + // Add column names as table headers + if ($column_names !== null) { + // Use '|-' for separating rows + $output .= "|-" . $this->_exportCRLF(); + + // Use '!' for separating table headers + foreach ($column_names as $column) { + if (! empty($aliases[$db]['tables'][$table]['columns'][$column]) + ) { + $column + = $aliases[$db]['tables'][$table]['columns'][$column]; + } + $output .= " ! " . $column . "" . $this->_exportCRLF(); + } + } + } + + // Get the table data from the database + $result = $GLOBALS['dbi']->query( + $sql_query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_UNBUFFERED + ); + $fields_cnt = $GLOBALS['dbi']->numFields($result); + + while ($row = $GLOBALS['dbi']->fetchRow($result)) { + $output .= "|-" . $this->_exportCRLF(); + + // Use '|' for separating table columns + for ($i = 0; $i < $fields_cnt; ++$i) { + $output .= " | " . $row[$i] . "" . $this->_exportCRLF(); + } + } + + // End table construction + $output .= "|}" . str_repeat($this->_exportCRLF(), 2); + + return $this->export->outputHandler($output); + } + + /** + * Outputs comments containing info about the exported tables + * + * @param string $text Text of comment + * + * @return string The formatted comment + */ + private function _exportComment($text = '') + { + // see https://www.mediawiki.org/wiki/Help:Formatting + $comment = $this->_exportCRLF(); + $comment .= '' . str_repeat($this->_exportCRLF(), 2); + + return $comment; + } + + /** + * Outputs CRLF + * + * @return string CRLF + */ + private function _exportCRLF() + { + // The CRLF expected by the mediawiki format is "\n" + return "\n"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportOds.php b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportOds.php new file mode 100644 index 0000000..bc062da --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportOds.php @@ -0,0 +1,345 @@ +setProperties(); + } + + /** + * Sets the export ODS properties + * + * @return void + */ + protected function setProperties() + { + $exportPluginProperties = new ExportPluginProperties(); + $exportPluginProperties->setText('OpenDocument Spreadsheet'); + $exportPluginProperties->setExtension('ods'); + $exportPluginProperties->setMimeType( + 'application/vnd.oasis.opendocument.spreadsheet' + ); + $exportPluginProperties->setForceFile(true); + $exportPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $exportPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new OptionsPropertyMainGroup("general_opts"); + // create primary items and add them to the group + $leaf = new TextPropertyItem( + "null", + __('Replace NULL with:') + ); + $generalOptions->addProperty($leaf); + $leaf = new BoolPropertyItem( + "columns", + __('Put columns names in the first row') + ); + $generalOptions->addProperty($leaf); + $leaf = new HiddenPropertyItem("structure_or_data"); + $generalOptions->addProperty($leaf); + // add the main group to the root group + $exportSpecificOptions->addProperty($generalOptions); + + // set the options for the export plugin property item + $exportPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $exportPluginProperties; + } + + /** + * Outputs export header + * + * @return bool Whether it succeeded + */ + public function exportHeader() + { + $GLOBALS['ods_buffer'] .= '' + . '' + . '' + . '' + . '' + . '/' + . '' + . '/' + . '' + . '' + . '' + . '' + . ':' + . '' + . ':' + . '' + . ' ' + . '' + . '' + . '' + . '' + . '/' + . '' + . '/' + . '' + . ' ' + . '' + . ':' + . '' + . ' ' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . ''; + + return true; + } + + /** + * Outputs export footer + * + * @return bool Whether it succeeded + */ + public function exportFooter() + { + $GLOBALS['ods_buffer'] .= '' + . '' + . ''; + + return $this->export->outputHandler( + OpenDocument::create( + 'application/vnd.oasis.opendocument.spreadsheet', + $GLOBALS['ods_buffer'] + ) + ); + } + + /** + * Outputs database header + * + * @param string $db Database name + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBHeader($db, $db_alias = '') + { + return true; + } + + /** + * Outputs database footer + * + * @param string $db Database name + * + * @return bool Whether it succeeded + */ + public function exportDBFooter($db) + { + return true; + } + + /** + * Outputs CREATE DATABASE statement + * + * @param string $db Database name + * @param string $export_type 'server', 'database', 'table' + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBCreate($db, $export_type, $db_alias = '') + { + return true; + } + + /** + * Outputs the content of a table in NHibernate format + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $sql_query SQL query for obtaining data + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportData( + $db, + $table, + $crlf, + $error_url, + $sql_query, + array $aliases = [] + ) { + global $what; + + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + // Gets the data from the database + $result = $GLOBALS['dbi']->query( + $sql_query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_UNBUFFERED + ); + $fields_cnt = $GLOBALS['dbi']->numFields($result); + $fields_meta = $GLOBALS['dbi']->getFieldsMeta($result); + $field_flags = []; + for ($j = 0; $j < $fields_cnt; $j++) { + $field_flags[$j] = $GLOBALS['dbi']->fieldFlags($result, $j); + } + + $GLOBALS['ods_buffer'] + .= ''; + + // If required, get fields name at the first line + if (isset($GLOBALS[$what . '_columns'])) { + $GLOBALS['ods_buffer'] .= ''; + for ($i = 0; $i < $fields_cnt; $i++) { + $col_as = $GLOBALS['dbi']->fieldName($result, $i); + if (! empty($aliases[$db]['tables'][$table]['columns'][$col_as])) { + $col_as = $aliases[$db]['tables'][$table]['columns'][$col_as]; + } + $GLOBALS['ods_buffer'] + .= '' + . '' + . htmlspecialchars( + stripslashes($col_as) + ) + . '' + . ''; + } // end for + $GLOBALS['ods_buffer'] .= ''; + } // end if + + // Format the data + while ($row = $GLOBALS['dbi']->fetchRow($result)) { + $GLOBALS['ods_buffer'] .= ''; + for ($j = 0; $j < $fields_cnt; $j++) { + if ($fields_meta[$j]->type === 'geometry') { + // export GIS types as hex + $row[$j] = '0x' . bin2hex($row[$j]); + } + if (! isset($row[$j]) || $row[$j] === null) { + $GLOBALS['ods_buffer'] + .= '' + . '' + . htmlspecialchars($GLOBALS[$what . '_null']) + . '' + . ''; + } elseif (false !== stripos($field_flags[$j], 'BINARY') + && $fields_meta[$j]->blob + ) { + // ignore BLOB + $GLOBALS['ods_buffer'] + .= '' + . '' + . ''; + } elseif ($fields_meta[$j]->type == "date") { + $GLOBALS['ods_buffer'] + .= '' + . '' + . htmlspecialchars($row[$j]) + . '' + . ''; + } elseif ($fields_meta[$j]->type == "time") { + $GLOBALS['ods_buffer'] + .= '' + . '' + . htmlspecialchars($row[$j]) + . '' + . ''; + } elseif ($fields_meta[$j]->type == "datetime") { + $GLOBALS['ods_buffer'] + .= '' + . '' + . htmlspecialchars($row[$j]) + . '' + . ''; + } elseif (($fields_meta[$j]->numeric + && $fields_meta[$j]->type != 'timestamp' + && ! $fields_meta[$j]->blob) + || $fields_meta[$j]->type == 'real' + ) { + $GLOBALS['ods_buffer'] + .= '' + . '' + . htmlspecialchars($row[$j]) + . '' + . ''; + } else { + $GLOBALS['ods_buffer'] + .= '' + . '' + . htmlspecialchars($row[$j]) + . '' + . ''; + } + } // end for + $GLOBALS['ods_buffer'] .= ''; + } // end while + $GLOBALS['dbi']->freeResult($result); + + $GLOBALS['ods_buffer'] .= ''; + + return true; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportOdt.php b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportOdt.php new file mode 100644 index 0000000..8173afb --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportOdt.php @@ -0,0 +1,813 @@ +setProperties(); + } + + /** + * Sets the export ODT properties + * + * @return void + */ + protected function setProperties() + { + global $plugin_param; + $hide_structure = false; + if ($plugin_param['export_type'] == 'table' + && ! $plugin_param['single_table'] + ) { + $hide_structure = true; + } + + $exportPluginProperties = new ExportPluginProperties(); + $exportPluginProperties->setText('OpenDocument Text'); + $exportPluginProperties->setExtension('odt'); + $exportPluginProperties->setMimeType( + 'application/vnd.oasis.opendocument.text' + ); + $exportPluginProperties->setForceFile(true); + $exportPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $exportPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // what to dump (structure/data/both) main group + $dumpWhat = new OptionsPropertyMainGroup( + "general_opts", + __('Dump table') + ); + // create primary items and add them to the group + $leaf = new RadioPropertyItem("structure_or_data"); + $leaf->setValues( + [ + 'structure' => __('structure'), + 'data' => __('data'), + 'structure_and_data' => __('structure and data'), + ] + ); + $dumpWhat->addProperty($leaf); + // add the main group to the root group + $exportSpecificOptions->addProperty($dumpWhat); + + // structure options main group + if (! $hide_structure) { + $structureOptions = new OptionsPropertyMainGroup( + "structure", + __('Object creation options') + ); + $structureOptions->setForce('data'); + // create primary items and add them to the group + if (! empty($GLOBALS['cfgRelation']['relation'])) { + $leaf = new BoolPropertyItem( + "relation", + __('Display foreign key relationships') + ); + $structureOptions->addProperty($leaf); + } + $leaf = new BoolPropertyItem( + "comments", + __('Display comments') + ); + $structureOptions->addProperty($leaf); + if (! empty($GLOBALS['cfgRelation']['mimework'])) { + $leaf = new BoolPropertyItem( + "mime", + __('Display media (MIME) types') + ); + $structureOptions->addProperty($leaf); + } + // add the main group to the root group + $exportSpecificOptions->addProperty($structureOptions); + } + + // data options main group + $dataOptions = new OptionsPropertyMainGroup( + "data", + __('Data dump options') + ); + $dataOptions->setForce('structure'); + // create primary items and add them to the group + $leaf = new BoolPropertyItem( + "columns", + __('Put columns names in the first row') + ); + $dataOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + 'null', + __('Replace NULL with:') + ); + $dataOptions->addProperty($leaf); + // add the main group to the root group + $exportSpecificOptions->addProperty($dataOptions); + + // set the options for the export plugin property item + $exportPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $exportPluginProperties; + } + + /** + * Outputs export header + * + * @return bool Whether it succeeded + */ + public function exportHeader() + { + $GLOBALS['odt_buffer'] .= '' + . '' + . '' + . ''; + + return true; + } + + /** + * Outputs export footer + * + * @return bool Whether it succeeded + */ + public function exportFooter() + { + $GLOBALS['odt_buffer'] .= '' + . '' + . ''; + if (! $this->export->outputHandler( + OpenDocument::create( + 'application/vnd.oasis.opendocument.text', + $GLOBALS['odt_buffer'] + ) + ) + ) { + return false; + } + + return true; + } + + /** + * Outputs database header + * + * @param string $db Database name + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBHeader($db, $db_alias = '') + { + if (empty($db_alias)) { + $db_alias = $db; + } + $GLOBALS['odt_buffer'] + .= '' + . __('Database') . ' ' . htmlspecialchars($db_alias) + . ''; + + return true; + } + + /** + * Outputs database footer + * + * @param string $db Database name + * + * @return bool Whether it succeeded + */ + public function exportDBFooter($db) + { + return true; + } + + /** + * Outputs CREATE DATABASE statement + * + * @param string $db Database name + * @param string $export_type 'server', 'database', 'table' + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBCreate($db, $export_type, $db_alias = '') + { + return true; + } + + /** + * Outputs the content of a table in NHibernate format + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $sql_query SQL query for obtaining data + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportData( + $db, + $table, + $crlf, + $error_url, + $sql_query, + array $aliases = [] + ) { + global $what; + + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + // Gets the data from the database + $result = $GLOBALS['dbi']->query( + $sql_query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_UNBUFFERED + ); + $fields_cnt = $GLOBALS['dbi']->numFields($result); + $fields_meta = $GLOBALS['dbi']->getFieldsMeta($result); + $field_flags = []; + for ($j = 0; $j < $fields_cnt; $j++) { + $field_flags[$j] = $GLOBALS['dbi']->fieldFlags($result, $j); + } + + $GLOBALS['odt_buffer'] + .= '' + . __('Dumping data for table') . ' ' . htmlspecialchars($table_alias) + . '' + . '' + . ''; + + // If required, get fields name at the first line + if (isset($GLOBALS[$what . '_columns'])) { + $GLOBALS['odt_buffer'] .= ''; + for ($i = 0; $i < $fields_cnt; $i++) { + $col_as = $GLOBALS['dbi']->fieldName($result, $i); + if (! empty($aliases[$db]['tables'][$table]['columns'][$col_as])) { + $col_as = $aliases[$db]['tables'][$table]['columns'][$col_as]; + } + $GLOBALS['odt_buffer'] + .= '' + . '' + . htmlspecialchars( + stripslashes($col_as) + ) + . '' + . ''; + } // end for + $GLOBALS['odt_buffer'] .= ''; + } // end if + + // Format the data + while ($row = $GLOBALS['dbi']->fetchRow($result)) { + $GLOBALS['odt_buffer'] .= ''; + for ($j = 0; $j < $fields_cnt; $j++) { + if ($fields_meta[$j]->type === 'geometry') { + // export GIS types as hex + $row[$j] = '0x' . bin2hex($row[$j]); + } + if (! isset($row[$j]) || $row[$j] === null) { + $GLOBALS['odt_buffer'] + .= '' + . '' + . htmlspecialchars($GLOBALS[$what . '_null']) + . '' + . ''; + } elseif (false !== stripos($field_flags[$j], 'BINARY') + && $fields_meta[$j]->blob + ) { + // ignore BLOB + $GLOBALS['odt_buffer'] + .= '' + . '' + . ''; + } elseif ($fields_meta[$j]->numeric + && $fields_meta[$j]->type != 'timestamp' + && ! $fields_meta[$j]->blob + ) { + $GLOBALS['odt_buffer'] + .= '' + . '' + . htmlspecialchars($row[$j]) + . '' + . ''; + } else { + $GLOBALS['odt_buffer'] + .= '' + . '' + . htmlspecialchars($row[$j]) + . '' + . ''; + } + } // end for + $GLOBALS['odt_buffer'] .= ''; + } // end while + $GLOBALS['dbi']->freeResult($result); + + $GLOBALS['odt_buffer'] .= ''; + + return true; + } + + /** + * Returns a stand-in CREATE definition to resolve view dependencies + * + * @param string $db the database name + * @param string $view the view name + * @param string $crlf the end of line sequence + * @param array $aliases Aliases of db/table/columns + * + * @return string resulting definition + */ + public function getTableDefStandIn($db, $view, $crlf, $aliases = []) + { + $db_alias = $db; + $view_alias = $view; + $this->initAlias($aliases, $db_alias, $view_alias); + /** + * Gets fields properties + */ + $GLOBALS['dbi']->selectDb($db); + + /** + * Displays the table structure + */ + $GLOBALS['odt_buffer'] + .= ''; + $columns_cnt = 4; + $GLOBALS['odt_buffer'] + .= ''; + /* Header */ + $GLOBALS['odt_buffer'] .= '' + . '' + . '' . __('Column') . '' + . '' + . '' + . '' . __('Type') . '' + . '' + . '' + . '' . __('Null') . '' + . '' + . '' + . '' . __('Default') . '' + . '' + . ''; + + $columns = $GLOBALS['dbi']->getColumns($db, $view); + foreach ($columns as $column) { + $col_as = $column['Field'] ?? null; + if (! empty($aliases[$db]['tables'][$view]['columns'][$col_as])) { + $col_as = $aliases[$db]['tables'][$view]['columns'][$col_as]; + } + $GLOBALS['odt_buffer'] .= $this->formatOneColumnDefinition( + $column, + $col_as + ); + $GLOBALS['odt_buffer'] .= ''; + } // end foreach + + $GLOBALS['odt_buffer'] .= ''; + + return ''; + } + + /** + * Returns $table's CREATE definition + * + * @param string $db the database name + * @param string $table the table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param bool $do_relation whether to include relation comments + * @param bool $do_comments whether to include the pmadb-style column + * comments as comments in the structure; + * this is deprecated but the parameter is + * left here because export.php calls + * PMA_exportStructure() also for other + * @param bool $do_mime whether to include mime comments + * @param bool $show_dates whether to include creation/update/check dates + * @param bool $add_semicolon whether to add semicolon and end-of-line at + * the end + * @param bool $view whether we're handling a view + * @param array $aliases Aliases of db/table/columns + * + * @return bool true + */ + public function getTableDef( + $db, + $table, + $crlf, + $error_url, + $do_relation, + $do_comments, + $do_mime, + $show_dates = false, + $add_semicolon = true, + $view = false, + array $aliases = [] + ) { + global $cfgRelation; + + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + /** + * Gets fields properties + */ + $GLOBALS['dbi']->selectDb($db); + + // Check if we can use Relations + list($res_rel, $have_rel) = $this->relation->getRelationsAndStatus( + $do_relation && ! empty($cfgRelation['relation']), + $db, + $table + ); + /** + * Displays the table structure + */ + $GLOBALS['odt_buffer'] .= ''; + $columns_cnt = 4; + if ($do_relation && $have_rel) { + $columns_cnt++; + } + if ($do_comments) { + $columns_cnt++; + } + if ($do_mime && $cfgRelation['mimework']) { + $columns_cnt++; + } + $GLOBALS['odt_buffer'] .= ''; + /* Header */ + $GLOBALS['odt_buffer'] .= '' + . '' + . '' . __('Column') . '' + . '' + . '' + . '' . __('Type') . '' + . '' + . '' + . '' . __('Null') . '' + . '' + . '' + . '' . __('Default') . '' + . ''; + if ($do_relation && $have_rel) { + $GLOBALS['odt_buffer'] .= '' + . '' . __('Links to') . '' + . ''; + } + if ($do_comments) { + $GLOBALS['odt_buffer'] .= '' + . '' . __('Comments') . '' + . ''; + $comments = $this->relation->getComments($db, $table); + } + if ($do_mime && $cfgRelation['mimework']) { + $GLOBALS['odt_buffer'] .= '' + . '' . __('Media (MIME) type') . '' + . ''; + $mime_map = $this->transformations->getMime($db, $table, true); + } + $GLOBALS['odt_buffer'] .= ''; + + $columns = $GLOBALS['dbi']->getColumns($db, $table); + foreach ($columns as $column) { + $col_as = $field_name = $column['Field']; + if (! empty($aliases[$db]['tables'][$table]['columns'][$col_as])) { + $col_as = $aliases[$db]['tables'][$table]['columns'][$col_as]; + } + $GLOBALS['odt_buffer'] .= $this->formatOneColumnDefinition( + $column, + $col_as + ); + if ($do_relation && $have_rel) { + $foreigner = $this->relation->searchColumnInForeigners($res_rel, $field_name); + if ($foreigner) { + $rtable = $foreigner['foreign_table']; + $rfield = $foreigner['foreign_field']; + if (! empty($aliases[$db]['tables'][$rtable]['columns'][$rfield]) + ) { + $rfield + = $aliases[$db]['tables'][$rtable]['columns'][$rfield]; + } + if (! empty($aliases[$db]['tables'][$rtable]['alias'])) { + $rtable = $aliases[$db]['tables'][$rtable]['alias']; + } + $relation = htmlspecialchars($rtable . ' (' . $rfield . ')'); + $GLOBALS['odt_buffer'] + .= '' + . '' + . htmlspecialchars($relation) + . '' + . ''; + } + } + if ($do_comments) { + if (isset($comments[$field_name])) { + $GLOBALS['odt_buffer'] + .= '' + . '' + . htmlspecialchars($comments[$field_name]) + . '' + . ''; + } else { + $GLOBALS['odt_buffer'] + .= '' + . '' + . ''; + } + } + if ($do_mime && $cfgRelation['mimework']) { + if (isset($mime_map[$field_name])) { + $GLOBALS['odt_buffer'] + .= '' + . '' + . htmlspecialchars( + str_replace('_', '/', $mime_map[$field_name]['mimetype']) + ) + . '' + . ''; + } else { + $GLOBALS['odt_buffer'] + .= '' + . '' + . ''; + } + } + $GLOBALS['odt_buffer'] .= ''; + } // end foreach + + $GLOBALS['odt_buffer'] .= ''; + + return true; + } // end of the '$this->getTableDef()' function + + /** + * Outputs triggers + * + * @param string $db database name + * @param string $table table name + * @param array $aliases Aliases of db/table/columns + * + * @return bool true + */ + protected function getTriggers($db, $table, array $aliases = []) + { + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + $GLOBALS['odt_buffer'] .= '' + . '' + . '' + . '' + . '' . __('Name') . '' + . '' + . '' + . '' . __('Time') . '' + . '' + . '' + . '' . __('Event') . '' + . '' + . '' + . '' . __('Definition') . '' + . '' + . ''; + + $triggers = $GLOBALS['dbi']->getTriggers($db, $table); + + foreach ($triggers as $trigger) { + $GLOBALS['odt_buffer'] .= ''; + $GLOBALS['odt_buffer'] .= '' + . '' + . htmlspecialchars($trigger['name']) + . '' + . ''; + $GLOBALS['odt_buffer'] .= '' + . '' + . htmlspecialchars($trigger['action_timing']) + . '' + . ''; + $GLOBALS['odt_buffer'] .= '' + . '' + . htmlspecialchars($trigger['event_manipulation']) + . '' + . ''; + $GLOBALS['odt_buffer'] .= '' + . '' + . htmlspecialchars($trigger['definition']) + . '' + . ''; + $GLOBALS['odt_buffer'] .= ''; + } + + $GLOBALS['odt_buffer'] .= ''; + + return true; + } + + /** + * Outputs table's structure + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $export_mode 'create_table', 'triggers', 'create_view', + * 'stand_in' + * @param string $export_type 'server', 'database', 'table' + * @param bool $do_relation whether to include relation comments + * @param bool $do_comments whether to include the pmadb-style column + * comments as comments in the structure; + * this is deprecated but the parameter is + * left here because export.php calls + * PMA_exportStructure() also for other + * @param bool $do_mime whether to include mime comments + * @param bool $dates whether to include creation/update/check dates + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportStructure( + $db, + $table, + $crlf, + $error_url, + $export_mode, + $export_type, + $do_relation = false, + $do_comments = false, + $do_mime = false, + $dates = false, + array $aliases = [] + ) { + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + switch ($export_mode) { + case 'create_table': + $GLOBALS['odt_buffer'] + .= '' + . __('Table structure for table') . ' ' . + htmlspecialchars($table_alias) + . ''; + $this->getTableDef( + $db, + $table, + $crlf, + $error_url, + $do_relation, + $do_comments, + $do_mime, + $dates, + true, + false, + $aliases + ); + break; + case 'triggers': + $triggers = $GLOBALS['dbi']->getTriggers($db, $table, $aliases); + if ($triggers) { + $GLOBALS['odt_buffer'] + .= '' + . __('Triggers') . ' ' + . htmlspecialchars($table_alias) + . ''; + $this->getTriggers($db, $table); + } + break; + case 'create_view': + $GLOBALS['odt_buffer'] + .= '' + . __('Structure for view') . ' ' + . htmlspecialchars($table_alias) + . ''; + $this->getTableDef( + $db, + $table, + $crlf, + $error_url, + $do_relation, + $do_comments, + $do_mime, + $dates, + true, + true, + $aliases + ); + break; + case 'stand_in': + $GLOBALS['odt_buffer'] + .= '' + . __('Stand-in structure for view') . ' ' + . htmlspecialchars($table_alias) + . ''; + // export a stand-in definition to resolve view dependencies + $this->getTableDefStandIn($db, $table, $crlf, $aliases); + } // end switch + + return true; + } // end of the '$this->exportStructure' function + + /** + * Formats the definition for one column + * + * @param array $column info about this column + * @param string $col_as column alias + * + * @return string Formatted column definition + */ + protected function formatOneColumnDefinition($column, $col_as = '') + { + if (empty($col_as)) { + $col_as = $column['Field']; + } + $definition = ''; + $definition .= '' + . '' . htmlspecialchars($col_as) . '' + . ''; + + $extracted_columnspec + = Util::extractColumnSpec($column['Type']); + $type = htmlspecialchars($extracted_columnspec['print_type']); + if (empty($type)) { + $type = ' '; + } + + $definition .= '' + . '' . htmlspecialchars($type) . '' + . ''; + if (! isset($column['Default'])) { + if ($column['Null'] != 'NO') { + $column['Default'] = 'NULL'; + } else { + $column['Default'] = ''; + } + } + $definition .= '' + . '' + . (($column['Null'] == '' || $column['Null'] == 'NO') + ? __('No') + : __('Yes')) + . '' + . ''; + $definition .= '' + . '' . htmlspecialchars($column['Default']) . '' + . ''; + + return $definition; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportPdf.php b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportPdf.php new file mode 100644 index 0000000..49e74b3 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportPdf.php @@ -0,0 +1,395 @@ +initSpecificVariables(); + + $this->setProperties(); + } + + /** + * Initialize the local variables that are used for export PDF + * + * @return void + */ + protected function initSpecificVariables() + { + if (! empty($_POST['pdf_report_title'])) { + $this->_setPdfReportTitle($_POST['pdf_report_title']); + } + $this->_setPdf(new Pdf('L', 'pt', 'A3')); + } + + /** + * Sets the export PDF properties + * + * @return void + */ + protected function setProperties() + { + $exportPluginProperties = new ExportPluginProperties(); + $exportPluginProperties->setText('PDF'); + $exportPluginProperties->setExtension('pdf'); + $exportPluginProperties->setMimeType('application/pdf'); + $exportPluginProperties->setForceFile(true); + $exportPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $exportPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new OptionsPropertyMainGroup("general_opts"); + // create primary items and add them to the group + $leaf = new TextPropertyItem( + "report_title", + __('Report title:') + ); + $generalOptions->addProperty($leaf); + // add the group to the root group + $exportSpecificOptions->addProperty($generalOptions); + + // what to dump (structure/data/both) main group + $dumpWhat = new OptionsPropertyMainGroup( + "dump_what", + __('Dump table') + ); + $leaf = new RadioPropertyItem("structure_or_data"); + $leaf->setValues( + [ + 'structure' => __('structure'), + 'data' => __('data'), + 'structure_and_data' => __('structure and data'), + ] + ); + $dumpWhat->addProperty($leaf); + // add the group to the root group + $exportSpecificOptions->addProperty($dumpWhat); + + // set the options for the export plugin property item + $exportPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $exportPluginProperties; + } + + /** + * Outputs export header + * + * @return bool Whether it succeeded + */ + public function exportHeader() + { + $pdf_report_title = $this->_getPdfReportTitle(); + $pdf = $this->_getPdf(); + $pdf->Open(); + + $attr = [ + 'titleFontSize' => 18, + 'titleText' => $pdf_report_title, + ]; + $pdf->setAttributes($attr); + $pdf->setTopMargin(30); + + return true; + } + + /** + * Outputs export footer + * + * @return bool Whether it succeeded + */ + public function exportFooter() + { + $pdf = $this->_getPdf(); + + // instead of $pdf->Output(): + return $this->export->outputHandler($pdf->getPDFData()); + } + + /** + * Outputs database header + * + * @param string $db Database name + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBHeader($db, $db_alias = '') + { + return true; + } + + /** + * Outputs database footer + * + * @param string $db Database name + * + * @return bool Whether it succeeded + */ + public function exportDBFooter($db) + { + return true; + } + + /** + * Outputs CREATE DATABASE statement + * + * @param string $db Database name + * @param string $export_type 'server', 'database', 'table' + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBCreate($db, $export_type, $db_alias = '') + { + return true; + } + + /** + * Outputs the content of a table in NHibernate format + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $sql_query SQL query for obtaining data + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportData( + $db, + $table, + $crlf, + $error_url, + $sql_query, + array $aliases = [] + ) { + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + $pdf = $this->_getPdf(); + $attr = [ + 'currentDb' => $db, + 'currentTable' => $table, + 'dbAlias' => $db_alias, + 'tableAlias' => $table_alias, + 'aliases' => $aliases, + 'purpose' => __('Dumping data'), + ]; + $pdf->setAttributes($attr); + $pdf->mysqlReport($sql_query); + + return true; + } // end of the 'PMA_exportData()' function + + /** + * Outputs table structure + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $export_mode 'create_table', 'triggers', 'create_view', + * 'stand_in' + * @param string $export_type 'server', 'database', 'table' + * @param bool $do_relation whether to include relation comments + * @param bool $do_comments whether to include the pmadb-style column + * comments as comments in the structure; + * this is deprecated but the parameter is + * left here because export.php calls + * PMA_exportStructure() also for other + * export types which use this parameter + * @param bool $do_mime whether to include mime comments + * @param bool $dates whether to include creation/update/check dates + * @param array $aliases aliases for db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportStructure( + $db, + $table, + $crlf, + $error_url, + $export_mode, + $export_type, + $do_relation = false, + $do_comments = false, + $do_mime = false, + $dates = false, + array $aliases = [] + ) { + $db_alias = $db; + $table_alias = $table; + $purpose = null; + $this->initAlias($aliases, $db_alias, $table_alias); + $pdf = $this->_getPdf(); + // getting purpose to show at top + switch ($export_mode) { + case 'create_table': + $purpose = __('Table structure'); + break; + case 'triggers': + $purpose = __('Triggers'); + break; + case 'create_view': + $purpose = __('View structure'); + break; + case 'stand_in': + $purpose = __('Stand in'); + } // end switch + + $attr = [ + 'currentDb' => $db, + 'currentTable' => $table, + 'dbAlias' => $db_alias, + 'tableAlias' => $table_alias, + 'aliases' => $aliases, + 'purpose' => $purpose, + ]; + $pdf->setAttributes($attr); + /** + * comment display set true as presently in pdf + * format, no option is present to take user input. + */ + $do_comments = true; + switch ($export_mode) { + case 'create_table': + $pdf->getTableDef( + $db, + $table, + $do_relation, + $do_comments, + $do_mime, + false, + $aliases + ); + break; + case 'triggers': + $pdf->getTriggers($db, $table); + break; + case 'create_view': + $pdf->getTableDef( + $db, + $table, + $do_relation, + $do_comments, + $do_mime, + false, + $aliases + ); + break; + case 'stand_in': + /* export a stand-in definition to resolve view dependencies + * Yet to develop this function + * $pdf->getTableDefStandIn($db, $table, $crlf); + */ + } // end switch + + return true; + } + + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the PhpMyAdmin\Plugins\Export\Helpers\Pdf instance + * + * @return Pdf + */ + private function _getPdf() + { + return $this->_pdf; + } + + /** + * Instantiates the PhpMyAdmin\Plugins\Export\Helpers\Pdf class + * + * @param Pdf $pdf The instance + * + * @return void + */ + private function _setPdf($pdf) + { + $this->_pdf = $pdf; + } + + /** + * Gets the PDF report title + * + * @return string + */ + private function _getPdfReportTitle() + { + return $this->_pdfReportTitle; + } + + /** + * Sets the PDF report title + * + * @param string $pdfReportTitle PDF report title + * + * @return void + */ + private function _setPdfReportTitle($pdfReportTitle) + { + $this->_pdfReportTitle = $pdfReportTitle; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportPhparray.php b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportPhparray.php new file mode 100644 index 0000000..3fe9bd1 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportPhparray.php @@ -0,0 +1,259 @@ +setProperties(); + } + + /** + * Sets the export PHP Array properties + * + * @return void + */ + protected function setProperties() + { + $exportPluginProperties = new ExportPluginProperties(); + $exportPluginProperties->setText('PHP array'); + $exportPluginProperties->setExtension('php'); + $exportPluginProperties->setMimeType('text/plain'); + $exportPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $exportPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new OptionsPropertyMainGroup("general_opts"); + // create primary items and add them to the group + $leaf = new HiddenPropertyItem("structure_or_data"); + $generalOptions->addProperty($leaf); + // add the main group to the root group + $exportSpecificOptions->addProperty($generalOptions); + + // set the options for the export plugin property item + $exportPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $exportPluginProperties; + } + + /** + * Removes end of comment from a string + * + * @param string $string String to replace + * + * @return string + */ + public function commentString($string) + { + return strtr($string, '*/', '-'); + } + + + /** + * Outputs export header + * + * @return bool Whether it succeeded + */ + public function exportHeader() + { + $this->export->outputHandler( + 'export->outputHandler( + '/**' . $GLOBALS['crlf'] + . ' * Database ' . $this->commentString(Util::backquote($db_alias)) + . $GLOBALS['crlf'] . ' */' . $GLOBALS['crlf'] + ); + + return true; + } + + /** + * Outputs database footer + * + * @param string $db Database name + * + * @return bool Whether it succeeded + */ + public function exportDBFooter($db) + { + return true; + } + + /** + * Outputs CREATE DATABASE statement + * + * @param string $db Database name + * @param string $export_type 'server', 'database', 'table' + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBCreate($db, $export_type, $db_alias = '') + { + return true; + } + + /** + * Outputs the content of a table in PHP array format + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $sql_query SQL query for obtaining data + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportData( + $db, + $table, + $crlf, + $error_url, + $sql_query, + array $aliases = [] + ) { + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + + $result = $GLOBALS['dbi']->query( + $sql_query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_UNBUFFERED + ); + + $columns_cnt = $GLOBALS['dbi']->numFields($result); + $columns = []; + for ($i = 0; $i < $columns_cnt; $i++) { + $col_as = $GLOBALS['dbi']->fieldName($result, $i); + if (! empty($aliases[$db]['tables'][$table]['columns'][$col_as])) { + $col_as = $aliases[$db]['tables'][$table]['columns'][$col_as]; + } + $columns[$i] = stripslashes($col_as); + } + + // fix variable names (based on + // https://www.php.net/manual/en/language.variables.basics.php) + if (! preg_match( + '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', + $table_alias + ) + ) { + // fix invalid characters in variable names by replacing them with + // underscores + $tablefixed = preg_replace( + '/[^a-zA-Z0-9_\x7f-\xff]/', + '_', + $table_alias + ); + + // variable name must not start with a number or dash... + if (preg_match('/^[a-zA-Z_\x7f-\xff]/', $tablefixed) === 0) { + $tablefixed = '_' . $tablefixed; + } + } else { + $tablefixed = $table; + } + + $buffer = ''; + $record_cnt = 0; + // Output table name as comment + $buffer .= $crlf . '/* ' + . $this->commentString(Util::backquote($db_alias)) . '.' + . $this->commentString(Util::backquote($table_alias)) . ' */' . $crlf; + $buffer .= '$' . $tablefixed . ' = array('; + + while ($record = $GLOBALS['dbi']->fetchRow($result)) { + $record_cnt++; + + if ($record_cnt == 1) { + $buffer .= $crlf . ' array('; + } else { + $buffer .= ',' . $crlf . ' array('; + } + + for ($i = 0; $i < $columns_cnt; $i++) { + $buffer .= var_export($columns[$i], true) + . " => " . var_export($record[$i], true) + . (($i + 1 >= $columns_cnt) ? '' : ','); + } + + $buffer .= ')'; + } + + $buffer .= $crlf . ');' . $crlf; + if (! $this->export->outputHandler($buffer)) { + return false; + } + + $GLOBALS['dbi']->freeResult($result); + + return true; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportSql.php b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportSql.php new file mode 100644 index 0000000..9c5b1a2 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportSql.php @@ -0,0 +1,2915 @@ +setProperties(); + + // Avoids undefined variables, use NULL so isset() returns false + if (! isset($GLOBALS['sql_backquotes'])) { + $GLOBALS['sql_backquotes'] = null; + } + } + + /** + * Sets the export SQL properties + * + * @return void + */ + protected function setProperties() + { + global $plugin_param; + + $hide_sql = false; + $hide_structure = false; + if ($plugin_param['export_type'] == 'table' + && ! $plugin_param['single_table'] + ) { + $hide_structure = true; + $hide_sql = true; + } + + if (! $hide_sql) { + $exportPluginProperties = new ExportPluginProperties(); + $exportPluginProperties->setText('SQL'); + $exportPluginProperties->setExtension('sql'); + $exportPluginProperties->setMimeType('text/x-sql'); + $exportPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $exportPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new OptionsPropertyMainGroup("general_opts"); + + // comments + $subgroup = new OptionsPropertySubgroup("include_comments"); + $leaf = new BoolPropertyItem( + 'include_comments', + __( + 'Display comments (includes info such as export' + . ' timestamp, PHP version, and server version)' + ) + ); + $subgroup->setSubgroupHeader($leaf); + + $leaf = new TextPropertyItem( + 'header_comment', + __('Additional custom header comment (\n splits lines):') + ); + $subgroup->addProperty($leaf); + $leaf = new BoolPropertyItem( + 'dates', + __( + 'Include a timestamp of when databases were created, last' + . ' updated, and last checked' + ) + ); + $subgroup->addProperty($leaf); + if (! empty($GLOBALS['cfgRelation']['relation'])) { + $leaf = new BoolPropertyItem( + 'relation', + __('Display foreign key relationships') + ); + $subgroup->addProperty($leaf); + } + if (! empty($GLOBALS['cfgRelation']['mimework'])) { + $leaf = new BoolPropertyItem( + 'mime', + __('Display media (MIME) types') + ); + $subgroup->addProperty($leaf); + } + $generalOptions->addProperty($subgroup); + + // enclose in a transaction + $leaf = new BoolPropertyItem( + "use_transaction", + __('Enclose export in a transaction') + ); + $leaf->setDoc( + [ + 'programs', + 'mysqldump', + 'option_mysqldump_single-transaction', + ] + ); + $generalOptions->addProperty($leaf); + + // disable foreign key checks + $leaf = new BoolPropertyItem( + "disable_fk", + __('Disable foreign key checks') + ); + $leaf->setDoc( + [ + 'manual_MySQL_Database_Administration', + 'server-system-variables', + 'sysvar_foreign_key_checks', + ] + ); + $generalOptions->addProperty($leaf); + + // export views as tables + $leaf = new BoolPropertyItem( + "views_as_tables", + __('Export views as tables') + ); + $generalOptions->addProperty($leaf); + + // export metadata + $leaf = new BoolPropertyItem( + "metadata", + __('Export metadata') + ); + $generalOptions->addProperty($leaf); + + // compatibility maximization + $compats = $GLOBALS['dbi']->getCompatibilities(); + if (count($compats) > 0) { + $values = []; + foreach ($compats as $val) { + $values[$val] = $val; + } + + $leaf = new SelectPropertyItem( + "compatibility", + __( + 'Database system or older MySQL server to maximize output' + . ' compatibility with:' + ) + ); + $leaf->setValues($values); + $leaf->setDoc( + [ + 'manual_MySQL_Database_Administration', + 'Server_SQL_mode', + ] + ); + $generalOptions->addProperty($leaf); + + unset($values); + } + + // what to dump (structure/data/both) + $subgroup = new OptionsPropertySubgroup( + "dump_table", + __("Dump table") + ); + $leaf = new RadioPropertyItem('structure_or_data'); + $leaf->setValues( + [ + 'structure' => __('structure'), + 'data' => __('data'), + 'structure_and_data' => __('structure and data'), + ] + ); + $subgroup->setSubgroupHeader($leaf); + $generalOptions->addProperty($subgroup); + + // add the main group to the root group + $exportSpecificOptions->addProperty($generalOptions); + + // structure options main group + if (! $hide_structure) { + $structureOptions = new OptionsPropertyMainGroup( + "structure", + __('Object creation options') + ); + $structureOptions->setForce('data'); + + // begin SQL Statements + $subgroup = new OptionsPropertySubgroup(); + $leaf = new MessageOnlyPropertyItem( + 'add_statements', + __('Add statements:') + ); + $subgroup->setSubgroupHeader($leaf); + + // server export options + if ($plugin_param['export_type'] == 'server') { + $leaf = new BoolPropertyItem( + "drop_database", + sprintf(__('Add %s statement'), 'DROP DATABASE IF EXISTS') + ); + $subgroup->addProperty($leaf); + } + + if ($plugin_param['export_type'] == 'database') { + $create_clause = 'CREATE DATABASE / USE'; + $leaf = new BoolPropertyItem( + 'create_database', + sprintf(__('Add %s statement'), $create_clause) + ); + $subgroup->addProperty($leaf); + } + + if ($plugin_param['export_type'] == 'table') { + $drop_clause = $GLOBALS['dbi']->getTable( + $GLOBALS['db'], + $GLOBALS['table'] + )->isView() + ? 'DROP VIEW' + : 'DROP TABLE'; + } else { + $drop_clause = 'DROP TABLE / VIEW / PROCEDURE' + . ' / FUNCTION / EVENT'; + } + + $drop_clause .= ' / TRIGGER'; + + $leaf = new BoolPropertyItem( + 'drop_table', + sprintf(__('Add %s statement'), $drop_clause) + ); + $subgroup->addProperty($leaf); + + $subgroup_create_table = new OptionsPropertySubgroup(); + + // Add table structure option + $leaf = new BoolPropertyItem( + 'create_table', + sprintf(__('Add %s statement'), 'CREATE TABLE') + ); + $subgroup_create_table->setSubgroupHeader($leaf); + + $leaf = new BoolPropertyItem( + 'if_not_exists', + 'IF NOT EXISTS ' . __( + '(less efficient as indexes will be generated during table ' + . 'creation)' + ) + ); + $subgroup_create_table->addProperty($leaf); + + $leaf = new BoolPropertyItem( + 'auto_increment', + sprintf(__('%s value'), 'AUTO_INCREMENT') + ); + $subgroup_create_table->addProperty($leaf); + + $subgroup->addProperty($subgroup_create_table); + + // Add view option + $subgroup_create_view = new OptionsPropertySubgroup(); + $leaf = new BoolPropertyItem( + 'create_view', + sprintf(__('Add %s statement'), 'CREATE VIEW') + ); + $subgroup_create_view->setSubgroupHeader($leaf); + + $leaf = new BoolPropertyItem( + 'view_current_user', + __('Exclude definition of current user') + ); + $subgroup_create_view->addProperty($leaf); + + $leaf = new BoolPropertyItem( + 'or_replace_view', + sprintf(__('%s view'), 'OR REPLACE') + ); + $subgroup_create_view->addProperty($leaf); + + $subgroup->addProperty($subgroup_create_view); + + $leaf = new BoolPropertyItem( + 'procedure_function', + sprintf( + __('Add %s statement'), + 'CREATE PROCEDURE / FUNCTION / EVENT' + ) + ); + $subgroup->addProperty($leaf); + + // Add triggers option + $leaf = new BoolPropertyItem( + 'create_trigger', + sprintf(__('Add %s statement'), 'CREATE TRIGGER') + ); + $subgroup->addProperty($leaf); + + $structureOptions->addProperty($subgroup); + + $leaf = new BoolPropertyItem( + "backquotes", + __( + 'Enclose table and column names with backquotes ' + . '(Protects column and table names formed with' + . ' special characters or keywords)' + ) + ); + + $structureOptions->addProperty($leaf); + + // add the main group to the root group + $exportSpecificOptions->addProperty($structureOptions); + } + + // begin Data options + $dataOptions = new OptionsPropertyMainGroup( + "data", + __('Data creation options') + ); + $dataOptions->setForce('structure'); + $leaf = new BoolPropertyItem( + "truncate", + __('Truncate table before insert') + ); + $dataOptions->addProperty($leaf); + + // begin SQL Statements + $subgroup = new OptionsPropertySubgroup(); + $leaf = new MessageOnlyPropertyItem( + __('Instead of INSERT statements, use:') + ); + $subgroup->setSubgroupHeader($leaf); + + $leaf = new BoolPropertyItem( + "delayed", + __('INSERT DELAYED statements') + ); + $leaf->setDoc( + [ + 'manual_MySQL_Database_Administration', + 'insert_delayed', + ] + ); + $subgroup->addProperty($leaf); + + $leaf = new BoolPropertyItem( + "ignore", + __('INSERT IGNORE statements') + ); + $leaf->setDoc( + [ + 'manual_MySQL_Database_Administration', + 'insert', + ] + ); + $subgroup->addProperty($leaf); + $dataOptions->addProperty($subgroup); + + // Function to use when dumping dat + $leaf = new SelectPropertyItem( + "type", + __('Function to use when dumping data:') + ); + $leaf->setValues( + [ + 'INSERT' => 'INSERT', + 'UPDATE' => 'UPDATE', + 'REPLACE' => 'REPLACE', + ] + ); + $dataOptions->addProperty($leaf); + + /* Syntax to use when inserting data */ + $subgroup = new OptionsPropertySubgroup(); + $leaf = new MessageOnlyPropertyItem( + null, + __('Syntax to use when inserting data:') + ); + $subgroup->setSubgroupHeader($leaf); + $leaf = new RadioPropertyItem( + "insert_syntax", + __('INSERT IGNORE statements') + ); + $leaf->setValues( + [ + 'complete' => __( + 'include column names in every INSERT statement' + . '
          Example: INSERT INTO' + . ' tbl_name (col_A,col_B,col_C) VALUES (1,2,3)' + ), + 'extended' => __( + 'insert multiple rows in every INSERT statement' + . '
          Example: INSERT INTO' + . ' tbl_name VALUES (1,2,3), (4,5,6), (7,8,9)' + ), + 'both' => __( + 'both of the above
          Example:' + . ' INSERT INTO tbl_name (col_A,col_B,col_C) VALUES' + . ' (1,2,3), (4,5,6), (7,8,9)' + ), + 'none' => __( + 'neither of the above
          Example:' + . ' INSERT INTO tbl_name VALUES (1,2,3)' + ), + ] + ); + $subgroup->addProperty($leaf); + $dataOptions->addProperty($subgroup); + + // Max length of query + $leaf = new NumberPropertyItem( + "max_query_size", + __('Maximal length of created query') + ); + $dataOptions->addProperty($leaf); + + // Dump binary columns in hexadecimal + $leaf = new BoolPropertyItem( + "hex_for_binary", + __( + 'Dump binary columns in hexadecimal notation' + . ' (for example, "abc" becomes 0x616263)' + ) + ); + $dataOptions->addProperty($leaf); + + // Dump time in UTC + $leaf = new BoolPropertyItem( + "utc_time", + __( + 'Dump TIMESTAMP columns in UTC (enables TIMESTAMP columns' + . ' to be dumped and reloaded between servers in different' + . ' time zones)' + ) + ); + $dataOptions->addProperty($leaf); + + // add the main group to the root group + $exportSpecificOptions->addProperty($dataOptions); + + // set the options for the export plugin property item + $exportPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $exportPluginProperties; + } + } + + /** + * Generates SQL for routines export + * + * @param string $db Database + * @param array $aliases Aliases of db/table/columns + * @param string $type Type of exported routine + * @param string $name Verbose name of exported routine + * @param array $routines List of routines to export + * @param string $delimiter Delimiter to use in SQL + * + * @return string SQL query + */ + protected function _exportRoutineSQL( + $db, + array $aliases, + $type, + $name, + array $routines, + $delimiter + ) { + global $crlf; + + $text = $this->_exportComment() + . $this->_exportComment($name) + . $this->_exportComment(); + + $used_alias = false; + $proc_query = ''; + + foreach ($routines as $routine) { + if (! empty($GLOBALS['sql_drop_table'])) { + $proc_query .= 'DROP ' . $type . ' IF EXISTS ' + . Util::backquote($routine) + . $delimiter . $crlf; + } + $create_query = $this->replaceWithAliases( + $GLOBALS['dbi']->getDefinition($db, $type, $routine), + $aliases, + $db, + '', + $flag + ); + // One warning per database + if ($flag) { + $used_alias = true; + } + $proc_query .= $create_query . $delimiter . $crlf . $crlf; + } + if ($used_alias) { + $text .= $this->_exportComment( + __('It appears your database uses routines;') + ) + . $this->_exportComment( + __('alias export may not work reliably in all cases.') + ) + . $this->_exportComment(); + } + $text .= $proc_query; + + return $text; + } + + /** + * Exports routines (procedures and functions) + * + * @param string $db Database + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportRoutines($db, array $aliases = []) + { + global $crlf; + + $db_alias = $db; + $this->initAlias($aliases, $db_alias); + + $text = ''; + $delimiter = '$$'; + + $procedure_names = $GLOBALS['dbi'] + ->getProceduresOrFunctions($db, 'PROCEDURE'); + $function_names = $GLOBALS['dbi']->getProceduresOrFunctions($db, 'FUNCTION'); + + if ($procedure_names || $function_names) { + $text .= $crlf + . 'DELIMITER ' . $delimiter . $crlf; + + if ($procedure_names) { + $text .= $this->_exportRoutineSQL( + $db, + $aliases, + 'PROCEDURE', + __('Procedures'), + $procedure_names, + $delimiter + ); + } + + if ($function_names) { + $text .= $this->_exportRoutineSQL( + $db, + $aliases, + 'FUNCTION', + __('Functions'), + $function_names, + $delimiter + ); + } + + $text .= 'DELIMITER ;' . $crlf; + } + + if (! empty($text)) { + return $this->export->outputHandler($text); + } + + return false; + } + + /** + * Possibly outputs comment + * + * @param string $text Text of comment + * + * @return string The formatted comment + */ + private function _exportComment($text = '') + { + if (isset($GLOBALS['sql_include_comments']) + && $GLOBALS['sql_include_comments'] + ) { + // see https://dev.mysql.com/doc/refman/5.0/en/ansi-diff-comments.html + if (empty($text)) { + return '--' . $GLOBALS['crlf']; + } + + $lines = preg_split("/\\r\\n|\\r|\\n/", $text); + $result = []; + foreach ($lines as $line) { + $result[] = '-- ' . $line . $GLOBALS['crlf']; + } + return implode('', $result); + } + + return ''; + } + + /** + * Possibly outputs CRLF + * + * @return string crlf or nothing + */ + private function _possibleCRLF() + { + if (isset($GLOBALS['sql_include_comments']) + && $GLOBALS['sql_include_comments'] + ) { + return $GLOBALS['crlf']; + } + + return ''; + } + + /** + * Outputs export footer + * + * @return bool Whether it succeeded + */ + public function exportFooter() + { + global $crlf; + + $foot = ''; + + if (isset($GLOBALS['sql_disable_fk'])) { + $foot .= 'SET FOREIGN_KEY_CHECKS=1;' . $crlf; + } + + if (isset($GLOBALS['sql_use_transaction'])) { + $foot .= 'COMMIT;' . $crlf; + } + + // restore connection settings + if ($this->_sent_charset) { + $foot .= $crlf + . '/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;' + . $crlf + . '/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;' + . $crlf + . '/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;' + . $crlf; + $this->_sent_charset = false; + } + + /* Restore timezone */ + if (isset($GLOBALS['sql_utc_time']) && $GLOBALS['sql_utc_time']) { + $GLOBALS['dbi']->query('SET time_zone = "' . $GLOBALS['old_tz'] . '"'); + } + + return $this->export->outputHandler($foot); + } + + /** + * Outputs export header. It is the first method to be called, so all + * the required variables are initialized here. + * + * @return bool Whether it succeeded + */ + public function exportHeader() + { + global $crlf, $cfg; + + if (isset($GLOBALS['sql_compatibility'])) { + $tmp_compat = $GLOBALS['sql_compatibility']; + if ($tmp_compat == 'NONE') { + $tmp_compat = ''; + } + $GLOBALS['dbi']->tryQuery('SET SQL_MODE="' . $tmp_compat . '"'); + unset($tmp_compat); + } + $head = $this->_exportComment('phpMyAdmin SQL Dump') + . $this->_exportComment('version ' . PMA_VERSION) + . $this->_exportComment('https://www.phpmyadmin.net/') + . $this->_exportComment(); + $host_string = __('Host:') . ' ' . $cfg['Server']['host']; + if (! empty($cfg['Server']['port'])) { + $host_string .= ':' . $cfg['Server']['port']; + } + $head .= $this->_exportComment($host_string); + $head .= $this->_exportComment( + __('Generation Time:') . ' ' + . Util::localisedDate() + ) + . $this->_exportComment( + __('Server version:') . ' ' . $GLOBALS['dbi']->getVersionString() + ) + . $this->_exportComment(__('PHP Version:') . ' ' . PHP_VERSION) + . $this->_possibleCRLF(); + + if (isset($GLOBALS['sql_header_comment']) + && ! empty($GLOBALS['sql_header_comment']) + ) { + // '\n' is not a newline (like "\n" would be), it's the characters + // backslash and n, as explained on the export interface + $lines = explode('\n', $GLOBALS['sql_header_comment']); + $head .= $this->_exportComment(); + foreach ($lines as $one_line) { + $head .= $this->_exportComment($one_line); + } + $head .= $this->_exportComment(); + } + + if (isset($GLOBALS['sql_disable_fk'])) { + $head .= 'SET FOREIGN_KEY_CHECKS=0;' . $crlf; + } + + // We want exported AUTO_INCREMENT columns to have still same value, + // do this only for recent MySQL exports + if (! isset($GLOBALS['sql_compatibility']) + || $GLOBALS['sql_compatibility'] == 'NONE' + ) { + $head .= 'SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";' . $crlf; + } + + if (isset($GLOBALS['sql_use_transaction'])) { + $head .= 'SET AUTOCOMMIT = 0;' . $crlf + . 'START TRANSACTION;' . $crlf; + } + + /* Change timezone if we should export timestamps in UTC */ + if (isset($GLOBALS['sql_utc_time']) && $GLOBALS['sql_utc_time']) { + $head .= 'SET time_zone = "+00:00";' . $crlf; + $GLOBALS['old_tz'] = $GLOBALS['dbi'] + ->fetchValue('SELECT @@session.time_zone'); + $GLOBALS['dbi']->query('SET time_zone = "+00:00"'); + } + + $head .= $this->_possibleCRLF(); + + if (! empty($GLOBALS['asfile'])) { + // we are saving as file, therefore we provide charset information + // so that a utility like the mysql client can interpret + // the file correctly + if (isset($GLOBALS['charset']) + && isset(Charsets::$mysqlCharsetMap[$GLOBALS['charset']]) + ) { + // we got a charset from the export dialog + $set_names = Charsets::$mysqlCharsetMap[$GLOBALS['charset']]; + } else { + // by default we use the connection charset + $set_names = Charsets::$mysqlCharsetMap['utf-8']; + } + if ($set_names == 'utf8' && $GLOBALS['dbi']->getVersion() > 50503) { + $set_names = 'utf8mb4'; + } + $head .= $crlf + . '/*!40101 SET @OLD_CHARACTER_SET_CLIENT=' + . '@@CHARACTER_SET_CLIENT */;' . $crlf + . '/*!40101 SET @OLD_CHARACTER_SET_RESULTS=' + . '@@CHARACTER_SET_RESULTS */;' . $crlf + . '/*!40101 SET @OLD_COLLATION_CONNECTION=' + . '@@COLLATION_CONNECTION */;' . $crlf + . '/*!40101 SET NAMES ' . $set_names . ' */;' . $crlf . $crlf; + $this->_sent_charset = true; + } + + return $this->export->outputHandler($head); + } + + /** + * Outputs CREATE DATABASE statement + * + * @param string $db Database name + * @param string $export_type 'server', 'database', 'table' + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBCreate($db, $export_type, $db_alias = '') + { + global $crlf; + + if (empty($db_alias)) { + $db_alias = $db; + } + if (isset($GLOBALS['sql_compatibility'])) { + $compat = $GLOBALS['sql_compatibility']; + } else { + $compat = 'NONE'; + } + if (isset($GLOBALS['sql_drop_database'])) { + if (! $this->export->outputHandler( + 'DROP DATABASE IF EXISTS ' + . Util::backquoteCompat( + $db_alias, + $compat, + isset($GLOBALS['sql_backquotes']) + ) + . ';' . $crlf + ) + ) { + return false; + } + } + if ($export_type == 'database' && ! isset($GLOBALS['sql_create_database'])) { + return true; + } + + $create_query = 'CREATE DATABASE IF NOT EXISTS ' + . Util::backquoteCompat( + $db_alias, + $compat, + isset($GLOBALS['sql_backquotes']) + ); + $collation = $GLOBALS['dbi']->getDbCollation($db); + if (mb_strpos($collation, '_')) { + $create_query .= ' DEFAULT CHARACTER SET ' + . mb_substr( + $collation, + 0, + mb_strpos($collation, '_') + ) + . ' COLLATE ' . $collation; + } else { + $create_query .= ' DEFAULT CHARACTER SET ' . $collation; + } + $create_query .= ';' . $crlf; + if (! $this->export->outputHandler($create_query)) { + return false; + } + + return $this->_exportUseStatement($db_alias, $compat); + } + + /** + * Outputs USE statement + * + * @param string $db db to use + * @param string $compat sql compatibility + * + * @return bool Whether it succeeded + */ + private function _exportUseStatement($db, $compat) + { + global $crlf; + + if (isset($GLOBALS['sql_compatibility']) + && $GLOBALS['sql_compatibility'] == 'NONE' + ) { + $result = $this->export->outputHandler( + 'USE ' + . Util::backquoteCompat( + $db, + $compat, + isset($GLOBALS['sql_backquotes']) + ) + . ';' . $crlf + ); + } else { + $result = $this->export->outputHandler('USE ' . $db . ';' . $crlf); + } + + return $result; + } + + /** + * Outputs database header + * + * @param string $db Database name + * @param string $db_alias Alias of db + * + * @return bool Whether it succeeded + */ + public function exportDBHeader($db, $db_alias = '') + { + if (empty($db_alias)) { + $db_alias = $db; + } + if (isset($GLOBALS['sql_compatibility'])) { + $compat = $GLOBALS['sql_compatibility']; + } else { + $compat = 'NONE'; + } + $head = $this->_exportComment() + . $this->_exportComment( + __('Database:') . ' ' + . Util::backquoteCompat( + $db_alias, + $compat, + isset($GLOBALS['sql_backquotes']) + ) + ) + . $this->_exportComment(); + + return $this->export->outputHandler($head); + } + + /** + * Outputs database footer + * + * @param string $db Database name + * + * @return bool Whether it succeeded + */ + public function exportDBFooter($db) + { + global $crlf; + + $result = true; + + //add indexes to the sql dump file + if (isset($GLOBALS['sql_indexes'])) { + $result = $this->export->outputHandler($GLOBALS['sql_indexes']); + unset($GLOBALS['sql_indexes']); + } + //add auto increments to the sql dump file + if (isset($GLOBALS['sql_auto_increments'])) { + $result = $this->export->outputHandler($GLOBALS['sql_auto_increments']); + unset($GLOBALS['sql_auto_increments']); + } + //add constraints to the sql dump file + if (isset($GLOBALS['sql_constraints'])) { + $result = $this->export->outputHandler($GLOBALS['sql_constraints']); + unset($GLOBALS['sql_constraints']); + } + + return $result; + } + + /** + * Exports events + * + * @param string $db Database + * + * @return bool Whether it succeeded + */ + public function exportEvents($db) + { + global $crlf; + + $text = ''; + $delimiter = '$$'; + + $event_names = $GLOBALS['dbi']->fetchResult( + "SELECT EVENT_NAME FROM information_schema.EVENTS WHERE" + . " EVENT_SCHEMA= '" . $GLOBALS['dbi']->escapeString($db) + . "';" + ); + + if ($event_names) { + $text .= $crlf + . "DELIMITER " . $delimiter . $crlf; + + $text .= $this->_exportComment() + . $this->_exportComment(__('Events')) + . $this->_exportComment(); + + foreach ($event_names as $event_name) { + if (! empty($GLOBALS['sql_drop_table'])) { + $text .= "DROP EVENT " + . Util::backquote($event_name) + . $delimiter . $crlf; + } + $text .= $GLOBALS['dbi']->getDefinition($db, 'EVENT', $event_name) + . $delimiter . $crlf . $crlf; + } + + $text .= "DELIMITER ;" . $crlf; + } + + if (! empty($text)) { + return $this->export->outputHandler($text); + } + + return false; + } + + /** + * Exports metadata from Configuration Storage + * + * @param string $db database being exported + * @param string|array $tables table(s) being exported + * @param array $metadataTypes types of metadata to export + * + * @return bool Whether it succeeded + */ + public function exportMetadata( + $db, + $tables, + array $metadataTypes + ) { + $cfgRelation = $this->relation->getRelationsParam(); + if (! isset($cfgRelation['db'])) { + return true; + } + + $comment = $this->_possibleCRLF() + . $this->_possibleCRLF() + . $this->_exportComment() + . $this->_exportComment(__('Metadata')) + . $this->_exportComment(); + if (! $this->export->outputHandler($comment)) { + return false; + } + + if (! $this->_exportUseStatement( + $cfgRelation['db'], + $GLOBALS['sql_compatibility'] + ) + ) { + return false; + } + + $r = true; + if (is_array($tables)) { + // export metadata for each table + foreach ($tables as $table) { + $r &= $this->_exportMetadata($db, $table, $metadataTypes); + } + // export metadata for the database + $r &= $this->_exportMetadata($db, null, $metadataTypes); + } else { + // export metadata for single table + $r &= $this->_exportMetadata($db, $tables, $metadataTypes); + } + + return $r; + } + + /** + * Exports metadata from Configuration Storage + * + * @param string $db database being exported + * @param string $table table being exported + * @param array $metadataTypes types of metadata to export + * + * @return bool Whether it succeeded + */ + private function _exportMetadata( + $db, + $table, + array $metadataTypes + ) { + $cfgRelation = $this->relation->getRelationsParam(); + + if (isset($table)) { + $types = [ + 'column_info' => 'db_name', + 'table_uiprefs' => 'db_name', + 'tracking' => 'db_name', + ]; + } else { + $types = [ + 'bookmark' => 'dbase', + 'relation' => 'master_db', + 'pdf_pages' => 'db_name', + 'savedsearches' => 'db_name', + 'central_columns' => 'db_name', + ]; + } + + $aliases = []; + + $comment = $this->_possibleCRLF() + . $this->_exportComment(); + + if (isset($table)) { + $comment .= $this->_exportComment( + sprintf( + __('Metadata for table %s'), + $table + ) + ); + } else { + $comment .= $this->_exportComment( + sprintf( + __('Metadata for database %s'), + $db + ) + ); + } + + $comment .= $this->_exportComment(); + + if (! $this->export->outputHandler($comment)) { + return false; + } + + foreach ($types as $type => $dbNameColumn) { + if (in_array($type, $metadataTypes) && isset($cfgRelation[$type])) { + // special case, designer pages and their coordinates + if ($type == 'pdf_pages') { + $sql_query = "SELECT `page_nr`, `page_descr` FROM " + . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation[$type]) + . " WHERE " . Util::backquote($dbNameColumn) + . " = '" . $GLOBALS['dbi']->escapeString($db) . "'"; + + $result = $GLOBALS['dbi']->fetchResult( + $sql_query, + 'page_nr', + 'page_descr' + ); + + foreach ($result as $page => $name) { + // insert row for pdf_page + $sql_query_row = "SELECT `db_name`, `page_descr` FROM " + . Util::backquote($cfgRelation['db']) + . "." . Util::backquote( + $cfgRelation[$type] + ) + . " WHERE " . Util::backquote( + $dbNameColumn + ) + . " = '" . $GLOBALS['dbi']->escapeString($db) . "'" + . " AND `page_nr` = '" . intval($page) . "'"; + + if (! $this->exportData( + $cfgRelation['db'], + $cfgRelation[$type], + $GLOBALS['crlf'], + '', + $sql_query_row, + $aliases + ) + ) { + return false; + } + + $lastPage = $GLOBALS['crlf'] + . "SET @LAST_PAGE = LAST_INSERT_ID();" + . $GLOBALS['crlf']; + if (! $this->export->outputHandler($lastPage)) { + return false; + } + + $sql_query_coords = "SELECT `db_name`, `table_name`, " + . "'@LAST_PAGE' AS `pdf_page_number`, `x`, `y` FROM " + . Util::backquote($cfgRelation['db']) + . "." . Util::backquote( + $cfgRelation['table_coords'] + ) + . " WHERE `pdf_page_number` = '" . $page . "'"; + + $GLOBALS['exporting_metadata'] = true; + if (! $this->exportData( + $cfgRelation['db'], + $cfgRelation['table_coords'], + $GLOBALS['crlf'], + '', + $sql_query_coords, + $aliases + ) + ) { + $GLOBALS['exporting_metadata'] = false; + + return false; + } + $GLOBALS['exporting_metadata'] = false; + } + continue; + } + + // remove auto_incrementing id field for some tables + if ($type == 'bookmark') { + $sql_query = "SELECT `dbase`, `user`, `label`, `query` FROM "; + } elseif ($type == 'column_info') { + $sql_query = "SELECT `db_name`, `table_name`, `column_name`," + . " `comment`, `mimetype`, `transformation`," + . " `transformation_options`, `input_transformation`," + . " `input_transformation_options` FROM"; + } elseif ($type == 'savedsearches') { + $sql_query = "SELECT `username`, `db_name`, `search_name`," + . " `search_data` FROM"; + } else { + $sql_query = "SELECT * FROM "; + } + $sql_query .= Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation[$type]) + . " WHERE " . Util::backquote($dbNameColumn) + . " = '" . $GLOBALS['dbi']->escapeString($db) . "'"; + if (isset($table)) { + $sql_query .= " AND `table_name` = '" + . $GLOBALS['dbi']->escapeString($table) . "'"; + } + + if (! $this->exportData( + $cfgRelation['db'], + $cfgRelation[$type], + $GLOBALS['crlf'], + '', + $sql_query, + $aliases + ) + ) { + return false; + } + } + } + + return true; + } + + /** + * Returns a stand-in CREATE definition to resolve view dependencies + * + * @param string $db the database name + * @param string $view the view name + * @param string $crlf the end of line sequence + * @param array $aliases Aliases of db/table/columns + * + * @return string resulting definition + */ + public function getTableDefStandIn($db, $view, $crlf, $aliases = []) + { + $db_alias = $db; + $view_alias = $view; + $this->initAlias($aliases, $db_alias, $view_alias); + $create_query = ''; + if (! empty($GLOBALS['sql_drop_table'])) { + $create_query .= 'DROP VIEW IF EXISTS ' + . Util::backquote($view_alias) + . ';' . $crlf; + } + + $create_query .= 'CREATE TABLE '; + + if (isset($GLOBALS['sql_if_not_exists']) + && $GLOBALS['sql_if_not_exists'] + ) { + $create_query .= 'IF NOT EXISTS '; + } + $create_query .= Util::backquote($view_alias) . ' (' . $crlf; + $tmp = []; + $columns = $GLOBALS['dbi']->getColumnsFull($db, $view); + foreach ($columns as $column_name => $definition) { + $col_alias = $column_name; + if (! empty($aliases[$db]['tables'][$view]['columns'][$col_alias])) { + $col_alias = $aliases[$db]['tables'][$view]['columns'][$col_alias]; + } + $tmp[] = Util::backquote($col_alias) . ' ' . + $definition['Type'] . $crlf; + } + $create_query .= implode(',', $tmp) . ');' . $crlf; + + return $create_query; + } + + /** + * Returns CREATE definition that matches $view's structure + * + * @param string $db the database name + * @param string $view the view name + * @param string $crlf the end of line sequence + * @param bool $add_semicolon whether to add semicolon and end-of-line at + * the end + * @param array $aliases Aliases of db/table/columns + * + * @return string resulting schema + */ + private function _getTableDefForView( + $db, + $view, + $crlf, + $add_semicolon = true, + array $aliases = [] + ) { + $db_alias = $db; + $view_alias = $view; + $this->initAlias($aliases, $db_alias, $view_alias); + $create_query = "CREATE TABLE"; + if (isset($GLOBALS['sql_if_not_exists'])) { + $create_query .= " IF NOT EXISTS "; + } + $create_query .= Util::backquote($view_alias) . "(" . $crlf; + + $columns = $GLOBALS['dbi']->getColumns($db, $view, null, true); + + $firstCol = true; + foreach ($columns as $column) { + $col_alias = $column['Field']; + if (! empty($aliases[$db]['tables'][$view]['columns'][$col_alias])) { + $col_alias = $aliases[$db]['tables'][$view]['columns'][$col_alias]; + } + $extracted_columnspec = Util::extractColumnSpec( + $column['Type'] + ); + + if (! $firstCol) { + $create_query .= "," . $crlf; + } + $create_query .= " " . Util::backquote($col_alias); + $create_query .= " " . $column['Type']; + if ($extracted_columnspec['can_contain_collation'] + && ! empty($column['Collation']) + ) { + $create_query .= " COLLATE " . $column['Collation']; + } + if ($column['Null'] == 'NO') { + $create_query .= " NOT NULL"; + } + if (isset($column['Default'])) { + $create_query .= " DEFAULT '" + . $GLOBALS['dbi']->escapeString($column['Default']) . "'"; + } else { + if ($column['Null'] == 'YES') { + $create_query .= " DEFAULT NULL"; + } + } + if (! empty($column['Comment'])) { + $create_query .= " COMMENT '" + . $GLOBALS['dbi']->escapeString($column['Comment']) . "'"; + } + $firstCol = false; + } + $create_query .= $crlf . ")" . ($add_semicolon ? ';' : '') . $crlf; + + if (isset($GLOBALS['sql_compatibility'])) { + $compat = $GLOBALS['sql_compatibility']; + } else { + $compat = 'NONE'; + } + if ($compat == 'MSSQL') { + $create_query = $this->_makeCreateTableMSSQLCompatible( + $create_query + ); + } + + return $create_query; + } + + /** + * Returns $table's CREATE definition + * + * @param string $db the database name + * @param string $table the table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case + * of error + * @param bool $show_dates whether to include creation/ + * update/check dates + * @param bool $add_semicolon whether to add semicolon and + * end-of-line at the end + * @param bool $view whether we're handling a view + * @param bool $update_indexes_increments whether we need to update + * two global variables + * @param array $aliases Aliases of db/table/columns + * + * @return string resulting schema + */ + public function getTableDef( + $db, + $table, + $crlf, + $error_url, + $show_dates = false, + $add_semicolon = true, + $view = false, + $update_indexes_increments = true, + array $aliases = [] + ) { + global $sql_drop_table, $sql_backquotes, $sql_constraints, + $sql_constraints_query, $sql_indexes, $sql_indexes_query, + $sql_auto_increments, $sql_drop_foreign_keys; + + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + + $schema_create = ''; + $auto_increment = ''; + $new_crlf = $crlf; + + if (isset($GLOBALS['sql_compatibility'])) { + $compat = $GLOBALS['sql_compatibility']; + } else { + $compat = 'NONE'; + } + + // need to use PhpMyAdmin\DatabaseInterface::QUERY_STORE + // with $GLOBALS['dbi']->numRows() in mysqli + $result = $GLOBALS['dbi']->tryQuery( + 'SHOW TABLE STATUS FROM ' . Util::backquote($db) + . ' WHERE Name = \'' . $GLOBALS['dbi']->escapeString((string) $table) . '\'', + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + if ($result != false) { + if ($GLOBALS['dbi']->numRows($result) > 0) { + $tmpres = $GLOBALS['dbi']->fetchAssoc($result); + + // Here we optionally add the AUTO_INCREMENT next value, + // but starting with MySQL 5.0.24, the clause is already included + // in SHOW CREATE TABLE so we'll remove it below + if (isset($GLOBALS['sql_auto_increment']) + && ! empty($tmpres['Auto_increment']) + ) { + $auto_increment .= ' AUTO_INCREMENT=' + . $tmpres['Auto_increment'] . ' '; + } + + if ($show_dates + && isset($tmpres['Create_time']) + && ! empty($tmpres['Create_time']) + ) { + $schema_create .= $this->_exportComment( + __('Creation:') . ' ' + . Util::localisedDate( + strtotime($tmpres['Create_time']) + ) + ); + $new_crlf = $this->_exportComment() . $crlf; + } + + if ($show_dates + && isset($tmpres['Update_time']) + && ! empty($tmpres['Update_time']) + ) { + $schema_create .= $this->_exportComment( + __('Last update:') . ' ' + . Util::localisedDate( + strtotime($tmpres['Update_time']) + ) + ); + $new_crlf = $this->_exportComment() . $crlf; + } + + if ($show_dates + && isset($tmpres['Check_time']) + && ! empty($tmpres['Check_time']) + ) { + $schema_create .= $this->_exportComment( + __('Last check:') . ' ' + . Util::localisedDate( + strtotime($tmpres['Check_time']) + ) + ); + $new_crlf = $this->_exportComment() . $crlf; + } + } + $GLOBALS['dbi']->freeResult($result); + } + + $schema_create .= $new_crlf; + + if (! empty($sql_drop_table) + && $GLOBALS['dbi']->getTable($db, $table)->isView() + ) { + $schema_create .= 'DROP VIEW IF EXISTS ' + . Util::backquote($table_alias, $sql_backquotes) . ';' + . $crlf; + } + + // no need to generate a DROP VIEW here, it was done earlier + if (! empty($sql_drop_table) + && ! $GLOBALS['dbi']->getTable($db, $table)->isView() + ) { + $schema_create .= 'DROP TABLE IF EXISTS ' + . Util::backquote($table_alias, $sql_backquotes) . ';' + . $crlf; + } + + // Complete table dump, + // Whether to quote table and column names or not + if ($sql_backquotes) { + $GLOBALS['dbi']->query('SET SQL_QUOTE_SHOW_CREATE = 1'); + } else { + $GLOBALS['dbi']->query('SET SQL_QUOTE_SHOW_CREATE = 0'); + } + + // I don't see the reason why this unbuffered query could cause problems, + // because SHOW CREATE TABLE returns only one row, and we free the + // results below. Nonetheless, we got 2 user reports about this + // (see bug 1562533) so I removed the unbuffered mode. + // $result = $GLOBALS['dbi']->query('SHOW CREATE TABLE ' . backquote($db) + // . '.' . backquote($table), null, DatabaseInterface::QUERY_UNBUFFERED); + // + // Note: SHOW CREATE TABLE, at least in MySQL 5.1.23, does not + // produce a displayable result for the default value of a BIT + // column, nor does the mysqldump command. See MySQL bug 35796 + $GLOBALS['dbi']->tryQuery('USE ' . Util::backquote($db)); + $result = $GLOBALS['dbi']->tryQuery( + 'SHOW CREATE TABLE ' . Util::backquote($db) . '.' + . Util::backquote($table) + ); + // an error can happen, for example the table is crashed + $tmp_error = $GLOBALS['dbi']->getError(); + if ($tmp_error) { + $message = sprintf(__('Error reading structure for table %s:'), "$db.$table"); + $message .= ' ' . $tmp_error; + if (! defined('TESTSUITE')) { + trigger_error($message, E_USER_ERROR); + } + return $this->_exportComment($message); + } + + // Old mode is stored so it can be restored once exporting is done. + $old_mode = Context::$MODE; + + $warning = ''; + if ($result != false && ($row = $GLOBALS['dbi']->fetchRow($result))) { + $create_query = $row[1]; + unset($row); + + // Convert end of line chars to one that we want (note that MySQL + // doesn't return query it will accept in all cases) + if (mb_strpos($create_query, "(\r\n ")) { + $create_query = str_replace("\r\n", $crlf, $create_query); + } elseif (mb_strpos($create_query, "(\n ")) { + $create_query = str_replace("\n", $crlf, $create_query); + } elseif (mb_strpos($create_query, "(\r ")) { + $create_query = str_replace("\r", $crlf, $create_query); + } + + /* + * Drop database name from VIEW creation. + * + * This is a bit tricky, but we need to issue SHOW CREATE TABLE with + * database name, but we don't want name to show up in CREATE VIEW + * statement. + */ + if ($view) { + $create_query = preg_replace( + '/' . preg_quote(Util::backquote($db), '/') . '\./', + '', + $create_query + ); + + // exclude definition of current user + if (isset($GLOBALS['sql_view_current_user'])) { + $create_query = preg_replace( + '/(^|\s)DEFINER=([\S]+)/', + '', + $create_query + ); + } + + // whether to replace existing view or not + if (isset($GLOBALS['sql_or_replace_view'])) { + $create_query = preg_replace( + '/^CREATE/', + 'CREATE OR REPLACE', + $create_query + ); + } + } + + // Substitute aliases in `CREATE` query. + $create_query = $this->replaceWithAliases( + $create_query, + $aliases, + $db, + $table, + $flag + ); + + // One warning per view. + if ($flag && $view) { + $warning = $this->_exportComment() + . $this->_exportComment( + __('It appears your database uses views;') + ) + . $this->_exportComment( + __('alias export may not work reliably in all cases.') + ) + . $this->_exportComment(); + } + + // Adding IF NOT EXISTS, if required. + if (isset($GLOBALS['sql_if_not_exists'])) { + $create_query = preg_replace( + '/^CREATE TABLE/', + 'CREATE TABLE IF NOT EXISTS', + $create_query + ); + } + + // Making the query MSSQL compatible. + if ($compat == 'MSSQL') { + $create_query = $this->_makeCreateTableMSSQLCompatible( + $create_query + ); + } + + // Views have no constraints, indexes, etc. They do not require any + // analysis. + if (! $view) { + if (empty($sql_backquotes)) { + // Option "Enclose table and column names with backquotes" + // was checked. + Context::$MODE |= Context::SQL_MODE_NO_ENCLOSING_QUOTES; + } + + // Using appropriate quotes. + if (($compat === 'MSSQL') || ($sql_backquotes === '"')) { + Context::$MODE |= Context::SQL_MODE_ANSI_QUOTES; + } + } + + /** + * Parser used for analysis. + * + * @var Parser + */ + $parser = new Parser($create_query); + + /** + * `CREATE TABLE` statement. + * + * @var CreateStatement + */ + $statement = $parser->statements[0]; + + if (! empty($statement->entityOptions)) { + $engine = $statement->entityOptions->has('ENGINE'); + } else { + $engine = ''; + } + + /* Avoid operation on ARCHIVE tables as those can not be altered */ + if (! empty($statement->fields) && (empty($engine) || strtoupper($engine) != 'ARCHIVE')) { + + /** + * Fragments containining definition of each constraint. + * + * @var array + */ + $constraints = []; + + /** + * Fragments containining definition of each index. + * + * @var array + */ + $indexes = []; + + /** + * Fragments containining definition of each FULLTEXT index. + * + * @var array + */ + $indexes_fulltext = []; + + /** + * Fragments containining definition of each foreign key that will + * be dropped. + * + * @var array + */ + $dropped = []; + + /** + * Fragment containining definition of the `AUTO_INCREMENT`. + * + * @var array + */ + $auto_increment = []; + + // Scanning each field of the `CREATE` statement to fill the arrays + // above. + // If the field is used in any of the arrays above, it is removed + // from the original definition. + // Also, AUTO_INCREMENT attribute is removed. + /** @var CreateDefinition $field */ + foreach ($statement->fields as $key => $field) { + if ($field->isConstraint) { + // Creating the parts that add constraints. + $constraints[] = $field::build($field); + unset($statement->fields[$key]); + } elseif (! empty($field->key)) { + // Creating the parts that add indexes (must not be + // constraints). + if ($field->key->type === 'FULLTEXT KEY') { + $indexes_fulltext[] = $field->build($field); + unset($statement->fields[$key]); + } else { + if (empty($GLOBALS['sql_if_not_exists'])) { + $indexes[] = str_replace( + 'COMMENT=\'', + 'COMMENT \'', + $field::build($field) + ); + unset($statement->fields[$key]); + } + } + } + + // Creating the parts that drop foreign keys. + if (! empty($field->key)) { + if ($field->key->type === 'FOREIGN KEY') { + $dropped[] = 'FOREIGN KEY ' . Context::escape( + $field->name + ); + unset($statement->fields[$key]); + } + } + + // Dropping AUTO_INCREMENT. + if (! empty($field->options)) { + if ($field->options->has('AUTO_INCREMENT') + && empty($GLOBALS['sql_if_not_exists']) + ) { + $auto_increment[] = $field::build($field); + $field->options->remove('AUTO_INCREMENT'); + } + } + } + + /** + * The header of the `ALTER` statement (`ALTER TABLE tbl`). + * + * @var string + */ + $alter_header = 'ALTER TABLE ' . + Util::backquoteCompat( + $table_alias, + $compat, + $sql_backquotes + ); + + /** + * The footer of the `ALTER` statement (usually ';') + * + * @var string + */ + $alter_footer = ';' . $crlf; + + // Generating constraints-related query. + if (! empty($constraints)) { + $sql_constraints_query = $alter_header . $crlf . ' ADD ' + . implode(',' . $crlf . ' ADD ', $constraints) + . $alter_footer; + + $sql_constraints = $this->generateComment( + $crlf, + $sql_constraints, + __('Constraints for dumped tables'), + __('Constraints for table'), + $table_alias, + $compat + ) . $sql_constraints_query; + } + + // Generating indexes-related query. + $sql_indexes_query = ''; + + if (! empty($indexes)) { + $sql_indexes_query .= $alter_header . $crlf . ' ADD ' + . implode(',' . $crlf . ' ADD ', $indexes) + . $alter_footer; + } + + if (! empty($indexes_fulltext)) { + // InnoDB supports one FULLTEXT index creation at a time. + // So FULLTEXT indexes are created one-by-one after other + // indexes where created. + $sql_indexes_query .= $alter_header . + ' ADD ' . implode( + $alter_footer . $alter_header . ' ADD ', + $indexes_fulltext + ) . $alter_footer; + } + + if (! empty($indexes) || ! empty($indexes_fulltext)) { + $sql_indexes = $this->generateComment( + $crlf, + $sql_indexes, + __('Indexes for dumped tables'), + __('Indexes for table'), + $table_alias, + $compat + ) . $sql_indexes_query; + } + + // Generating drop foreign keys-related query. + if (! empty($dropped)) { + $sql_drop_foreign_keys = $alter_header . $crlf . ' DROP ' + . implode(',' . $crlf . ' DROP ', $dropped) + . $alter_footer; + } + + // Generating auto-increment-related query. + if (! empty($auto_increment) && $update_indexes_increments) { + $sql_auto_increments_query = $alter_header . $crlf . ' MODIFY ' + . implode(',' . $crlf . ' MODIFY ', $auto_increment); + if (isset($GLOBALS['sql_auto_increment']) + && ($statement->entityOptions->has('AUTO_INCREMENT') !== false) + ) { + if (! isset($GLOBALS['table_data']) + || (isset($GLOBALS['table_data']) + && in_array($table, $GLOBALS['table_data'])) + ) { + $sql_auto_increments_query .= ', AUTO_INCREMENT=' + . $statement->entityOptions->has('AUTO_INCREMENT'); + } + } + $sql_auto_increments_query .= ';' . $crlf; + + $sql_auto_increments = $this->generateComment( + $crlf, + $sql_auto_increments, + __('AUTO_INCREMENT for dumped tables'), + __('AUTO_INCREMENT for table'), + $table_alias, + $compat + ) . $sql_auto_increments_query; + } + + // Removing the `AUTO_INCREMENT` attribute from the `CREATE TABLE` + // too. + if (! empty($statement->entityOptions) + && (empty($GLOBALS['sql_if_not_exists']) + || empty($GLOBALS['sql_auto_increment'])) + ) { + $statement->entityOptions->remove('AUTO_INCREMENT'); + } + + // Rebuilding the query. + $create_query = $statement->build(); + } + + $schema_create .= $create_query; + } + + $GLOBALS['dbi']->freeResult($result); + + // Restoring old mode. + Context::$MODE = $old_mode; + + return $warning . $schema_create . ($add_semicolon ? ';' . $crlf : ''); + } // end of the 'getTableDef()' function + + /** + * Returns $table's comments, relations etc. + * + * @param string $db database name + * @param string $table table name + * @param string $crlf end of line sequence + * @param bool $do_relation whether to include relation comments + * @param bool $do_mime whether to include mime comments + * @param array $aliases Aliases of db/table/columns + * + * @return string resulting comments + */ + private function _getTableComments( + $db, + $table, + $crlf, + $do_relation = false, + $do_mime = false, + array $aliases = [] + ) { + global $cfgRelation, $sql_backquotes; + + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + + $schema_create = ''; + + // Check if we can use Relations + list($res_rel, $have_rel) = $this->relation->getRelationsAndStatus( + $do_relation && ! empty($cfgRelation['relation']), + $db, + $table + ); + + if ($do_mime && $cfgRelation['mimework']) { + if (! ($mime_map = $this->transformations->getMime($db, $table, true))) { + unset($mime_map); + } + } + + if (isset($mime_map) && count($mime_map) > 0) { + $schema_create .= $this->_possibleCRLF() + . $this->_exportComment() + . $this->_exportComment( + __('MEDIA (MIME) TYPES FOR TABLE') . ' ' + . Util::backquote($table, $sql_backquotes) . ':' + ); + foreach ($mime_map as $mime_field => $mime) { + $schema_create .= $this->_exportComment( + ' ' + . Util::backquote($mime_field, $sql_backquotes) + ) + . $this->_exportComment( + ' ' + . Util::backquote( + $mime['mimetype'], + $sql_backquotes + ) + ); + } + $schema_create .= $this->_exportComment(); + } + + if ($have_rel) { + $schema_create .= $this->_possibleCRLF() + . $this->_exportComment() + . $this->_exportComment( + __('RELATIONSHIPS FOR TABLE') . ' ' + . Util::backquote($table_alias, $sql_backquotes) + . ':' + ); + + foreach ($res_rel as $rel_field => $rel) { + if ($rel_field != 'foreign_keys_data') { + $rel_field_alias = ! empty( + $aliases[$db]['tables'][$table]['columns'][$rel_field] + ) ? $aliases[$db]['tables'][$table]['columns'][$rel_field] + : $rel_field; + $schema_create .= $this->_exportComment( + ' ' + . Util::backquote( + $rel_field_alias, + $sql_backquotes + ) + ) + . $this->_exportComment( + ' ' + . Util::backquote( + $rel['foreign_table'], + $sql_backquotes + ) + . ' -> ' + . Util::backquote( + $rel['foreign_field'], + $sql_backquotes + ) + ); + } else { + foreach ($rel as $one_key) { + foreach ($one_key['index_list'] as $index => $field) { + $rel_field_alias = ! empty( + $aliases[$db]['tables'][$table]['columns'][$field] + ) ? $aliases[$db]['tables'][$table]['columns'][$field] + : $field; + $schema_create .= $this->_exportComment( + ' ' + . Util::backquote( + $rel_field_alias, + $sql_backquotes + ) + ) + . $this->_exportComment( + ' ' + . Util::backquote( + $one_key['ref_table_name'], + $sql_backquotes + ) + . ' -> ' + . Util::backquote( + $one_key['ref_index_list'][$index], + $sql_backquotes + ) + ); + } + } + } + } + $schema_create .= $this->_exportComment(); + } + + return $schema_create; + } // end of the '_getTableComments()' function + + /** + * Outputs table's structure + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $export_mode 'create_table','triggers','create_view', + * 'stand_in' + * @param string $export_type 'server', 'database', 'table' + * @param bool $relation whether to include relation comments + * @param bool $comments whether to include the pmadb-style column + * comments as comments in the structure; this is + * deprecated but the parameter is left here + * because export.php calls exportStructure() + * also for other export types which use this + * parameter + * @param bool $mime whether to include mime comments + * @param bool $dates whether to include creation/update/check dates + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportStructure( + $db, + $table, + $crlf, + $error_url, + $export_mode, + $export_type, + $relation = false, + $comments = false, + $mime = false, + $dates = false, + array $aliases = [] + ) { + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + if (isset($GLOBALS['sql_compatibility'])) { + $compat = $GLOBALS['sql_compatibility']; + } else { + $compat = 'NONE'; + } + + $formatted_table_name = Util::backquoteCompat( + $table_alias, + $compat, + isset($GLOBALS['sql_backquotes']) + ); + $dump = $this->_possibleCRLF() + . $this->_exportComment(str_repeat('-', 56)) + . $this->_possibleCRLF() + . $this->_exportComment(); + + switch ($export_mode) { + case 'create_table': + $dump .= $this->_exportComment( + __('Table structure for table') . ' ' . $formatted_table_name + ); + $dump .= $this->_exportComment(); + $dump .= $this->getTableDef( + $db, + $table, + $crlf, + $error_url, + $dates, + true, + false, + true, + $aliases + ); + $dump .= $this->_getTableComments( + $db, + $table, + $crlf, + $relation, + $mime, + $aliases + ); + break; + case 'triggers': + $dump = ''; + $delimiter = '$$'; + $triggers = $GLOBALS['dbi']->getTriggers($db, $table, $delimiter); + if ($triggers) { + $dump .= $this->_possibleCRLF() + . $this->_exportComment() + . $this->_exportComment( + __('Triggers') . ' ' . $formatted_table_name + ) + . $this->_exportComment(); + $used_alias = false; + $trigger_query = ''; + foreach ($triggers as $trigger) { + if (! empty($GLOBALS['sql_drop_table'])) { + $trigger_query .= $trigger['drop'] . ';' . $crlf; + } + + $trigger_query .= 'DELIMITER ' . $delimiter . $crlf; + $trigger_query .= $this->replaceWithAliases( + $trigger['create'], + $aliases, + $db, + $table, + $flag + ); + if ($flag) { + $used_alias = true; + } + $trigger_query .= 'DELIMITER ;' . $crlf; + } + // One warning per table. + if ($used_alias) { + $dump .= $this->_exportComment( + __('It appears your table uses triggers;') + ) + . $this->_exportComment( + __('alias export may not work reliably in all cases.') + ) + . $this->_exportComment(); + } + $dump .= $trigger_query; + } + break; + case 'create_view': + if (empty($GLOBALS['sql_views_as_tables'])) { + $dump .= $this->_exportComment( + __('Structure for view') + . ' ' + . $formatted_table_name + ) + . $this->_exportComment(); + // delete the stand-in table previously created (if any) + if ($export_type != 'table') { + $dump .= 'DROP TABLE IF EXISTS ' + . Util::backquote($table_alias) . ';' . $crlf; + } + $dump .= $this->getTableDef( + $db, + $table, + $crlf, + $error_url, + $dates, + true, + true, + true, + $aliases + ); + } else { + $dump .= $this->_exportComment( + sprintf( + __('Structure for view %s exported as a table'), + $formatted_table_name + ) + ) + . $this->_exportComment(); + // delete the stand-in table previously created (if any) + if ($export_type != 'table') { + $dump .= 'DROP TABLE IF EXISTS ' + . Util::backquote($table_alias) . ';' . $crlf; + } + $dump .= $this->_getTableDefForView( + $db, + $table, + $crlf, + true, + $aliases + ); + } + break; + case 'stand_in': + $dump .= $this->_exportComment( + __('Stand-in structure for view') . ' ' . $formatted_table_name + ) + . $this->_exportComment( + __('(See below for the actual view)') + ) + . $this->_exportComment(); + // export a stand-in definition to resolve view dependencies + $dump .= $this->getTableDefStandIn($db, $table, $crlf, $aliases); + } // end switch + + // this one is built by getTableDef() to use in table copy/move + // but not in the case of export + unset($GLOBALS['sql_constraints_query']); + + return $this->export->outputHandler($dump); + } + + /** + * Outputs the content of a table in SQL format + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $sql_query SQL query for obtaining data + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportData( + $db, + $table, + $crlf, + $error_url, + $sql_query, + array $aliases = [] + ) { + global $current_row, $sql_backquotes; + + // Do not export data for merge tables + if ($GLOBALS['dbi']->getTable($db, $table)->isMerge()) { + return true; + } + + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + + if (isset($GLOBALS['sql_compatibility'])) { + $compat = $GLOBALS['sql_compatibility']; + } else { + $compat = 'NONE'; + } + + $formatted_table_name = Util::backquoteCompat( + $table_alias, + $compat, + $sql_backquotes + ); + + // Do not export data for a VIEW, unless asked to export the view as a table + // (For a VIEW, this is called only when exporting a single VIEW) + if ($GLOBALS['dbi']->getTable($db, $table)->isView() + && empty($GLOBALS['sql_views_as_tables']) + ) { + $head = $this->_possibleCRLF() + . $this->_exportComment() + . $this->_exportComment('VIEW ' . $formatted_table_name) + . $this->_exportComment(__('Data:') . ' ' . __('None')) + . $this->_exportComment() + . $this->_possibleCRLF(); + + return $this->export->outputHandler($head); + } + + $result = $GLOBALS['dbi']->tryQuery( + $sql_query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_UNBUFFERED + ); + // a possible error: the table has crashed + $tmp_error = $GLOBALS['dbi']->getError(); + if ($tmp_error) { + $message = sprintf(__('Error reading data for table %s:'), "$db.$table"); + $message .= ' ' . $tmp_error; + if (! defined('TESTSUITE')) { + trigger_error($message, E_USER_ERROR); + } + return $this->export->outputHandler( + $this->_exportComment($message) + ); + } + + if ($result == false) { + $GLOBALS['dbi']->freeResult($result); + + return true; + } + + $fields_cnt = $GLOBALS['dbi']->numFields($result); + + // Get field information + $fields_meta = $GLOBALS['dbi']->getFieldsMeta($result); + $field_flags = []; + for ($j = 0; $j < $fields_cnt; $j++) { + $field_flags[$j] = $GLOBALS['dbi']->fieldFlags($result, $j); + } + + $field_set = []; + for ($j = 0; $j < $fields_cnt; $j++) { + $col_as = $fields_meta[$j]->name; + if (! empty($aliases[$db]['tables'][$table]['columns'][$col_as])) { + $col_as = $aliases[$db]['tables'][$table]['columns'][$col_as]; + } + $field_set[$j] = Util::backquoteCompat( + $col_as, + $compat, + $sql_backquotes + ); + } + + if (isset($GLOBALS['sql_type']) + && $GLOBALS['sql_type'] == 'UPDATE' + ) { + // update + $schema_insert = 'UPDATE '; + if (isset($GLOBALS['sql_ignore'])) { + $schema_insert .= 'IGNORE '; + } + // avoid EOL blank + $schema_insert .= Util::backquoteCompat( + $table_alias, + $compat, + $sql_backquotes + ) . ' SET'; + } else { + // insert or replace + if (isset($GLOBALS['sql_type']) + && $GLOBALS['sql_type'] == 'REPLACE' + ) { + $sql_command = 'REPLACE'; + } else { + $sql_command = 'INSERT'; + } + + // delayed inserts? + if (isset($GLOBALS['sql_delayed'])) { + $insert_delayed = ' DELAYED'; + } else { + $insert_delayed = ''; + } + + // insert ignore? + if (isset($GLOBALS['sql_type']) + && $GLOBALS['sql_type'] == 'INSERT' + && isset($GLOBALS['sql_ignore']) + ) { + $insert_delayed .= ' IGNORE'; + } + //truncate table before insert + if (isset($GLOBALS['sql_truncate']) + && $GLOBALS['sql_truncate'] + && $sql_command == 'INSERT' + ) { + $truncate = 'TRUNCATE TABLE ' + . Util::backquoteCompat( + $table_alias, + $compat, + $sql_backquotes + ) . ";"; + $truncatehead = $this->_possibleCRLF() + . $this->_exportComment() + . $this->_exportComment( + __('Truncate table before insert') . ' ' + . $formatted_table_name + ) + . $this->_exportComment() + . $crlf; + $this->export->outputHandler($truncatehead); + $this->export->outputHandler($truncate); + } + + // scheme for inserting fields + if ($GLOBALS['sql_insert_syntax'] == 'complete' + || $GLOBALS['sql_insert_syntax'] == 'both' + ) { + $fields = implode(', ', $field_set); + $schema_insert = $sql_command . $insert_delayed . ' INTO ' + . Util::backquoteCompat( + $table_alias, + $compat, + $sql_backquotes + ) + // avoid EOL blank + . ' (' . $fields . ') VALUES'; + } else { + $schema_insert = $sql_command . $insert_delayed . ' INTO ' + . Util::backquoteCompat( + $table_alias, + $compat, + $sql_backquotes + ) + . ' VALUES'; + } + } + + //\x08\\x09, not required + $current_row = 0; + $query_size = 0; + if (($GLOBALS['sql_insert_syntax'] == 'extended' + || $GLOBALS['sql_insert_syntax'] == 'both') + && (! isset($GLOBALS['sql_type']) + || $GLOBALS['sql_type'] != 'UPDATE') + ) { + $separator = ','; + $schema_insert .= $crlf; + } else { + $separator = ';'; + } + + while ($row = $GLOBALS['dbi']->fetchRow($result)) { + if ($current_row == 0) { + $head = $this->_possibleCRLF() + . $this->_exportComment() + . $this->_exportComment( + __('Dumping data for table') . ' ' + . $formatted_table_name + ) + . $this->_exportComment() + . $crlf; + if (! $this->export->outputHandler($head)) { + return false; + } + } + // We need to SET IDENTITY_INSERT ON for MSSQL + if (isset($GLOBALS['sql_compatibility']) + && $GLOBALS['sql_compatibility'] == 'MSSQL' + && $current_row == 0 + ) { + if (! $this->export->outputHandler( + 'SET IDENTITY_INSERT ' + . Util::backquoteCompat( + $table_alias, + $compat, + $sql_backquotes + ) + . ' ON ;' . $crlf + ) + ) { + return false; + } + } + $current_row++; + $values = []; + for ($j = 0; $j < $fields_cnt; $j++) { + // NULL + if (! isset($row[$j]) || $row[$j] === null) { + $values[] = 'NULL'; + } elseif ($fields_meta[$j]->numeric + && $fields_meta[$j]->type != 'timestamp' + && ! $fields_meta[$j]->blob + ) { + // a number + // timestamp is numeric on some MySQL 4.1, BLOBs are + // sometimes numeric + $values[] = $row[$j]; + } elseif (false !== stripos($field_flags[$j], 'BINARY') + && isset($GLOBALS['sql_hex_for_binary']) + ) { + // a true BLOB + // - mysqldump only generates hex data when the --hex-blob + // option is used, for fields having the binary attribute + // no hex is generated + // - a TEXT field returns type blob but a real blob + // returns also the 'binary' flag + + // empty blobs need to be different, but '0' is also empty + // :-( + if (empty($row[$j]) && $row[$j] != '0') { + $values[] = '\'\''; + } else { + $values[] = '0x' . bin2hex($row[$j]); + } + } elseif ($fields_meta[$j]->type == 'bit') { + // detection of 'bit' works only on mysqli extension + $values[] = "b'" . $GLOBALS['dbi']->escapeString( + Util::printableBitValue( + (int) $row[$j], + (int) $fields_meta[$j]->length + ) + ) + . "'"; + } elseif ($fields_meta[$j]->type === 'geometry') { + // export GIS types as hex + $values[] = '0x' . bin2hex($row[$j]); + } elseif (! empty($GLOBALS['exporting_metadata']) + && $row[$j] == '@LAST_PAGE' + ) { + $values[] = '@LAST_PAGE'; + } else { + // something else -> treat as a string + $values[] = '\'' + . $GLOBALS['dbi']->escapeString($row[$j]) + . '\''; + } // end if + } // end for + + // should we make update? + if (isset($GLOBALS['sql_type']) + && $GLOBALS['sql_type'] == 'UPDATE' + ) { + $insert_line = $schema_insert; + for ($i = 0; $i < $fields_cnt; $i++) { + if (0 == $i) { + $insert_line .= ' '; + } + if ($i > 0) { + // avoid EOL blank + $insert_line .= ','; + } + $insert_line .= $field_set[$i] . ' = ' . $values[$i]; + } + + list($tmp_unique_condition, $tmp_clause_is_unique) + = Util::getUniqueCondition( + $result, // handle + $fields_cnt, // fields_cnt + $fields_meta, // fields_meta + $row, // row + false, // force_unique + false, // restrict_to_table + null // analyzed_sql_results + ); + $insert_line .= ' WHERE ' . $tmp_unique_condition; + unset($tmp_unique_condition, $tmp_clause_is_unique); + } else { + // Extended inserts case + if ($GLOBALS['sql_insert_syntax'] == 'extended' + || $GLOBALS['sql_insert_syntax'] == 'both' + ) { + if ($current_row == 1) { + $insert_line = $schema_insert . '(' + . implode(', ', $values) . ')'; + } else { + $insert_line = '(' . implode(', ', $values) . ')'; + $insertLineSize = mb_strlen($insert_line); + $sql_max_size = $GLOBALS['sql_max_query_size']; + if (isset($sql_max_size) + && $sql_max_size > 0 + && $query_size + $insertLineSize > $sql_max_size + ) { + if (! $this->export->outputHandler(';' . $crlf)) { + return false; + } + $query_size = 0; + $current_row = 1; + $insert_line = $schema_insert . $insert_line; + } + } + $query_size += mb_strlen($insert_line); + // Other inserts case + } else { + $insert_line = $schema_insert + . '(' . implode(', ', $values) . ')'; + } + } + unset($values); + + if (! $this->export->outputHandler( + ($current_row == 1 ? '' : $separator . $crlf) + . $insert_line + ) + ) { + return false; + } + } // end while + + if ($current_row > 0) { + if (! $this->export->outputHandler(';' . $crlf)) { + return false; + } + } + + // We need to SET IDENTITY_INSERT OFF for MSSQL + if (isset($GLOBALS['sql_compatibility']) + && $GLOBALS['sql_compatibility'] == 'MSSQL' + && $current_row > 0 + ) { + $outputSucceeded = $this->export->outputHandler( + $crlf . 'SET IDENTITY_INSERT ' + . Util::backquoteCompat( + $table_alias, + $compat, + $sql_backquotes + ) + . ' OFF;' . $crlf + ); + if (! $outputSucceeded) { + return false; + } + } + + $GLOBALS['dbi']->freeResult($result); + + return true; + } // end of the 'exportData()' function + + /** + * Make a create table statement compatible with MSSQL + * + * @param string $create_query MySQL create table statement + * + * @return string MSSQL compatible create table statement + */ + private function _makeCreateTableMSSQLCompatible($create_query) + { + // In MSSQL + // 1. No 'IF NOT EXISTS' in CREATE TABLE + // 2. DATE field doesn't exists, we will use DATETIME instead + // 3. UNSIGNED attribute doesn't exist + // 4. No length on INT, TINYINT, SMALLINT, BIGINT and no precision on + // FLOAT fields + // 5. No KEY and INDEX inside CREATE TABLE + // 6. DOUBLE field doesn't exists, we will use FLOAT instead + + $create_query = preg_replace( + "/^CREATE TABLE IF NOT EXISTS/", + 'CREATE TABLE', + $create_query + ); + // first we need to replace all lines ended with '" DATE ...,\n' + // last preg_replace preserve us from situation with date text + // inside DEFAULT field value + $create_query = preg_replace( + "/\" date DEFAULT NULL(,)?\n/", + '" datetime DEFAULT NULL$1' . "\n", + $create_query + ); + $create_query = preg_replace( + "/\" date NOT NULL(,)?\n/", + '" datetime NOT NULL$1' . "\n", + $create_query + ); + $create_query = preg_replace( + '/" date NOT NULL DEFAULT \'([^\'])/', + '" datetime NOT NULL DEFAULT \'$1', + $create_query + ); + + // next we need to replace all lines ended with ') UNSIGNED ...,' + // last preg_replace preserve us from situation with unsigned text + // inside DEFAULT field value + $create_query = preg_replace( + "/\) unsigned NOT NULL(,)?\n/", + ') NOT NULL$1' . "\n", + $create_query + ); + $create_query = preg_replace( + "/\) unsigned DEFAULT NULL(,)?\n/", + ') DEFAULT NULL$1' . "\n", + $create_query + ); + $create_query = preg_replace( + '/\) unsigned NOT NULL DEFAULT \'([^\'])/', + ') NOT NULL DEFAULT \'$1', + $create_query + ); + + // we need to replace all lines ended with + // '" INT|TINYINT([0-9]{1,}) ...,' last preg_replace preserve us + // from situation with int([0-9]{1,}) text inside DEFAULT field + // value + $create_query = preg_replace( + '/" (int|tinyint|smallint|bigint)\([0-9]+\) DEFAULT NULL(,)?\n/', + '" $1 DEFAULT NULL$2' . "\n", + $create_query + ); + $create_query = preg_replace( + '/" (int|tinyint|smallint|bigint)\([0-9]+\) NOT NULL(,)?\n/', + '" $1 NOT NULL$2' . "\n", + $create_query + ); + $create_query = preg_replace( + '/" (int|tinyint|smallint|bigint)\([0-9]+\) NOT NULL DEFAULT \'([^\'])/', + '" $1 NOT NULL DEFAULT \'$2', + $create_query + ); + + // we need to replace all lines ended with + // '" FLOAT|DOUBLE([0-9,]{1,}) ...,' + // last preg_replace preserve us from situation with + // float([0-9,]{1,}) text inside DEFAULT field value + $create_query = preg_replace( + '/" (float|double)(\([0-9]+,[0-9,]+\))? DEFAULT NULL(,)?\n/', + '" float DEFAULT NULL$3' . "\n", + $create_query + ); + $create_query = preg_replace( + '/" (float|double)(\([0-9,]+,[0-9,]+\))? NOT NULL(,)?\n/', + '" float NOT NULL$3' . "\n", + $create_query + ); + return preg_replace( + '/" (float|double)(\([0-9,]+,[0-9,]+\))? NOT NULL DEFAULT \'([^\'])/', + '" float NOT NULL DEFAULT \'$3', + $create_query + ); + + // @todo remove indexes from CREATE TABLE + } + + /** + * replaces db/table/column names with their aliases + * + * @param string $sql_query SQL query in which aliases are to be substituted + * @param array $aliases Alias information for db/table/column + * @param string $db the database name + * @param string $table the tablename + * @param string $flag the flag denoting whether any replacement was done + * + * @return string query replaced with aliases + */ + public function replaceWithAliases( + $sql_query, + array $aliases, + $db, + $table = '', + &$flag = null + ) { + $flag = false; + + /** + * The parser of this query. + * + * @var Parser $parser + */ + $parser = new Parser($sql_query); + + if (empty($parser->statements[0])) { + return $sql_query; + } + + /** + * The statement that represents the query. + * + * @var CreateStatement $statement + */ + $statement = $parser->statements[0]; + + /** + * Old database name. + * + * @var string $old_database + */ + $old_database = $db; + + // Replacing aliases in `CREATE TABLE` statement. + if ($statement->options->has('TABLE')) { + // Extracting the name of the old database and table from the + // statement to make sure the parameters are corect. + if (! empty($statement->name->database)) { + $old_database = $statement->name->database; + } + + /** + * Old table name. + * + * @var string $old_table + */ + $old_table = $statement->name->table; + + // Finding the aliased database name. + // The database might be empty so we have to add a few checks. + $new_database = null; + if (! empty($statement->name->database)) { + $new_database = $statement->name->database; + if (! empty($aliases[$old_database]['alias'])) { + $new_database = $aliases[$old_database]['alias']; + } + } + + // Finding the aliases table name. + $new_table = $old_table; + if (! empty($aliases[$old_database]['tables'][$old_table]['alias'])) { + $new_table = $aliases[$old_database]['tables'][$old_table]['alias']; + } + + // Replacing new values. + if (($statement->name->database !== $new_database) + || ($statement->name->table !== $new_table) + ) { + $statement->name->database = $new_database; + $statement->name->table = $new_table; + $statement->name->expr = null; // Force rebuild. + $flag = true; + } + + /** @var CreateDefinition $field */ + foreach ($statement->fields as $field) { + // Column name. + if (! empty($field->type)) { + if (! empty($aliases[$old_database]['tables'][$old_table]['columns'][$field->name])) { + $field->name = $aliases[$old_database]['tables'][$old_table]['columns'][$field->name]; + $flag = true; + } + } + + // Key's columns. + if (! empty($field->key)) { + foreach ($field->key->columns as $key => $column) { + if (! empty($aliases[$old_database]['tables'][$old_table]['columns'][$column['name']])) { + $field->key->columns[$key]['name'] = $aliases[$old_database]['tables'][$old_table]['columns'][$column['name']]; + $flag = true; + } + } + } + + // References. + if (! empty($field->references)) { + $ref_table = $field->references->table->table; + // Replacing table. + if (! empty($aliases[$old_database]['tables'][$ref_table]['alias'])) { + $field->references->table->table + = $aliases[$old_database]['tables'][$ref_table]['alias']; + $field->references->table->expr = null; + $flag = true; + } + // Replacing column names. + foreach ($field->references->columns as $key => $column) { + if (! empty($aliases[$old_database]['tables'][$ref_table]['columns'][$column])) { + $field->references->columns[$key] + = $aliases[$old_database]['tables'][$ref_table]['columns'][$column]; + $flag = true; + } + } + } + } + } elseif ($statement->options->has('TRIGGER')) { + // Extracting the name of the old database and table from the + // statement to make sure the parameters are corect. + if (! empty($statement->table->database)) { + $old_database = $statement->table->database; + } + + /** + * Old table name. + * + * @var string $old_table + */ + $old_table = $statement->table->table; + + if (! empty($aliases[$old_database]['tables'][$old_table]['alias'])) { + $statement->table->table + = $aliases[$old_database]['tables'][$old_table]['alias']; + $statement->table->expr = null; // Force rebuild. + $flag = true; + } + } + + if ($statement->options->has('TRIGGER') + || $statement->options->has('PROCEDURE') + || $statement->options->has('FUNCTION') + || $statement->options->has('VIEW') + ) { + // Repalcing the body. + for ($i = 0, $count = count($statement->body); $i < $count; ++$i) { + + /** + * Token parsed at this moment. + * + * @var Token $token + */ + $token = $statement->body[$i]; + + // Replacing only symbols (that are not variables) and unknown + // identifiers. + if (($token->type === Token::TYPE_SYMBOL) + && (! ($token->flags & Token::FLAG_SYMBOL_VARIABLE)) + || (($token->type === Token::TYPE_KEYWORD) + && (! ($token->flags & Token::FLAG_KEYWORD_RESERVED)) + || ($token->type === Token::TYPE_NONE)) + ) { + $alias = $this->getAlias($aliases, $token->value); + if (! empty($alias)) { + // Replacing the token. + $token->token = Context::escape($alias); + $flag = true; + } + } + } + } + + return $statement->build(); + } + + /** + * Generate comment + * + * @param string $crlf Carriage return character + * @param string|null $sql_statement SQL statement + * @param string $comment1 Comment for dumped table + * @param string $comment2 Comment for current table + * @param string $table_alias Table alias + * @param string $compat Compatibility mode + * + * @return string + */ + protected function generateComment( + $crlf, + ?string $sql_statement, + $comment1, + $comment2, + $table_alias, + $compat + ) { + if (! isset($sql_statement)) { + if (isset($GLOBALS['no_constraints_comments'])) { + $sql_statement = ''; + } else { + $sql_statement = $crlf + . $this->_exportComment() + . $this->_exportComment($comment1) + . $this->_exportComment(); + } + } + + // comments for current table + if (! isset($GLOBALS['no_constraints_comments'])) { + $sql_statement .= $crlf + . $this->_exportComment() + . $this->_exportComment( + $comment2 . ' ' . Util::backquoteCompat( + $table_alias, + $compat, + isset($GLOBALS['sql_backquotes']) + ) + ) + . $this->_exportComment(); + } + + return $sql_statement; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportTexytext.php b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportTexytext.php new file mode 100644 index 0000000..a2fdec8 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportTexytext.php @@ -0,0 +1,624 @@ +setProperties(); + } + + /** + * Sets the export Texy! text properties + * + * @return void + */ + protected function setProperties() + { + $exportPluginProperties = new ExportPluginProperties(); + $exportPluginProperties->setText('Texy! text'); + $exportPluginProperties->setExtension('txt'); + $exportPluginProperties->setMimeType('text/plain'); + $exportPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $exportPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // what to dump (structure/data/both) main group + $dumpWhat = new OptionsPropertyMainGroup( + "general_opts", + __('Dump table') + ); + // create primary items and add them to the group + $leaf = new RadioPropertyItem("structure_or_data"); + $leaf->setValues( + [ + 'structure' => __('structure'), + 'data' => __('data'), + 'structure_and_data' => __('structure and data'), + ] + ); + $dumpWhat->addProperty($leaf); + // add the main group to the root group + $exportSpecificOptions->addProperty($dumpWhat); + + // data options main group + $dataOptions = new OptionsPropertyMainGroup( + "data", + __('Data dump options') + ); + $dataOptions->setForce('structure'); + // create primary items and add them to the group + $leaf = new BoolPropertyItem( + "columns", + __('Put columns names in the first row') + ); + $dataOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + 'null', + __('Replace NULL with:') + ); + $dataOptions->addProperty($leaf); + // add the main group to the root group + $exportSpecificOptions->addProperty($dataOptions); + + // set the options for the export plugin property item + $exportPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $exportPluginProperties; + } + + /** + * Outputs export header + * + * @return bool Whether it succeeded + */ + public function exportHeader() + { + return true; + } + + /** + * Outputs export footer + * + * @return bool Whether it succeeded + */ + public function exportFooter() + { + return true; + } + + /** + * Outputs database header + * + * @param string $db Database name + * @param string $db_alias Alias of db + * + * @return bool Whether it succeeded + */ + public function exportDBHeader($db, $db_alias = '') + { + if (empty($db_alias)) { + $db_alias = $db; + } + + return $this->export->outputHandler( + '===' . __('Database') . ' ' . $db_alias . "\n\n" + ); + } + + /** + * Outputs database footer + * + * @param string $db Database name + * + * @return bool Whether it succeeded + */ + public function exportDBFooter($db) + { + return true; + } + + /** + * Outputs CREATE DATABASE statement + * + * @param string $db Database name + * @param string $export_type 'server', 'database', 'table' + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBCreate($db, $export_type, $db_alias = '') + { + return true; + } + + /** + * Outputs the content of a table in NHibernate format + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $sql_query SQL query for obtaining data + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportData( + $db, + $table, + $crlf, + $error_url, + $sql_query, + array $aliases = [] + ) { + global $what; + + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + + if (! $this->export->outputHandler( + '== ' . __('Dumping data for table') . ' ' . $table_alias . "\n\n" + ) + ) { + return false; + } + + // Gets the data from the database + $result = $GLOBALS['dbi']->query( + $sql_query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_UNBUFFERED + ); + $fields_cnt = $GLOBALS['dbi']->numFields($result); + + // If required, get fields name at the first line + if (isset($GLOBALS[$what . '_columns'])) { + $text_output = "|------\n"; + for ($i = 0; $i < $fields_cnt; $i++) { + $col_as = $GLOBALS['dbi']->fieldName($result, $i); + if (! empty($aliases[$db]['tables'][$table]['columns'][$col_as])) { + $col_as = $aliases[$db]['tables'][$table]['columns'][$col_as]; + } + $text_output .= '|' + . htmlspecialchars(stripslashes($col_as)); + } // end for + $text_output .= "\n|------\n"; + if (! $this->export->outputHandler($text_output)) { + return false; + } + } // end if + + // Format the data + while ($row = $GLOBALS['dbi']->fetchRow($result)) { + $text_output = ''; + for ($j = 0; $j < $fields_cnt; $j++) { + if (! isset($row[$j]) || $row[$j] === null) { + $value = $GLOBALS[$what . '_null']; + } elseif ($row[$j] == '0' || $row[$j] != '') { + $value = $row[$j]; + } else { + $value = ' '; + } + $text_output .= '|' + . str_replace( + '|', + '|', + htmlspecialchars($value) + ); + } // end for + $text_output .= "\n"; + if (! $this->export->outputHandler($text_output)) { + return false; + } + } // end while + $GLOBALS['dbi']->freeResult($result); + + return true; + } + + /** + * Returns a stand-in CREATE definition to resolve view dependencies + * + * @param string $db the database name + * @param string $view the view name + * @param string $crlf the end of line sequence + * @param array $aliases Aliases of db/table/columns + * + * @return string resulting definition + */ + public function getTableDefStandIn($db, $view, $crlf, $aliases = []) + { + $text_output = ''; + + /** + * Get the unique keys in the table + */ + $unique_keys = []; + $keys = $GLOBALS['dbi']->getTableIndexes($db, $view); + foreach ($keys as $key) { + if ($key['Non_unique'] == 0) { + $unique_keys[] = $key['Column_name']; + } + } + + /** + * Gets fields properties + */ + $GLOBALS['dbi']->selectDb($db); + + /** + * Displays the table structure + */ + + $text_output .= "|------\n" + . '|' . __('Column') + . '|' . __('Type') + . '|' . __('Null') + . '|' . __('Default') + . "\n|------\n"; + + $columns = $GLOBALS['dbi']->getColumns($db, $view); + foreach ($columns as $column) { + $col_as = $column['Field'] ?? null; + if (! empty($aliases[$db]['tables'][$view]['columns'][$col_as])) { + $col_as = $aliases[$db]['tables'][$view]['columns'][$col_as]; + } + $text_output .= $this->formatOneColumnDefinition( + $column, + $unique_keys, + $col_as + ); + $text_output .= "\n"; + } // end foreach + + return $text_output; + } + + /** + * Returns $table's CREATE definition + * + * @param string $db the database name + * @param string $table the table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param bool $do_relation whether to include relation comments + * @param bool $do_comments whether to include the pmadb-style column + * comments as comments in the structure; + * this is deprecated but the parameter is + * left here because export.php calls + * $this->exportStructure() also for other + * export types which use this parameter + * @param bool $do_mime whether to include mime comments + * @param bool $show_dates whether to include creation/update/check dates + * @param bool $add_semicolon whether to add semicolon and end-of-line + * at the end + * @param bool $view whether we're handling a view + * @param array $aliases Aliases of db/table/columns + * + * @return string resulting schema + */ + public function getTableDef( + $db, + $table, + $crlf, + $error_url, + $do_relation, + $do_comments, + $do_mime, + $show_dates = false, + $add_semicolon = true, + $view = false, + array $aliases = [] + ) { + global $cfgRelation; + + $text_output = ''; + + /** + * Get the unique keys in the table + */ + $unique_keys = []; + $keys = $GLOBALS['dbi']->getTableIndexes($db, $table); + foreach ($keys as $key) { + if ($key['Non_unique'] == 0) { + $unique_keys[] = $key['Column_name']; + } + } + + /** + * Gets fields properties + */ + $GLOBALS['dbi']->selectDb($db); + + // Check if we can use Relations + list($res_rel, $have_rel) = $this->relation->getRelationsAndStatus( + $do_relation && ! empty($cfgRelation['relation']), + $db, + $table + ); + + /** + * Displays the table structure + */ + + $text_output .= "|------\n"; + $text_output .= '|' . __('Column'); + $text_output .= '|' . __('Type'); + $text_output .= '|' . __('Null'); + $text_output .= '|' . __('Default'); + if ($do_relation && $have_rel) { + $text_output .= '|' . __('Links to'); + } + if ($do_comments) { + $text_output .= '|' . __('Comments'); + $comments = $this->relation->getComments($db, $table); + } + if ($do_mime && $cfgRelation['mimework']) { + $text_output .= '|' . __('Media (MIME) type'); + $mime_map = $this->transformations->getMime($db, $table, true); + } + $text_output .= "\n|------\n"; + + $columns = $GLOBALS['dbi']->getColumns($db, $table); + foreach ($columns as $column) { + $col_as = $column['Field']; + if (! empty($aliases[$db]['tables'][$table]['columns'][$col_as])) { + $col_as = $aliases[$db]['tables'][$table]['columns'][$col_as]; + } + $text_output .= $this->formatOneColumnDefinition( + $column, + $unique_keys, + $col_as + ); + $field_name = $column['Field']; + if ($do_relation && $have_rel) { + $text_output .= '|' . htmlspecialchars( + $this->getRelationString( + $res_rel, + $field_name, + $db, + $aliases + ) + ); + } + if ($do_comments && $cfgRelation['commwork']) { + $text_output .= '|' + . (isset($comments[$field_name]) + ? htmlspecialchars($comments[$field_name]) + : ''); + } + if ($do_mime && $cfgRelation['mimework']) { + $text_output .= '|' + . (isset($mime_map[$field_name]) + ? htmlspecialchars( + str_replace('_', '/', $mime_map[$field_name]['mimetype']) + ) + : ''); + } + + $text_output .= "\n"; + } // end foreach + + return $text_output; + } // end of the '$this->getTableDef()' function + + /** + * Outputs triggers + * + * @param string $db database name + * @param string $table table name + * + * @return string Formatted triggers list + */ + public function getTriggers($db, $table) + { + $dump = "|------\n"; + $dump .= '|' . __('Name'); + $dump .= '|' . __('Time'); + $dump .= '|' . __('Event'); + $dump .= '|' . __('Definition'); + $dump .= "\n|------\n"; + + $triggers = $GLOBALS['dbi']->getTriggers($db, $table); + + foreach ($triggers as $trigger) { + $dump .= '|' . $trigger['name']; + $dump .= '|' . $trigger['action_timing']; + $dump .= '|' . $trigger['event_manipulation']; + $dump .= '|' . + str_replace( + '|', + '|', + htmlspecialchars($trigger['definition']) + ); + $dump .= "\n"; + } + + return $dump; + } + + /** + * Outputs table's structure + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $export_mode 'create_table', 'triggers', 'create_view', + * 'stand_in' + * @param string $export_type 'server', 'database', 'table' + * @param bool $do_relation whether to include relation comments + * @param bool $do_comments whether to include the pmadb-style column + * comments as comments in the structure; + * this is deprecated but the parameter is + * left here because export.php calls + * $this->exportStructure() also for other + * export types which use this parameter + * @param bool $do_mime whether to include mime comments + * @param bool $dates whether to include creation/update/check dates + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportStructure( + $db, + $table, + $crlf, + $error_url, + $export_mode, + $export_type, + $do_relation = false, + $do_comments = false, + $do_mime = false, + $dates = false, + array $aliases = [] + ) { + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + $dump = ''; + + switch ($export_mode) { + case 'create_table': + $dump .= '== ' . __('Table structure for table') . ' ' + . $table_alias . "\n\n"; + $dump .= $this->getTableDef( + $db, + $table, + $crlf, + $error_url, + $do_relation, + $do_comments, + $do_mime, + $dates, + true, + false, + $aliases + ); + break; + case 'triggers': + $dump = ''; + $triggers = $GLOBALS['dbi']->getTriggers($db, $table); + if ($triggers) { + $dump .= '== ' . __('Triggers') . ' ' . $table_alias . "\n\n"; + $dump .= $this->getTriggers($db, $table); + } + break; + case 'create_view': + $dump .= '== ' . __('Structure for view') . ' ' . $table_alias . "\n\n"; + $dump .= $this->getTableDef( + $db, + $table, + $crlf, + $error_url, + $do_relation, + $do_comments, + $do_mime, + $dates, + true, + true, + $aliases + ); + break; + case 'stand_in': + $dump .= '== ' . __('Stand-in structure for view') + . ' ' . $table . "\n\n"; + // export a stand-in definition to resolve view dependencies + $dump .= $this->getTableDefStandIn($db, $table, $crlf, $aliases); + } // end switch + + return $this->export->outputHandler($dump); + } + + /** + * Formats the definition for one column + * + * @param array $column info about this column + * @param array $unique_keys unique keys for this table + * @param string $col_alias Column Alias + * + * @return string Formatted column definition + */ + public function formatOneColumnDefinition( + $column, + $unique_keys, + $col_alias = '' + ) { + if (empty($col_alias)) { + $col_alias = $column['Field']; + } + $extracted_columnspec + = Util::extractColumnSpec($column['Type']); + $type = $extracted_columnspec['print_type']; + if (empty($type)) { + $type = ' '; + } + + if (! isset($column['Default'])) { + if ($column['Null'] != 'NO') { + $column['Default'] = 'NULL'; + } + } + + $fmt_pre = ''; + $fmt_post = ''; + if (in_array($column['Field'], $unique_keys)) { + $fmt_pre = '**' . $fmt_pre; + $fmt_post .= '**'; + } + if ($column['Key'] == 'PRI') { + $fmt_pre = '//' . $fmt_pre; + $fmt_post .= '//'; + } + $definition = '|' + . $fmt_pre . htmlspecialchars($col_alias) . $fmt_post; + $definition .= '|' . htmlspecialchars($type); + $definition .= '|' + . (($column['Null'] == '' || $column['Null'] == 'NO') + ? __('No') : __('Yes')); + $definition .= '|' + . htmlspecialchars( + isset($column['Default']) ? $column['Default'] : '' + ); + + return $definition; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportXml.php b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportXml.php new file mode 100644 index 0000000..db85add --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportXml.php @@ -0,0 +1,593 @@ +setProperties(); + } + + /** + * Initialize the local variables that are used for export XML + * + * @return void + */ + protected function initSpecificVariables() + { + global $table, $tables; + $this->_setTable($table); + if (is_array($tables)) { + $this->_setTables($tables); + } + } + + /** + * Sets the export XML properties + * + * @return void + */ + protected function setProperties() + { + // create the export plugin property item + $exportPluginProperties = new ExportPluginProperties(); + $exportPluginProperties->setText('XML'); + $exportPluginProperties->setExtension('xml'); + $exportPluginProperties->setMimeType('text/xml'); + $exportPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $exportPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new OptionsPropertyMainGroup("general_opts"); + // create primary items and add them to the group + $leaf = new HiddenPropertyItem("structure_or_data"); + $generalOptions->addProperty($leaf); + // add the main group to the root group + $exportSpecificOptions->addProperty($generalOptions); + + // export structure main group + $structure = new OptionsPropertyMainGroup( + "structure", + __('Object creation options (all are recommended)') + ); + + // create primary items and add them to the group + $leaf = new BoolPropertyItem( + "export_events", + __('Events') + ); + $structure->addProperty($leaf); + $leaf = new BoolPropertyItem( + "export_functions", + __('Functions') + ); + $structure->addProperty($leaf); + $leaf = new BoolPropertyItem( + "export_procedures", + __('Procedures') + ); + $structure->addProperty($leaf); + $leaf = new BoolPropertyItem( + "export_tables", + __('Tables') + ); + $structure->addProperty($leaf); + $leaf = new BoolPropertyItem( + "export_triggers", + __('Triggers') + ); + $structure->addProperty($leaf); + $leaf = new BoolPropertyItem( + "export_views", + __('Views') + ); + $structure->addProperty($leaf); + $exportSpecificOptions->addProperty($structure); + + // data main group + $data = new OptionsPropertyMainGroup( + "data", + __('Data dump options') + ); + // create primary items and add them to the group + $leaf = new BoolPropertyItem( + "export_contents", + __('Export contents') + ); + $data->addProperty($leaf); + $exportSpecificOptions->addProperty($data); + + // set the options for the export plugin property item + $exportPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $exportPluginProperties; + } + + /** + * Generates output for SQL defintions of routines + * + * @param string $db Database name + * @param string $type Item type to be used in XML output + * @param string $dbitype Item type used in DBI qieries + * + * @return string XML with definitions + */ + private function _exportRoutines($db, $type, $dbitype) + { + // Export routines + $routines = $GLOBALS['dbi']->getProceduresOrFunctions( + $db, + $dbitype + ); + return $this->_exportDefinitions($db, $type, $dbitype, $routines); + } + + /** + * Generates output for SQL defintions + * + * @param string $db Database name + * @param string $type Item type to be used in XML output + * @param string $dbitype Item type used in DBI qieries + * @param array $names Names of items to export + * + * @return string XML with definitions + */ + private function _exportDefinitions($db, $type, $dbitype, array $names) + { + global $crlf; + + $head = ''; + + if ($names) { + foreach ($names as $name) { + $head .= ' ' . $crlf; + + // Do some formatting + $sql = $GLOBALS['dbi']->getDefinition($db, $dbitype, $name); + $sql = htmlspecialchars(rtrim($sql)); + $sql = str_replace("\n", "\n ", $sql); + + $head .= " " . $sql . $crlf; + $head .= ' ' . $crlf; + } + } + + return $head; + } + + /** + * Outputs export header. It is the first method to be called, so all + * the required variables are initialized here. + * + * @return bool Whether it succeeded + */ + public function exportHeader() + { + $this->initSpecificVariables(); + global $crlf, $cfg, $db; + $table = $this->_getTable(); + $tables = $this->_getTables(); + + $export_struct = isset($GLOBALS['xml_export_functions']) + || isset($GLOBALS['xml_export_procedures']) + || isset($GLOBALS['xml_export_tables']) + || isset($GLOBALS['xml_export_triggers']) + || isset($GLOBALS['xml_export_views']); + $export_data = isset($GLOBALS['xml_export_contents']) ? true : false; + + if ($GLOBALS['output_charset_conversion']) { + $charset = $GLOBALS['charset']; + } else { + $charset = 'utf-8'; + } + + $head = '' . $crlf + . '' . $crlf . $crlf; + + $head .= '' . $crlf; + + if ($export_struct) { + $result = $GLOBALS['dbi']->fetchResult( + 'SELECT `DEFAULT_CHARACTER_SET_NAME`, `DEFAULT_COLLATION_NAME`' + . ' FROM `information_schema`.`SCHEMATA` WHERE `SCHEMA_NAME`' + . ' = \'' . $GLOBALS['dbi']->escapeString($db) . '\' LIMIT 1' + ); + $db_collation = $result[0]['DEFAULT_COLLATION_NAME']; + $db_charset = $result[0]['DEFAULT_CHARACTER_SET_NAME']; + + $head .= ' ' . $crlf; + $head .= ' ' . $crlf; + $head .= ' ' . $crlf; + + if ($tables === null) { + $tables = []; + } + + if (count($tables) === 0) { + $tables[] = $table; + } + + foreach ($tables as $table) { + // Export tables and views + $result = $GLOBALS['dbi']->fetchResult( + 'SHOW CREATE TABLE ' . Util::backquote($db) . '.' + . Util::backquote($table), + 0 + ); + $tbl = $result[$table][1]; + + $is_view = $GLOBALS['dbi']->getTable($db, $table) + ->isView(); + + if ($is_view) { + $type = 'view'; + } else { + $type = 'table'; + } + + if ($is_view && ! isset($GLOBALS['xml_export_views'])) { + continue; + } + + if (! $is_view && ! isset($GLOBALS['xml_export_tables'])) { + continue; + } + + $head .= ' ' + . $crlf; + + $tbl = " " . htmlspecialchars($tbl); + $tbl = str_replace("\n", "\n ", $tbl); + + $head .= $tbl . ';' . $crlf; + $head .= ' ' . $crlf; + + if (isset($GLOBALS['xml_export_triggers']) + && $GLOBALS['xml_export_triggers'] + ) { + // Export triggers + $triggers = $GLOBALS['dbi']->getTriggers($db, $table); + if ($triggers) { + foreach ($triggers as $trigger) { + $code = $trigger['create']; + $head .= ' ' . $crlf; + + // Do some formatting + $code = mb_substr(rtrim($code), 0, -3); + $code = " " . htmlspecialchars($code); + $code = str_replace("\n", "\n ", $code); + + $head .= $code . $crlf; + $head .= ' ' . $crlf; + } + + unset($trigger); + unset($triggers); + } + } + } + + if (isset($GLOBALS['xml_export_functions']) + && $GLOBALS['xml_export_functions'] + ) { + $head .= $this->_exportRoutines($db, 'function', 'FUNCTION'); + } + + if (isset($GLOBALS['xml_export_procedures']) + && $GLOBALS['xml_export_procedures'] + ) { + $head .= $this->_exportRoutines($db, 'procedure', 'PROCEDURE'); + } + + if (isset($GLOBALS['xml_export_events']) + && $GLOBALS['xml_export_events'] + ) { + // Export events + $events = $GLOBALS['dbi']->fetchResult( + "SELECT EVENT_NAME FROM information_schema.EVENTS " + . "WHERE EVENT_SCHEMA='" . $GLOBALS['dbi']->escapeString($db) + . "'" + ); + $head .= $this->_exportDefinitions( + $db, + 'event', + 'EVENT', + $events + ); + } + + unset($result); + + $head .= ' ' . $crlf; + $head .= ' ' . $crlf; + + if ($export_data) { + $head .= $crlf; + } + } + + return $this->export->outputHandler($head); + } + + /** + * Outputs export footer + * + * @return bool Whether it succeeded + */ + public function exportFooter() + { + $foot = ''; + + return $this->export->outputHandler($foot); + } + + /** + * Outputs database header + * + * @param string $db Database name + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBHeader($db, $db_alias = '') + { + global $crlf; + + if (empty($db_alias)) { + $db_alias = $db; + } + if (isset($GLOBALS['xml_export_contents']) + && $GLOBALS['xml_export_contents'] + ) { + $head = ' ' . $crlf . ' ' . $crlf; + + return $this->export->outputHandler($head); + } + + return true; + } + + /** + * Outputs database footer + * + * @param string $db Database name + * + * @return bool Whether it succeeded + */ + public function exportDBFooter($db) + { + global $crlf; + + if (isset($GLOBALS['xml_export_contents']) + && $GLOBALS['xml_export_contents'] + ) { + return $this->export->outputHandler(' ' . $crlf); + } + + return true; + } + + /** + * Outputs CREATE DATABASE statement + * + * @param string $db Database name + * @param string $export_type 'server', 'database', 'table' + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBCreate($db, $export_type, $db_alias = '') + { + return true; + } + + /** + * Outputs the content of a table in XML format + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $sql_query SQL query for obtaining data + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportData( + $db, + $table, + $crlf, + $error_url, + $sql_query, + array $aliases = [] + ) { + // Do not export data for merge tables + if ($GLOBALS['dbi']->getTable($db, $table)->isMerge()) { + return true; + } + + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + if (isset($GLOBALS['xml_export_contents']) + && $GLOBALS['xml_export_contents'] + ) { + $result = $GLOBALS['dbi']->query( + $sql_query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_UNBUFFERED + ); + + $columns_cnt = $GLOBALS['dbi']->numFields($result); + $columns = []; + for ($i = 0; $i < $columns_cnt; $i++) { + $columns[$i] = stripslashes($GLOBALS['dbi']->fieldName($result, $i)); + } + unset($i); + + $buffer = ' ' . $crlf; + if (! $this->export->outputHandler($buffer)) { + return false; + } + + while ($record = $GLOBALS['dbi']->fetchRow($result)) { + $buffer = ' ' . $crlf; + for ($i = 0; $i < $columns_cnt; $i++) { + $col_as = $columns[$i]; + if (! empty($aliases[$db]['tables'][$table]['columns'][$col_as]) + ) { + $col_as + = $aliases[$db]['tables'][$table]['columns'][$col_as]; + } + // If a cell is NULL, still export it to preserve + // the XML structure + if (! isset($record[$i]) || $record[$i] === null) { + $record[$i] = 'NULL'; + } + $buffer .= ' ' + . htmlspecialchars((string) $record[$i]) + . '' . $crlf; + } + $buffer .= '
    ' . $crlf; + + if (! $this->export->outputHandler($buffer)) { + return false; + } + } + $GLOBALS['dbi']->freeResult($result); + } + + return true; + } + + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the table name + * + * @return string + */ + private function _getTable() + { + return $this->_table; + } + + /** + * Sets the table name + * + * @param string $table table name + * + * @return void + */ + private function _setTable($table) + { + $this->_table = $table; + } + + /** + * Gets the table names + * + * @return array + */ + private function _getTables() + { + return $this->_tables; + } + + /** + * Sets the table names + * + * @param array $tables table names + * + * @return void + */ + private function _setTables(array $tables) + { + $this->_tables = $tables; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportYaml.php b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportYaml.php new file mode 100644 index 0000000..971f9c6 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Export/ExportYaml.php @@ -0,0 +1,230 @@ +setProperties(); + } + + /** + * Sets the export YAML properties + * + * @return void + */ + protected function setProperties() + { + $exportPluginProperties = new ExportPluginProperties(); + $exportPluginProperties->setText('YAML'); + $exportPluginProperties->setExtension('yml'); + $exportPluginProperties->setMimeType('text/yaml'); + $exportPluginProperties->setForceFile(true); + $exportPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $exportPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new OptionsPropertyMainGroup("general_opts"); + // create primary items and add them to the group + $leaf = new HiddenPropertyItem("structure_or_data"); + $generalOptions->addProperty($leaf); + // add the main group to the root group + $exportSpecificOptions->addProperty($generalOptions); + + // set the options for the export plugin property item + $exportPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $exportPluginProperties; + } + + /** + * Outputs export header + * + * @return bool Whether it succeeded + */ + public function exportHeader() + { + $this->export->outputHandler( + '%YAML 1.1' . $GLOBALS['crlf'] . '---' . $GLOBALS['crlf'] + ); + + return true; + } + + /** + * Outputs export footer + * + * @return bool Whether it succeeded + */ + public function exportFooter() + { + $this->export->outputHandler('...' . $GLOBALS['crlf']); + + return true; + } + + /** + * Outputs database header + * + * @param string $db Database name + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBHeader($db, $db_alias = '') + { + return true; + } + + /** + * Outputs database footer + * + * @param string $db Database name + * + * @return bool Whether it succeeded + */ + public function exportDBFooter($db) + { + return true; + } + + /** + * Outputs CREATE DATABASE statement + * + * @param string $db Database name + * @param string $export_type 'server', 'database', 'table' + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBCreate($db, $export_type, $db_alias = '') + { + return true; + } + + /** + * Outputs the content of a table in JSON format + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $sql_query SQL query for obtaining data + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportData( + $db, + $table, + $crlf, + $error_url, + $sql_query, + array $aliases = [] + ) { + $db_alias = $db; + $table_alias = $table; + $this->initAlias($aliases, $db_alias, $table_alias); + $result = $GLOBALS['dbi']->query( + $sql_query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_UNBUFFERED + ); + + $columns_cnt = $GLOBALS['dbi']->numFields($result); + $columns = []; + for ($i = 0; $i < $columns_cnt; $i++) { + $col_as = $GLOBALS['dbi']->fieldName($result, $i); + if (! empty($aliases[$db]['tables'][$table]['columns'][$col_as])) { + $col_as = $aliases[$db]['tables'][$table]['columns'][$col_as]; + } + $columns[$i] = stripslashes($col_as); + } + + $buffer = ''; + $record_cnt = 0; + while ($record = $GLOBALS['dbi']->fetchRow($result)) { + $record_cnt++; + + // Output table name as comment if this is the first record of the table + if ($record_cnt == 1) { + $buffer = '# ' . $db_alias . '.' . $table_alias . $crlf; + $buffer .= '-' . $crlf; + } else { + $buffer = '-' . $crlf; + } + + for ($i = 0; $i < $columns_cnt; $i++) { + if (! isset($record[$i])) { + continue; + } + + if ($record[$i] === null) { + $buffer .= ' ' . $columns[$i] . ': null' . $crlf; + continue; + } + + if (is_numeric($record[$i])) { + $buffer .= ' ' . $columns[$i] . ': ' . $record[$i] . $crlf; + continue; + } + + $record[$i] = str_replace( + [ + '\\', + '"', + "\n", + "\r", + ], + [ + '\\\\', + '\"', + '\n', + '\r', + ], + $record[$i] + ); + $buffer .= ' ' . $columns[$i] . ': "' . $record[$i] . '"' . $crlf; + } + + if (! $this->export->outputHandler($buffer)) { + return false; + } + } + $GLOBALS['dbi']->freeResult($result); + + return true; + } // end getTableYAML +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Export/Helpers/Pdf.php b/srcs/phpmyadmin/libraries/classes/Plugins/Export/Helpers/Pdf.php new file mode 100644 index 0000000..58f1b5a --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Export/Helpers/Pdf.php @@ -0,0 +1,855 @@ +relation = new Relation($GLOBALS['dbi']); + $this->transformations = new Transformations(); + } + + /** + * Add page if needed. + * + * @param float|int $h cell height. Default value: 0 + * @param mixed $y starting y position, leave empty for current + * position + * @param boolean $addpage if true add a page, otherwise only return + * the true/false state + * + * @return boolean true in case of page break, false otherwise. + */ + public function checkPageBreak($h = 0, $y = '', $addpage = true) + { + if (TCPDF_STATIC::empty_string($y)) { + $y = $this->y; + } + $current_page = $this->page; + if ((($y + $h) > $this->PageBreakTrigger) + && (! $this->InFooter) + && $this->AcceptPageBreak() + ) { + if ($addpage) { + //Automatic page break + $x = $this->x; + $this->AddPage($this->CurOrientation); + $this->y = $this->dataY; + $oldpage = $this->page - 1; + + $this_page_orm = $this->pagedim[$this->page]['orm']; + $old_page_orm = $this->pagedim[$oldpage]['orm']; + $this_page_olm = $this->pagedim[$this->page]['olm']; + $old_page_olm = $this->pagedim[$oldpage]['olm']; + if ($this->rtl) { + if ($this_page_orm != $old_page_orm) { + $this->x = $x - ($this_page_orm - $old_page_orm); + } else { + $this->x = $x; + } + } else { + if ($this_page_olm != $old_page_olm) { + $this->x = $x + ($this_page_olm - $old_page_olm); + } else { + $this->x = $x; + } + } + } + + return true; + } + + // account for columns mode + return $current_page != $this->page; + } + + /** + * This method is used to render the page header. + * + * @return void + */ + // @codingStandardsIgnoreLine + public function Header() + { + global $maxY; + // We don't want automatic page breaks while generating header + // as this can lead to infinite recursion as auto generated page + // will want header as well causing another page break + // FIXME: Better approach might be to try to compact the content + $this->SetAutoPageBreak(false); + // Check if header for this page already exists + if (! isset($this->headerset[$this->page])) { + $this->SetY($this->tMargin - ($this->FontSizePt / $this->k) * 5); + $this->cellFontSize = $this->FontSizePt; + $this->SetFont( + PdfLib::PMA_PDF_FONT, + '', + ($this->titleFontSize + ?: $this->FontSizePt) + ); + $this->Cell(0, $this->FontSizePt, $this->titleText, 0, 1, 'C'); + $this->SetFont(PdfLib::PMA_PDF_FONT, '', $this->cellFontSize); + $this->SetY($this->tMargin - ($this->FontSizePt / $this->k) * 2.5); + $this->Cell( + 0, + $this->FontSizePt, + __('Database:') . ' ' . $this->dbAlias . ', ' + . __('Table:') . ' ' . $this->tableAlias . ', ' + . __('Purpose:') . ' ' . $this->purpose, + 0, + 1, + 'L' + ); + $l = $this->lMargin; + foreach ($this->colTitles as $col => $txt) { + $this->SetXY($l, $this->tMargin); + $this->MultiCell( + $this->tablewidths[$col], + $this->FontSizePt, + $txt + ); + $l += $this->tablewidths[$col]; + $maxY = $maxY < $this->GetY() ? $this->GetY() : $maxY; + } + $this->SetXY($this->lMargin, $this->tMargin); + $this->SetFillColor(200, 200, 200); + $l = $this->lMargin; + foreach ($this->colTitles as $col => $txt) { + $this->SetXY($l, $this->tMargin); + $this->Cell( + $this->tablewidths[$col], + $maxY - $this->tMargin, + '', + 1, + 0, + 'L', + 1 + ); + $this->SetXY($l, $this->tMargin); + $this->MultiCell( + $this->tablewidths[$col], + $this->FontSizePt, + $txt, + 0, + 'C' + ); + $l += $this->tablewidths[$col]; + } + $this->SetFillColor(255, 255, 255); + // set headerset + $this->headerset[$this->page] = 1; + } + + $this->dataY = $maxY; + $this->SetAutoPageBreak(true); + } + + /** + * Generate table + * + * @param int $lineheight Height of line + * + * @return void + */ + public function morepagestable($lineheight = 8) + { + // some things to set and 'remember' + $l = $this->lMargin; + $startheight = $h = $this->dataY; + $startpage = $currpage = $this->page; + + // calculate the whole width + $fullwidth = 0; + foreach ($this->tablewidths as $width) { + $fullwidth += $width; + } + + // Now let's start to write the table + $row = 0; + $tmpheight = []; + $maxpage = $this->page; + + while ($data = $GLOBALS['dbi']->fetchRow($this->results)) { + $this->page = $currpage; + // write the horizontal borders + $this->Line($l, $h, $fullwidth + $l, $h); + // write the content and remember the height of the highest col + foreach ($data as $col => $txt) { + $this->page = $currpage; + $this->SetXY($l, $h); + if ($this->tablewidths[$col] > 0) { + $this->MultiCell( + $this->tablewidths[$col], + $lineheight, + $txt, + 0, + $this->colAlign[$col] + ); + $l += $this->tablewidths[$col]; + } + + if (! isset($tmpheight[$row . '-' . $this->page])) { + $tmpheight[$row . '-' . $this->page] = 0; + } + if ($tmpheight[$row . '-' . $this->page] < $this->GetY()) { + $tmpheight[$row . '-' . $this->page] = $this->GetY(); + } + if ($this->page > $maxpage) { + $maxpage = $this->page; + } + unset($data[$col]); + } + + // get the height we were in the last used page + $h = $tmpheight[$row . '-' . $maxpage]; + // set the "pointer" to the left margin + $l = $this->lMargin; + // set the $currpage to the last page + $currpage = $maxpage; + unset($data[$row]); + $row++; + } + // draw the borders + // we start adding a horizontal line on the last page + $this->page = $maxpage; + $this->Line($l, $h, $fullwidth + $l, $h); + // now we start at the top of the document and walk down + for ($i = $startpage; $i <= $maxpage; $i++) { + $this->page = $i; + $l = $this->lMargin; + $t = $i == $startpage ? $startheight : $this->tMargin; + $lh = $i == $maxpage ? $h : $this->h - $this->bMargin; + $this->Line($l, $t, $l, $lh); + foreach ($this->tablewidths as $width) { + $l += $width; + $this->Line($l, $t, $l, $lh); + } + } + // set it to the last page, if not it'll cause some problems + $this->page = $maxpage; + } + + /** + * Sets a set of attributes. + * + * @param array $attr array containing the attributes + * + * @return void + */ + public function setAttributes(array $attr = []) + { + foreach ($attr as $key => $val) { + $this->$key = $val; + } + } + + /** + * Defines the top margin. + * The method can be called before creating the first page. + * + * @param float $topMargin the margin + * + * @return void + */ + public function setTopMargin($topMargin) + { + $this->tMargin = $topMargin; + } + + /** + * Prints triggers + * + * @param string $db database name + * @param string $table table name + * + * @return void + */ + public function getTriggers($db, $table) + { + $triggers = $GLOBALS['dbi']->getTriggers($db, $table); + if ([] === $triggers) { + return; //prevents printing blank trigger list for any table + } + + unset($this->tablewidths); + unset($this->colTitles); + unset($this->titleWidth); + unset($this->colFits); + unset($this->display_column); + unset($this->colAlign); + + /** + * Making table heading + * Keeping column width constant + */ + $this->colTitles[0] = __('Name'); + $this->tablewidths[0] = 90; + $this->colTitles[1] = __('Time'); + $this->tablewidths[1] = 80; + $this->colTitles[2] = __('Event'); + $this->tablewidths[2] = 40; + $this->colTitles[3] = __('Definition'); + $this->tablewidths[3] = 240; + + for ($columns_cnt = 0; $columns_cnt < 4; $columns_cnt++) { + $this->colAlign[$columns_cnt] = 'L'; + $this->display_column[$columns_cnt] = true; + } + + // Starting to fill table with required info + + $this->SetY($this->tMargin); + $this->AddPage(); + $this->SetFont(PdfLib::PMA_PDF_FONT, '', 9); + + $l = $this->lMargin; + $startheight = $h = $this->dataY; + $startpage = $currpage = $this->page; + + // calculate the whole width + $fullwidth = 0; + foreach ($this->tablewidths as $width) { + $fullwidth += $width; + } + + $row = 0; + $tmpheight = []; + $maxpage = $this->page; + $data = []; + + foreach ($triggers as $trigger) { + $data[] = $trigger['name']; + $data[] = $trigger['action_timing']; + $data[] = $trigger['event_manipulation']; + $data[] = $trigger['definition']; + $this->page = $currpage; + // write the horizontal borders + $this->Line($l, $h, $fullwidth + $l, $h); + // write the content and remember the height of the highest col + foreach ($data as $col => $txt) { + $this->page = $currpage; + $this->SetXY($l, $h); + if ($this->tablewidths[$col] > 0) { + $this->MultiCell( + $this->tablewidths[$col], + $this->FontSizePt, + $txt, + 0, + $this->colAlign[$col] + ); + $l += $this->tablewidths[$col]; + } + + if (! isset($tmpheight[$row . '-' . $this->page])) { + $tmpheight[$row . '-' . $this->page] = 0; + } + if ($tmpheight[$row . '-' . $this->page] < $this->GetY()) { + $tmpheight[$row . '-' . $this->page] = $this->GetY(); + } + if ($this->page > $maxpage) { + $maxpage = $this->page; + } + } + // get the height we were in the last used page + $h = $tmpheight[$row . '-' . $maxpage]; + // set the "pointer" to the left margin + $l = $this->lMargin; + // set the $currpage to the last page + $currpage = $maxpage; + unset($data); + $row++; + } + // draw the borders + // we start adding a horizontal line on the last page + $this->page = $maxpage; + $this->Line($l, $h, $fullwidth + $l, $h); + // now we start at the top of the document and walk down + for ($i = $startpage; $i <= $maxpage; $i++) { + $this->page = $i; + $l = $this->lMargin; + $t = $i == $startpage ? $startheight : $this->tMargin; + $lh = $i == $maxpage ? $h : $this->h - $this->bMargin; + $this->Line($l, $t, $l, $lh); + foreach ($this->tablewidths as $width) { + $l += $width; + $this->Line($l, $t, $l, $lh); + } + } + // set it to the last page, if not it'll cause some problems + $this->page = $maxpage; + } + + /** + * Print $table's CREATE definition + * + * @param string $db the database name + * @param string $table the table name + * @param bool $do_relation whether to include relation comments + * @param bool $do_comments whether to include the pmadb-style column + * comments as comments in the structure; + * this is deprecated but the parameter is + * left here because export.php calls + * PMA_exportStructure() also for other + * export types which use this parameter + * @param bool $do_mime whether to include mime comments + * @param bool $view whether we're handling a view + * @param array $aliases aliases of db/table/columns + * + * @return void + */ + public function getTableDef( + $db, + $table, + $do_relation, + $do_comments, + $do_mime, + $view = false, + array $aliases = [] + ) { + // set $cfgRelation here, because there is a chance that it's modified + // since the class initialization + global $cfgRelation; + + unset($this->tablewidths); + unset($this->colTitles); + unset($this->titleWidth); + unset($this->colFits); + unset($this->display_column); + unset($this->colAlign); + + /** + * Gets fields properties + */ + $GLOBALS['dbi']->selectDb($db); + + /** + * All these three checks do_relation, do_comment and do_mime is + * not required. As presently all are set true by default. + * But when, methods to take user input will be developed, + * it will be of use + */ + // Check if we can use Relations + if ($do_relation) { + // Find which tables are related with the current one and write it in + // an array + $res_rel = $this->relation->getForeigners($db, $table); + $have_rel = ! empty($res_rel); + } else { + $have_rel = false; + } // end if + + //column count and table heading + + $this->colTitles[0] = __('Column'); + $this->tablewidths[0] = 90; + $this->colTitles[1] = __('Type'); + $this->tablewidths[1] = 80; + $this->colTitles[2] = __('Null'); + $this->tablewidths[2] = 40; + $this->colTitles[3] = __('Default'); + $this->tablewidths[3] = 120; + + for ($columns_cnt = 0; $columns_cnt < 4; $columns_cnt++) { + $this->colAlign[$columns_cnt] = 'L'; + $this->display_column[$columns_cnt] = true; + } + + if ($do_relation && $have_rel) { + $this->colTitles[$columns_cnt] = __('Links to'); + $this->display_column[$columns_cnt] = true; + $this->colAlign[$columns_cnt] = 'L'; + $this->tablewidths[$columns_cnt] = 120; + $columns_cnt++; + } + if ($do_comments /*&& $cfgRelation['commwork']*/) { + $this->colTitles[$columns_cnt] = __('Comments'); + $this->display_column[$columns_cnt] = true; + $this->colAlign[$columns_cnt] = 'L'; + $this->tablewidths[$columns_cnt] = 120; + $columns_cnt++; + } + if ($do_mime && $cfgRelation['mimework']) { + $this->colTitles[$columns_cnt] = __('Media (MIME) type'); + $this->display_column[$columns_cnt] = true; + $this->colAlign[$columns_cnt] = 'L'; + $this->tablewidths[$columns_cnt] = 120; + $columns_cnt++; + } + + // Starting to fill table with required info + + $this->SetY($this->tMargin); + $this->AddPage(); + $this->SetFont(PdfLib::PMA_PDF_FONT, '', 9); + + // Now let's start to write the table structure + + if ($do_comments) { + $comments = $this->relation->getComments($db, $table); + } + if ($do_mime && $cfgRelation['mimework']) { + $mime_map = $this->transformations->getMime($db, $table, true); + } + + $columns = $GLOBALS['dbi']->getColumns($db, $table); + + // some things to set and 'remember' + $l = $this->lMargin; + $startheight = $h = $this->dataY; + $startpage = $currpage = $this->page; + // calculate the whole width + $fullwidth = 0; + foreach ($this->tablewidths as $width) { + $fullwidth += $width; + } + + $row = 0; + $tmpheight = []; + $maxpage = $this->page; + $data = []; + + // fun begin + foreach ($columns as $column) { + $extracted_columnspec + = Util::extractColumnSpec($column['Type']); + + $type = $extracted_columnspec['print_type']; + if (empty($type)) { + $type = ' '; + } + + if (! isset($column['Default'])) { + if ($column['Null'] != 'NO') { + $column['Default'] = 'NULL'; + } + } + $data[] = $column['Field']; + $data[] = $type; + $data[] = $column['Null'] == '' || $column['Null'] == 'NO' + ? 'No' + : 'Yes'; + $data[] = isset($column['Default']) ? $column['Default'] : ''; + + $field_name = $column['Field']; + + if ($do_relation && $have_rel) { + $data[] = isset($res_rel[$field_name]) + ? $res_rel[$field_name]['foreign_table'] + . ' (' . $res_rel[$field_name]['foreign_field'] + . ')' + : ''; + } + if ($do_comments) { + $data[] = isset($comments[$field_name]) + ? $comments[$field_name] + : ''; + } + if ($do_mime) { + $data[] = isset($mime_map[$field_name]) + ? $mime_map[$field_name]['mimetype'] + : ''; + } + + $this->page = $currpage; + // write the horizontal borders + $this->Line($l, $h, $fullwidth + $l, $h); + // write the content and remember the height of the highest col + foreach ($data as $col => $txt) { + $this->page = $currpage; + $this->SetXY($l, $h); + if ($this->tablewidths[$col] > 0) { + $this->MultiCell( + $this->tablewidths[$col], + $this->FontSizePt, + $txt, + 0, + $this->colAlign[$col] + ); + $l += $this->tablewidths[$col]; + } + + if (! isset($tmpheight[$row . '-' . $this->page])) { + $tmpheight[$row . '-' . $this->page] = 0; + } + if ($tmpheight[$row . '-' . $this->page] < $this->GetY()) { + $tmpheight[$row . '-' . $this->page] = $this->GetY(); + } + if ($this->page > $maxpage) { + $maxpage = $this->page; + } + } + + // get the height we were in the last used page + $h = $tmpheight[$row . '-' . $maxpage]; + // set the "pointer" to the left margin + $l = $this->lMargin; + // set the $currpage to the last page + $currpage = $maxpage; + unset($data); + $row++; + } + // draw the borders + // we start adding a horizontal line on the last page + $this->page = $maxpage; + $this->Line($l, $h, $fullwidth + $l, $h); + // now we start at the top of the document and walk down + for ($i = $startpage; $i <= $maxpage; $i++) { + $this->page = $i; + $l = $this->lMargin; + $t = $i == $startpage ? $startheight : $this->tMargin; + $lh = $i == $maxpage ? $h : $this->h - $this->bMargin; + $this->Line($l, $t, $l, $lh); + foreach ($this->tablewidths as $width) { + $l += $width; + $this->Line($l, $t, $l, $lh); + } + } + // set it to the last page, if not it'll cause some problems + $this->page = $maxpage; + } + + /** + * MySQL report + * + * @param string $query Query to execute + * + * @return void + */ + public function mysqlReport($query) + { + unset($this->tablewidths); + unset($this->colTitles); + unset($this->titleWidth); + unset($this->colFits); + unset($this->display_column); + unset($this->colAlign); + + /** + * Pass 1 for column widths + */ + $this->results = $GLOBALS['dbi']->query( + $query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_UNBUFFERED + ); + $this->numFields = $GLOBALS['dbi']->numFields($this->results); + $this->fields = $GLOBALS['dbi']->getFieldsMeta($this->results); + + // sColWidth = starting col width (an average size width) + $availableWidth = $this->w - $this->lMargin - $this->rMargin; + $this->sColWidth = $availableWidth / $this->numFields; + $totalTitleWidth = 0; + + // loop through results header and set initial + // col widths/ titles/ alignment + // if a col title is less than the starting col width, + // reduce that column size + $colFits = []; + $titleWidth = []; + for ($i = 0; $i < $this->numFields; $i++) { + $col_as = $this->fields[$i]->name; + $db = $this->currentDb; + $table = $this->currentTable; + if (! empty($this->aliases[$db]['tables'][$table]['columns'][$col_as])) { + $col_as = $this->aliases[$db]['tables'][$table]['columns'][$col_as]; + } + $stringWidth = $this->GetStringWidth($col_as) + 6; + // save the real title's width + $titleWidth[$i] = $stringWidth; + $totalTitleWidth += $stringWidth; + + // set any column titles less than the start width to + // the column title width + if ($stringWidth < $this->sColWidth) { + $colFits[$i] = $stringWidth; + } + $this->colTitles[$i] = $col_as; + $this->display_column[$i] = true; + + switch ($this->fields[$i]->type) { + case 'int': + $this->colAlign[$i] = 'R'; + break; + case 'blob': + case 'tinyblob': + case 'mediumblob': + case 'longblob': + /** + * @todo do not deactivate completely the display + * but show the field's name and [BLOB] + */ + if (false !== stripos($this->fields[$i]->flags, 'BINARY')) { + $this->display_column[$i] = false; + unset($this->colTitles[$i]); + } + $this->colAlign[$i] = 'L'; + break; + default: + $this->colAlign[$i] = 'L'; + } + } + + // title width verification + if ($totalTitleWidth > $availableWidth) { + $adjustingMode = true; + } else { + $adjustingMode = false; + // we have enough space for all the titles at their + // original width so use the true title's width + foreach ($titleWidth as $key => $val) { + $colFits[$key] = $val; + } + } + + // loop through the data; any column whose contents + // is greater than the column size is resized + /** + * @todo force here a LIMIT to avoid reading all rows + */ + while ($row = $GLOBALS['dbi']->fetchRow($this->results)) { + foreach ($colFits as $key => $val) { + $stringWidth = $this->GetStringWidth($row[$key]) + 6; + if ($adjustingMode && ($stringWidth > $this->sColWidth)) { + // any column whose data's width is bigger than + // the start width is now discarded + unset($colFits[$key]); + } else { + // if data's width is bigger than the current column width, + // enlarge the column (but avoid enlarging it if the + // data's width is very big) + if ($stringWidth > $val + && $stringWidth < ($this->sColWidth * 3) + ) { + $colFits[$key] = $stringWidth; + } + } + } + } + + $totAlreadyFitted = 0; + foreach ($colFits as $key => $val) { + // set fitted columns to smallest size + $this->tablewidths[$key] = $val; + // to work out how much (if any) space has been freed up + $totAlreadyFitted += $val; + } + + if ($adjustingMode) { + $surplus = (count($colFits) * $this->sColWidth) - $totAlreadyFitted; + $surplusToAdd = $surplus / ($this->numFields - count($colFits)); + } else { + $surplusToAdd = 0; + } + + for ($i = 0; $i < $this->numFields; $i++) { + if (! array_key_exists($i, $colFits)) { + $this->tablewidths[$i] = $this->sColWidth + $surplusToAdd; + } + if ($this->display_column[$i] == false) { + $this->tablewidths[$i] = 0; + } + } + + ksort($this->tablewidths); + + $GLOBALS['dbi']->freeResult($this->results); + + // Pass 2 + + $this->results = $GLOBALS['dbi']->query( + $query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_UNBUFFERED + ); + $this->SetY($this->tMargin); + $this->AddPage(); + $this->SetFont(PdfLib::PMA_PDF_FONT, '', 9); + $this->morepagestable($this->FontSizePt); + $GLOBALS['dbi']->freeResult($this->results); + } // end of mysqlReport function +} // end of Pdf class diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Export/Helpers/TableProperty.php b/srcs/phpmyadmin/libraries/classes/Plugins/Export/Helpers/TableProperty.php new file mode 100644 index 0000000..8991251 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Export/Helpers/TableProperty.php @@ -0,0 +1,277 @@ +name = trim((string) $row[0]); + $this->type = trim((string) $row[1]); + $this->nullable = trim((string) $row[2]); + $this->key = trim((string) $row[3]); + $this->defaultValue = trim((string) $row[4]); + $this->ext = trim((string) $row[5]); + } + + /** + * Gets the pure type + * + * @return string type + */ + public function getPureType() + { + $pos = mb_strpos($this->type, "("); + if ($pos > 0) { + return mb_substr($this->type, 0, $pos); + } + return $this->type; + } + + /** + * Tells whether the key is null or not + * + * @return string true if the key is not null, false otherwise + */ + public function isNotNull() + { + return $this->nullable === "NO" ? "true" : "false"; + } + + /** + * Tells whether the key is unique or not + * + * @return string "true" if the key is unique, "false" otherwise + */ + public function isUnique(): string + { + return ($this->key === "PRI" || $this->key === "UNI") ? "true" : "false"; + } + + /** + * Gets the .NET primitive type + * + * @return string type + */ + public function getDotNetPrimitiveType() + { + if (mb_strpos($this->type, "int") === 0) { + return "int"; + } + if (mb_strpos($this->type, "longtext") === 0) { + return "string"; + } + if (mb_strpos($this->type, "long") === 0) { + return "long"; + } + if (mb_strpos($this->type, "char") === 0) { + return "string"; + } + if (mb_strpos($this->type, "varchar") === 0) { + return "string"; + } + if (mb_strpos($this->type, "text") === 0) { + return "string"; + } + if (mb_strpos($this->type, "tinyint") === 0) { + return "bool"; + } + if (mb_strpos($this->type, "datetime") === 0) { + return "DateTime"; + } + return "unknown"; + } + + /** + * Gets the .NET object type + * + * @return string type + */ + public function getDotNetObjectType() + { + if (mb_strpos($this->type, "int") === 0) { + return "Int32"; + } + if (mb_strpos($this->type, "longtext") === 0) { + return "String"; + } + if (mb_strpos($this->type, "long") === 0) { + return "Long"; + } + if (mb_strpos($this->type, "char") === 0) { + return "String"; + } + if (mb_strpos($this->type, "varchar") === 0) { + return "String"; + } + if (mb_strpos($this->type, "text") === 0) { + return "String"; + } + if (mb_strpos($this->type, "tinyint") === 0) { + return "Boolean"; + } + if (mb_strpos($this->type, "datetime") === 0) { + return "DateTime"; + } + return "Unknown"; + } + + /** + * Gets the index name + * + * @return string containing the name of the index + */ + public function getIndexName() + { + if (strlen($this->key) > 0) { + return "index=\"" + . htmlspecialchars($this->name, ENT_COMPAT, 'UTF-8') + . "\""; + } + return ""; + } + + /** + * Tells whether the key is primary or not + * + * @return bool true if the key is primary, false otherwise + */ + public function isPK(): bool + { + return $this->key === "PRI"; + } + + /** + * Formats a string for C# + * + * @param string $text string to be formatted + * + * @return string formatted text + */ + public function formatCs($text) + { + $text = str_replace( + '#name#', + ExportCodegen::cgMakeIdentifier($this->name, false), + $text + ); + return $this->format($text); + } + + /** + * Formats a string for XML + * + * @param string $text string to be formatted + * + * @return string formatted text + */ + public function formatXml($text) + { + $text = str_replace( + [ + '#name#', + '#indexName#', + ], + [ + htmlspecialchars($this->name, ENT_COMPAT, 'UTF-8'), + $this->getIndexName(), + ], + $text + ); + return $this->format($text); + } + + /** + * Formats a string + * + * @param string $text string to be formatted + * + * @return string formatted text + */ + public function format($text) + { + $text = str_replace( + [ + '#ucfirstName#', + '#dotNetPrimitiveType#', + '#dotNetObjectType#', + '#type#', + '#notNull#', + '#unique#', + ], + [ + ExportCodegen::cgMakeIdentifier($this->name), + $this->getDotNetPrimitiveType(), + $this->getDotNetObjectType(), + $this->getPureType(), + $this->isNotNull(), + $this->isUnique(), + ], + $text + ); + return $text; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Export/README b/srcs/phpmyadmin/libraries/classes/Plugins/Export/README new file mode 100644 index 0000000..5ada85b --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Export/README @@ -0,0 +1,255 @@ +This directory holds export plugins for phpMyAdmin. Any new plugin should +basically follow the structure presented here. Official plugins need to +have str* messages with their definition in language files, but if you build +some plugins for your use, you can directly use texts in plugin. + +setProperties(); + } + + // optional - declare global variables and use getters later + /** + * Initialize the local variables that are used specific for export SQL + * + * @global type $global_variable_name + * [..] + * + * @return void + */ + protected function initSpecificVariables() + { + global $global_variable_name; + $this->_setGlobalVariableName($global_variable_name); + } + + /** + * Sets the export plugin properties. + * Called in the constructor. + * + * @return void + */ + protected function setProperties() + { + $exportPluginProperties = new PhpMyAdmin\Properties\Plugins\ExportPluginProperties(); + $exportPluginProperties->setText('[name]'); // the name of your plug-in + $exportPluginProperties->setExtension('[ext]'); // extension this plug-in can handle + $exportPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $exportPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new PhpMyAdmin\Properties\Options\Groups\OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new PhpMyAdmin\Properties\Options\Groups\OptionsPropertyMainGroup( + "general_opts" + ); + + // optional : + // create primary items and add them to the group + // type - one of the classes listed in libraries/properties/options/items/ + // name - form element name + // text - description in GUI + // size - size of text element + // len - maximal size of input + // values - possible values of the item + $leaf = new PhpMyAdmin\Properties\Options\Items\RadioPropertyItem( + "structure_or_data" + ); + $leaf->setValues( + array( + 'structure' => __('structure'), + 'data' => __('data'), + 'structure_and_data' => __('structure and data') + ) + ); + $generalOptions->addProperty($leaf); + + // add the main group to the root group + $exportSpecificOptions->addProperty($generalOptions); + + // set the options for the export plugin property item + $exportPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $exportPluginProperties; + } + + /** + * Outputs export header + * + * @return bool Whether it succeeded + */ + public function exportHeader () + { + // implementation + return true; + } + + /** + * Outputs export footer + * + * @return bool Whether it succeeded + */ + public function exportFooter () + { + // implementation + return true; + } + + /** + * Outputs database header + * + * @param string $db Database name + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBHeader ($db, $db_alias = '') + { + // implementation + return true; + } + + /** + * Outputs database footer + * + * @param string $db Database name + * + * @return bool Whether it succeeded + */ + public function exportDBFooter ($db) + { + // implementation + return true; + } + + /** + * Outputs CREATE DATABASE statement + * + * @param string $db Database name + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + public function exportDBCreate($db, $db_alias = '') + { + // implementation + return true; + } + + /** + * Outputs the content of a table in [Name] format + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $sql_query SQL query for obtaining data + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportData( + $db, $table, $crlf, $error_url, $sql_query, $aliases = array() + ) { + // implementation; + return true; + } + + // optional - implement other methods defined in PhpMyAdmin\Plugins\ExportPlugin.class.php: + // - exportRoutines() + // - exportStructure() + // - getTableDefStandIn() + // - getTriggers() + + // optional - implement other private methods in order to avoid + // having huge methods or avoid duplicate code. Make use of them + // as well as of the getters and setters declared both here + // and in the PhpMyAdmin\Plugins\ExportPlugin class + + + // optional: + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + + /** + * Getter description + * + * @return type + */ + private function _getMyOptionalVariable() + { + return $this->_myOptionalVariable; + } + + /** + * Setter description + * + * @param type $my_optional_variable description + * + * @return void + */ + private function _setMyOptionalVariable($my_optional_variable) + { + $this->_myOptionalVariable = $my_optional_variable; + } + + /** + * Getter description + * + * @return type + */ + private function _getGlobalVariableName() + { + return $this->_globalVariableName; + } + + /** + * Setter description + * + * @param type $global_variable_name description + * + * @return void + */ + private function _setGlobalVariableName($global_variable_name) + { + $this->_globalVariableName = $global_variable_name; + } +} +?> diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/ExportPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/ExportPlugin.php new file mode 100644 index 0000000..a81a826 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/ExportPlugin.php @@ -0,0 +1,386 @@ +relation = new Relation($GLOBALS['dbi']); + $this->export = new Export($GLOBALS['dbi']); + $this->transformations = new Transformations(); + } + + /** + * Outputs export header + * + * @return bool Whether it succeeded + */ + abstract public function exportHeader(); + + /** + * Outputs export footer + * + * @return bool Whether it succeeded + */ + abstract public function exportFooter(); + + /** + * Outputs database header + * + * @param string $db Database name + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + abstract public function exportDBHeader($db, $db_alias = ''); + + /** + * Outputs database footer + * + * @param string $db Database name + * + * @return bool Whether it succeeded + */ + abstract public function exportDBFooter($db); + + /** + * Outputs CREATE DATABASE statement + * + * @param string $db Database name + * @param string $export_type 'server', 'database', 'table' + * @param string $db_alias Aliases of db + * + * @return bool Whether it succeeded + */ + abstract public function exportDBCreate($db, $export_type, $db_alias = ''); + + /** + * Outputs the content of a table + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $sql_query SQL query for obtaining data + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + abstract public function exportData( + $db, + $table, + $crlf, + $error_url, + $sql_query, + array $aliases = [] + ); + + /** + * The following methods are used in export.php or in db_operations.php, + * but they are not implemented by all export plugins + */ + + /** + * Exports routines (procedures and functions) + * + * @param string $db Database + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportRoutines($db, array $aliases = []) + { + } + + /** + * Exports events + * + * @param string $db Database + * + * @return bool Whether it succeeded + */ + public function exportEvents($db) + { + } + + /** + * Outputs table's structure + * + * @param string $db database name + * @param string $table table name + * @param string $crlf the end of line sequence + * @param string $error_url the url to go back in case of error + * @param string $export_mode 'create_table','triggers','create_view', + * 'stand_in' + * @param string $export_type 'server', 'database', 'table' + * @param bool $relation whether to include relation comments + * @param bool $comments whether to include the pmadb-style column comments + * as comments in the structure; this is deprecated + * but the parameter is left here because export.php + * calls exportStructure() also for other export + * types which use this parameter + * @param bool $mime whether to include mime comments + * @param bool $dates whether to include creation/update/check dates + * @param array $aliases Aliases of db/table/columns + * + * @return bool Whether it succeeded + */ + public function exportStructure( + $db, + $table, + $crlf, + $error_url, + $export_mode, + $export_type, + $relation = false, + $comments = false, + $mime = false, + $dates = false, + array $aliases = [] + ) { + } + + /** + * Exports metadata from Configuration Storage + * + * @param string $db database being exported + * @param string|array $tables table(s) being exported + * @param array $metadataTypes types of metadata to export + * + * @return bool Whether it succeeded + */ + public function exportMetadata( + $db, + $tables, + array $metadataTypes + ) { + } + + /** + * Returns a stand-in CREATE definition to resolve view dependencies + * + * @param string $db the database name + * @param string $view the view name + * @param string $crlf the end of line sequence + * @param array $aliases Aliases of db/table/columns + * + * @return string resulting definition + */ + public function getTableDefStandIn($db, $view, $crlf, $aliases = []) + { + } + + /** + * Outputs triggers + * + * @param string $db database name + * @param string $table table name + * + * @return string Formatted triggers list + */ + protected function getTriggers($db, $table) + { + } + + /** + * Initialize the specific variables for each export plugin + * + * @return void + */ + protected function initSpecificVariables() + { + } + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the export specific format plugin properties + * + * @return ExportPluginProperties + */ + public function getProperties() + { + return $this->properties; + } + + /** + * Sets the export plugins properties and is implemented by each export + * plugin + * + * @return void + */ + abstract protected function setProperties(); + + /** + * The following methods are implemented here so that they + * can be used by all export plugin without overriding it. + * Note: If you are creating a export plugin then don't include + * below methods unless you want to override them. + */ + + /** + * Initialize aliases + * + * @param array $aliases Alias information for db/table/column + * @param string $db the database + * @param string $table the table + * + * @return void + */ + public function initAlias($aliases, &$db, &$table = null) + { + if (! empty($aliases[$db]['tables'][$table]['alias'])) { + $table = $aliases[$db]['tables'][$table]['alias']; + } + if (! empty($aliases[$db]['alias'])) { + $db = $aliases[$db]['alias']; + } + } + + /** + * Search for alias of a identifier. + * + * @param array $aliases Alias information for db/table/column + * @param string $id the identifier to be searched + * @param string $type db/tbl/col or any combination of them + * representing what to be searched + * @param string $db the database in which search is to be done + * @param string $tbl the table in which search is to be done + * + * @return string alias of the identifier if found or '' + */ + public function getAlias(array $aliases, $id, $type = 'dbtblcol', $db = '', $tbl = '') + { + if (! empty($db) && isset($aliases[$db])) { + $aliases = [ + $db => $aliases[$db], + ]; + } + // search each database + foreach ($aliases as $db_key => $db) { + // check if id is database and has alias + if (false !== stripos($type, 'db') + && $db_key === $id + && ! empty($db['alias']) + ) { + return $db['alias']; + } + if (empty($db['tables'])) { + continue; + } + if (! empty($tbl) && isset($db['tables'][$tbl])) { + $db['tables'] = [ + $tbl => $db['tables'][$tbl], + ]; + } + // search each of its tables + foreach ($db['tables'] as $table_key => $table) { + // check if id is table and has alias + if (false !== stripos($type, 'tbl') + && $table_key === $id + && ! empty($table['alias']) + ) { + return $table['alias']; + } + if (empty($table['columns'])) { + continue; + } + // search each of its columns + foreach ($table['columns'] as $col_key => $col) { + // check if id is column + if (false !== stripos($type, 'col') + && $col_key === $id + && ! empty($col) + ) { + return $col; + } + } + } + } + + return ''; + } + + /** + * Gives the relation string and + * also substitutes with alias if required + * in this format: + * [Foreign Table] ([Foreign Field]) + * + * @param array $res_rel the foreigners array + * @param string $field_name the field name + * @param string $db the field name + * @param array $aliases Alias information for db/table/column + * + * @return string the Relation string + */ + public function getRelationString( + array $res_rel, + $field_name, + $db, + array $aliases = [] + ) { + $relation = ''; + $foreigner = $this->relation->searchColumnInForeigners($res_rel, $field_name); + if ($foreigner) { + $ftable = $foreigner['foreign_table']; + $ffield = $foreigner['foreign_field']; + if (! empty($aliases[$db]['tables'][$ftable]['columns'][$ffield])) { + $ffield = $aliases[$db]['tables'][$ftable]['columns'][$ffield]; + } + if (! empty($aliases[$db]['tables'][$ftable]['alias'])) { + $ftable = $aliases[$db]['tables'][$ftable]['alias']; + } + $relation = $ftable . ' (' . $ffield . ')'; + } + + return $relation; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/IOTransformationsPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/IOTransformationsPlugin.php new file mode 100644 index 0000000..3652eba --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/IOTransformationsPlugin.php @@ -0,0 +1,98 @@ +error; + } + + /** + * Returns the success status + * + * @return bool + */ + public function isSuccess() + { + return $this->success; + } + + /** + * Resets the object properties + * + * @return void + */ + public function reset() + { + $this->success = true; + $this->error = ''; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/AbstractImportCsv.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/AbstractImportCsv.php new file mode 100644 index 0000000..5c5a250 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/AbstractImportCsv.php @@ -0,0 +1,94 @@ +setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $importPluginProperties + // this will be shown as "Format specific options" + $importSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new OptionsPropertyMainGroup("general_opts"); + + // create common items and add them to the group + $leaf = new BoolPropertyItem( + "replace", + __( + 'Update data when duplicate keys found on import (add ON DUPLICATE ' + . 'KEY UPDATE)' + ) + ); + $generalOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + "terminated", + __('Columns separated with:') + ); + $leaf->setSize(2); + $generalOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + "enclosed", + __('Columns enclosed with:') + ); + $leaf->setSize(2); + $leaf->setLen(2); + $generalOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + "escaped", + __('Columns escaped with:') + ); + $leaf->setSize(2); + $leaf->setLen(2); + $generalOptions->addProperty($leaf); + $leaf = new TextPropertyItem( + "new_line", + __('Lines terminated with:') + ); + $leaf->setSize(2); + $generalOptions->addProperty($leaf); + + // add the main group to the root group + $importSpecificOptions->addProperty($generalOptions); + + // set the options for the import plugin property item + $importPluginProperties->setOptions($importSpecificOptions); + $this->properties = $importPluginProperties; + + return $generalOptions; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportCsv.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportCsv.php new file mode 100644 index 0000000..e5494a9 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportCsv.php @@ -0,0 +1,818 @@ +setProperties(); + } + + /** + * Sets the import plugin properties. + * Called in the constructor. + * + * @return void + */ + protected function setProperties() + { + $this->_setAnalyze(false); + + if ($GLOBALS['plugin_param'] !== 'table') { + $this->_setAnalyze(true); + } + + $generalOptions = parent::setProperties(); + $this->properties->setText('CSV'); + $this->properties->setExtension('csv'); + + if ($GLOBALS['plugin_param'] !== 'table') { + $leaf = new TextPropertyItem( + "new_tbl_name", + __( + 'Name of the new table (optional):' + ) + ); + $generalOptions->addProperty($leaf); + + if ($GLOBALS['plugin_param'] === 'server') { + $leaf = new TextPropertyItem( + "new_db_name", + __( + 'Name of the new database (optional):' + ) + ); + $generalOptions->addProperty($leaf); + } + + $leaf = new NumberPropertyItem( + "partial_import", + __( + 'Import these many number of rows (optional):' + ) + ); + $generalOptions->addProperty($leaf); + + $leaf = new BoolPropertyItem( + "col_names", + __( + 'The first line of the file contains the table column names' + . ' (if this is unchecked, the first line will become part' + . ' of the data)' + ) + ); + $generalOptions->addProperty($leaf); + } else { + $leaf = new NumberPropertyItem( + "partial_import", + __( + 'Import these many number of rows (optional):' + ) + ); + $generalOptions->addProperty($leaf); + + $hint = new Message( + __( + 'If the data in each row of the file is not' + . ' in the same order as in the database, list the corresponding' + . ' column names here. Column names must be separated by commas' + . ' and not enclosed in quotations.' + ) + ); + $leaf = new TextPropertyItem( + "columns", + __('Column names:') . ' ' . Util::showHint($hint) + ); + $generalOptions->addProperty($leaf); + } + + $leaf = new BoolPropertyItem( + "ignore", + __('Do not abort on INSERT error') + ); + $generalOptions->addProperty($leaf); + } + + /** + * Handles the whole import logic + * + * @param array $sql_data 2-element array with sql data + * + * @return void + */ + public function doImport(array &$sql_data = []) + { + global $db, $table, $csv_terminated, $csv_enclosed, $csv_escaped, + $csv_new_line, $csv_columns, $err_url, $import_file_name; + // $csv_replace and $csv_ignore should have been here, + // but we use directly from $_POST + global $error, $timeout_passed, $finished, $message; + + $import_file_name = basename($import_file_name, ".csv"); + $import_file_name = mb_strtolower($import_file_name); + $import_file_name = preg_replace("/[^a-zA-Z0-9_]/", "_", $import_file_name); + + $replacements = [ + '\\n' => "\n", + '\\t' => "\t", + '\\r' => "\r", + ]; + $csv_terminated = strtr($csv_terminated, $replacements); + $csv_enclosed = strtr($csv_enclosed, $replacements); + $csv_escaped = strtr($csv_escaped, $replacements); + $csv_new_line = strtr($csv_new_line, $replacements); + + $param_error = false; + if (strlen($csv_terminated) === 0) { + $message = Message::error( + __('Invalid parameter for CSV import: %s') + ); + $message->addParam(__('Columns terminated with')); + $error = true; + $param_error = true; + // The default dialog of MS Excel when generating a CSV produces a + // semi-colon-separated file with no chance of specifying the + // enclosing character. Thus, users who want to import this file + // tend to remove the enclosing character on the Import dialog. + // I could not find a test case where having no enclosing characters + // confuses this script. + // But the parser won't work correctly with strings so we allow just + // one character. + } elseif (mb_strlen($csv_enclosed) > 1) { + $message = Message::error( + __('Invalid parameter for CSV import: %s') + ); + $message->addParam(__('Columns enclosed with')); + $error = true; + $param_error = true; + // I could not find a test case where having no escaping characters + // confuses this script. + // But the parser won't work correctly with strings so we allow just + // one character. + } elseif (mb_strlen($csv_escaped) > 1) { + $message = Message::error( + __('Invalid parameter for CSV import: %s') + ); + $message->addParam(__('Columns escaped with')); + $error = true; + $param_error = true; + } elseif (mb_strlen($csv_new_line) != 1 + && $csv_new_line != 'auto' + ) { + $message = Message::error( + __('Invalid parameter for CSV import: %s') + ); + $message->addParam(__('Lines terminated with')); + $error = true; + $param_error = true; + } + + // If there is an error in the parameters entered, + // indicate that immediately. + if ($param_error) { + Util::mysqlDie( + $message->getMessage(), + '', + false, + $err_url + ); + } + + $buffer = ''; + $required_fields = 0; + $sql_template = ''; + $fields = []; + if (! $this->_getAnalyze()) { + $sql_template = 'INSERT'; + if (isset($_POST['csv_ignore'])) { + $sql_template .= ' IGNORE'; + } + $sql_template .= ' INTO ' . Util::backquote($table); + + $tmp_fields = $GLOBALS['dbi']->getColumns($db, $table); + + if (empty($csv_columns)) { + $fields = $tmp_fields; + } else { + $sql_template .= ' ('; + $fields = []; + $tmp = preg_split('/,( ?)/', $csv_columns); + foreach ($tmp as $key => $val) { + if (count($fields) > 0) { + $sql_template .= ', '; + } + /* Trim also `, if user already included backquoted fields */ + $val = trim($val, " \t\r\n\0\x0B`"); + $found = false; + foreach ($tmp_fields as $field) { + if ($field['Field'] == $val) { + $found = true; + break; + } + } + if (! $found) { + $message = Message::error( + __( + 'Invalid column (%s) specified! Ensure that columns' + . ' names are spelled correctly, separated by commas' + . ', and not enclosed in quotes.' + ) + ); + $message->addParam($val); + $error = true; + break; + } + if (isset($field)) { + $fields[] = $field; + } + $sql_template .= Util::backquote($val); + } + $sql_template .= ') '; + } + + $required_fields = count($fields); + + $sql_template .= ' VALUES ('; + } + + // Defaults for parser + $i = 0; + $len = 0; + $lastlen = null; + $line = 1; + $lasti = -1; + $values = []; + $csv_finish = false; + $max_lines = 0; // defaults to 0 (get all the lines) + + // If we get a negative value, probably someone changed min value attribute in DOM or there is an integer overflow, whatever be the case, get all the lines + if (isset($_REQUEST['csv_partial_import']) && $_REQUEST['csv_partial_import'] > 0) { + $max_lines = $_REQUEST['csv_partial_import']; + } + $max_lines_constraint = $max_lines+1; + // if the first row has to be counted as column names, include one more row in the max lines + if (isset($_REQUEST['csv_col_names'])) { + $max_lines_constraint++; + } + + $tempRow = []; + $rows = []; + $col_names = []; + $tables = []; + + $col_count = 0; + $max_cols = 0; + $csv_terminated_len = mb_strlen($csv_terminated); + while (! ($finished && $i >= $len) && ! $error && ! $timeout_passed) { + $data = $this->import->getNextChunk(); + if ($data === false) { + // subtract data we didn't handle yet and stop processing + $GLOBALS['offset'] -= strlen($buffer); + break; + } elseif ($data !== true) { + // Append new data to buffer + $buffer .= $data; + unset($data); + + // Force a trailing new line at EOF to prevent parsing problems + if ($finished && $buffer) { + $finalch = mb_substr($buffer, -1); + if ($csv_new_line == 'auto' + && $finalch != "\r" + && $finalch != "\n" + ) { + $buffer .= "\n"; + } elseif ($csv_new_line != 'auto' + && $finalch != $csv_new_line + ) { + $buffer .= $csv_new_line; + } + } + + // Do not parse string when we're not at the end + // and don't have new line inside + if (($csv_new_line == 'auto' + && mb_strpos($buffer, "\r") === false + && mb_strpos($buffer, "\n") === false) + || ($csv_new_line != 'auto' + && mb_strpos($buffer, $csv_new_line) === false) + ) { + continue; + } + } + + // Current length of our buffer + $len = mb_strlen($buffer); + // Currently parsed char + + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) { + $ch = $this->readCsvTerminatedString( + $buffer, + $ch, + $i, + $csv_terminated_len + ); + $i += $csv_terminated_len - 1; + } + while ($i < $len) { + // Deadlock protection + if ($lasti == $i && $lastlen == $len) { + $message = Message::error( + __('Invalid format of CSV input on line %d.') + ); + $message->addParam($line); + $error = true; + break; + } + $lasti = $i; + $lastlen = $len; + + // This can happen with auto EOL and \r at the end of buffer + if (! $csv_finish) { + // Grab empty field + if ($ch == $csv_terminated) { + if ($i == $len - 1) { + break; + } + $values[] = ''; + $i++; + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) { + $ch = $this->readCsvTerminatedString( + $buffer, + $ch, + $i, + $csv_terminated_len + ); + $i += $csv_terminated_len - 1; + } + continue; + } + + // Grab one field + $fallbacki = $i; + if ($ch == $csv_enclosed) { + if ($i == $len - 1) { + break; + } + $need_end = true; + $i++; + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) { + $ch = $this->readCsvTerminatedString( + $buffer, + $ch, + $i, + $csv_terminated_len + ); + $i += $csv_terminated_len - 1; + } + } else { + $need_end = false; + } + $fail = false; + $value = ''; + while (($need_end + && ($ch != $csv_enclosed + || $csv_enclosed == $csv_escaped)) + || (! $need_end + && ! ($ch == $csv_terminated + || $ch == $csv_new_line + || ($csv_new_line == 'auto' + && ($ch == "\r" || $ch == "\n")))) + ) { + if ($ch == $csv_escaped) { + if ($i == $len - 1) { + $fail = true; + break; + } + $i++; + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 + && $ch == $csv_terminated[0] + ) { + $ch = $this->readCsvTerminatedString( + $buffer, + $ch, + $i, + $csv_terminated_len + ); + $i += $csv_terminated_len - 1; + } + if ($csv_enclosed == $csv_escaped + && ($ch == $csv_terminated + || $ch == $csv_new_line + || ($csv_new_line == 'auto' + && ($ch == "\r" || $ch == "\n"))) + ) { + break; + } + } + $value .= $ch; + if ($i == $len - 1) { + if (! $finished) { + $fail = true; + } + break; + } + $i++; + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) { + $ch = $this->readCsvTerminatedString( + $buffer, + $ch, + $i, + $csv_terminated_len + ); + $i += $csv_terminated_len - 1; + } + } + + // unquoted NULL string + if (false === $need_end && $value === 'NULL') { + $value = null; + } + + if ($fail) { + $i = $fallbacki; + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 && $ch == $csv_terminated[0]) { + $i += $csv_terminated_len - 1; + } + break; + } + // Need to strip trailing enclosing char? + if ($need_end && $ch == $csv_enclosed) { + if ($finished && $i == $len - 1) { + $ch = null; + } elseif ($i == $len - 1) { + $i = $fallbacki; + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 + && $ch == $csv_terminated[0] + ) { + $i += $csv_terminated_len - 1; + } + break; + } else { + $i++; + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 + && $ch == $csv_terminated[0] + ) { + $ch = $this->readCsvTerminatedString( + $buffer, + $ch, + $i, + $csv_terminated_len + ); + $i += $csv_terminated_len - 1; + } + } + } + // Are we at the end? + if ($ch == $csv_new_line + || ($csv_new_line == 'auto' && ($ch == "\r" || $ch == "\n")) + || ($finished && $i == $len - 1) + ) { + $csv_finish = true; + } + // Go to next char + if ($ch == $csv_terminated) { + if ($i == $len - 1) { + $i = $fallbacki; + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 + && $ch == $csv_terminated[0] + ) { + $i += $csv_terminated_len - 1; + } + break; + } + $i++; + $ch = mb_substr($buffer, $i, 1); + if ($csv_terminated_len > 1 + && $ch == $csv_terminated[0] + ) { + $ch = $this->readCsvTerminatedString( + $buffer, + $ch, + $i, + $csv_terminated_len + ); + $i += $csv_terminated_len - 1; + } + } + // If everything went okay, store value + $values[] = $value; + } + + // End of line + if ($csv_finish + || $ch == $csv_new_line + || ($csv_new_line == 'auto' && ($ch == "\r" || $ch == "\n")) + ) { + if ($csv_new_line == 'auto' && $ch == "\r") { // Handle "\r\n" + if ($i >= ($len - 2) && ! $finished) { + break; // We need more data to decide new line + } + if (mb_substr($buffer, $i + 1, 1) == "\n") { + $i++; + } + } + // We didn't parse value till the end of line, so there was + // empty one + if (! $csv_finish) { + $values[] = ''; + } + + if ($this->_getAnalyze()) { + foreach ($values as $val) { + $tempRow[] = $val; + ++$col_count; + } + + if ($col_count > $max_cols) { + $max_cols = $col_count; + } + $col_count = 0; + + $rows[] = $tempRow; + $tempRow = []; + } else { + // Do we have correct count of values? + if (count($values) != $required_fields) { + // Hack for excel + if ($values[count($values) - 1] == ';') { + unset($values[count($values) - 1]); + } else { + $message = Message::error( + __( + 'Invalid column count in CSV input' + . ' on line %d.' + ) + ); + $message->addParam($line); + $error = true; + break; + } + } + + $first = true; + $sql = $sql_template; + foreach ($values as $key => $val) { + if (! $first) { + $sql .= ', '; + } + if ($val === null) { + $sql .= 'NULL'; + } else { + $sql .= '\'' + . $GLOBALS['dbi']->escapeString($val) + . '\''; + } + + $first = false; + } + $sql .= ')'; + if (isset($_POST['csv_replace'])) { + $sql .= " ON DUPLICATE KEY UPDATE "; + foreach ($fields as $field) { + $fieldName = Util::backquote( + $field['Field'] + ); + $sql .= $fieldName . " = VALUES(" . $fieldName + . "), "; + } + $sql = rtrim($sql, ', '); + } + + /** + * @todo maybe we could add original line to verbose + * SQL in comment + */ + $this->import->runQuery($sql, $sql, $sql_data); + } + + $line++; + $csv_finish = false; + $values = []; + $buffer = mb_substr($buffer, $i + 1); + $len = mb_strlen($buffer); + $i = 0; + $lasti = -1; + $ch = mb_substr($buffer, 0, 1); + if ($max_lines > 0 && $line == $max_lines_constraint) { + $finished = 1; + break; + } + } + } // End of parser loop + if ($max_lines > 0 && $line == $max_lines_constraint) { + $finished = 1; + break; + } + } // End of import loop + + if ($this->_getAnalyze()) { + /* Fill out all rows */ + $num_rows = count($rows); + for ($i = 0; $i < $num_rows; ++$i) { + for ($j = count($rows[$i]); $j < $max_cols; ++$j) { + $rows[$i][] = 'NULL'; + } + } + + if (isset($_REQUEST['csv_col_names'])) { + $col_names = array_splice($rows, 0, 1); + $col_names = $col_names[0]; + // MySQL column names can't end with a space character. + foreach ($col_names as $key => $col_name) { + $col_names[$key] = rtrim($col_name); + } + } + + if ((isset($col_names) && count($col_names) != $max_cols) + || ! isset($col_names) + ) { + // Fill out column names + for ($i = 0; $i < $max_cols; ++$i) { + $col_names[] = 'COL ' . ($i + 1); + } + } + + // get new table name, if user didn't provide one, set the default name + if (isset($_REQUEST['csv_new_tbl_name']) + && strlen($_REQUEST['csv_new_tbl_name']) > 0 + ) { + $tbl_name = $_REQUEST['csv_new_tbl_name']; + } elseif (mb_strlen((string) $db)) { + $result = $GLOBALS['dbi']->fetchResult('SHOW TABLES'); + + // logic to get table name from filename + // if no table then use filename as table name + if (count($result) === 0) { + $tbl_name = $import_file_name; + } else { + // check to see if {filename} as table exist + $name_array = preg_grep("/{$import_file_name}/isU", $result); + // if no use filename as table name + if (count($name_array) === 0) { + $tbl_name = $import_file_name; + } else { + // check if {filename}_ as table exist + $name_array = preg_grep("/{$import_file_name}_/isU", $result); + $tbl_name = $import_file_name . "_" . (count($name_array) + 1); + } + } + } else { + $tbl_name = $import_file_name; + } + + $tables[] = [ + $tbl_name, + $col_names, + $rows, + ]; + + /* Obtain the best-fit MySQL types for each column */ + $analyses = []; + $analyses[] = $this->import->analyzeTable($tables[0]); + + /** + * string $db_name (no backquotes) + * + * array $table = array(table_name, array() column_names, array()() rows) + * array $tables = array of "$table"s + * + * array $analysis = array(array() column_types, array() column_sizes) + * array $analyses = array of "$analysis"s + * + * array $create = array of SQL strings + * + * array $options = an associative array of options + */ + + /* Set database name to the currently selected one, if applicable, + * Otherwise, check if user provided the database name in the request, + * if not, set the default name + */ + if (isset($_REQUEST['csv_new_db_name']) + && strlen($_REQUEST['csv_new_db_name']) > 0 + ) { + $newDb = $_REQUEST['csv_new_db_name']; + } else { + $result = $GLOBALS['dbi']->fetchResult('SHOW DATABASES'); + if (! is_array($result)) { + $result = []; + } + $newDb = 'CSV_DB ' . (count($result) + 1); + } + list($db_name, $options) = $this->getDbnameAndOptions($db, $newDb); + + /* Non-applicable parameters */ + $create = null; + + /* Created and execute necessary SQL statements from data */ + $this->import->buildSql($db_name, $tables, $analyses, $create, $options, $sql_data); + + unset($tables); + unset($analyses); + } + + // Commit any possible data in buffers + $this->import->runQuery('', '', $sql_data); + + if (count($values) != 0 && ! $error) { + $message = Message::error( + __('Invalid format of CSV input on line %d.') + ); + $message->addParam($line); + $error = true; + } + } + + /** + * Read the expected column_separated_with String of length + * $csv_terminated_len from the $buffer + * into variable $ch and return the read string $ch + * + * @param string $buffer The original string buffer read from + * csv file + * @param string $ch Partially read "column Separated with" + * string, also used to return after + * reading length equal $csv_terminated_len + * @param int $i Current read counter of buffer string + * @param int $csv_terminated_len The length of "column separated with" + * String + * + * @return string + */ + public function readCsvTerminatedString($buffer, $ch, $i, $csv_terminated_len) + { + for ($j = 0; $j < $csv_terminated_len - 1; $j++) { + $i++; + $ch .= mb_substr($buffer, $i, 1); + } + + return $ch; + } + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Returns true if the table should be analyzed, false otherwise + * + * @return bool + */ + private function _getAnalyze() + { + return $this->_analyze; + } + + /** + * Sets to true if the table should be analyzed, false otherwise + * + * @param bool $analyze status + * + * @return void + */ + private function _setAnalyze($analyze) + { + $this->_analyze = $analyze; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportLdi.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportLdi.php new file mode 100644 index 0000000..91260c5 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportLdi.php @@ -0,0 +1,176 @@ +setProperties(); + } + + /** + * Sets the import plugin properties. + * Called in the constructor. + * + * @return void + */ + protected function setProperties() + { + if ($GLOBALS['cfg']['Import']['ldi_local_option'] == 'auto') { + $GLOBALS['cfg']['Import']['ldi_local_option'] = false; + + $result = $GLOBALS['dbi']->tryQuery( + 'SELECT @@local_infile;' + ); + if ($result != false && $GLOBALS['dbi']->numRows($result) > 0) { + $tmp = $GLOBALS['dbi']->fetchRow($result); + if ($tmp[0] == 'ON') { + $GLOBALS['cfg']['Import']['ldi_local_option'] = true; + } + } + $GLOBALS['dbi']->freeResult($result); + unset($result); + } + + $generalOptions = parent::setProperties(); + $this->properties->setText('CSV using LOAD DATA'); + $this->properties->setExtension('ldi'); + + $leaf = new TextPropertyItem( + "columns", + __('Column names: ') + ); + $generalOptions->addProperty($leaf); + + $leaf = new BoolPropertyItem( + "ignore", + __('Do not abort on INSERT error') + ); + $generalOptions->addProperty($leaf); + + $leaf = new BoolPropertyItem( + "local_option", + __('Use LOCAL keyword') + ); + $generalOptions->addProperty($leaf); + } + + /** + * Handles the whole import logic + * + * @param array $sql_data 2-element array with sql data + * + * @return void + */ + public function doImport(array &$sql_data = []) + { + global $finished, $import_file, $charset_conversion, $table; + global $ldi_local_option, $ldi_replace, $ldi_ignore, $ldi_terminated, + $ldi_enclosed, $ldi_escaped, $ldi_new_line, $skip_queries, $ldi_columns; + + $compression = $GLOBALS['import_handle']->getCompression(); + + if ($import_file == 'none' + || $compression != 'none' + || $charset_conversion + ) { + // We handle only some kind of data! + $GLOBALS['message'] = Message::error( + __('This plugin does not support compressed imports!') + ); + $GLOBALS['error'] = true; + + return; + } + + $sql = 'LOAD DATA'; + if (isset($ldi_local_option)) { + $sql .= ' LOCAL'; + } + $sql .= ' INFILE \'' . $GLOBALS['dbi']->escapeString($import_file) + . '\''; + if (isset($ldi_replace)) { + $sql .= ' REPLACE'; + } elseif (isset($ldi_ignore)) { + $sql .= ' IGNORE'; + } + $sql .= ' INTO TABLE ' . Util::backquote($table); + + if (strlen((string) $ldi_terminated) > 0) { + $sql .= ' FIELDS TERMINATED BY \'' . $ldi_terminated . '\''; + } + if (strlen((string) $ldi_enclosed) > 0) { + $sql .= ' ENCLOSED BY \'' + . $GLOBALS['dbi']->escapeString($ldi_enclosed) . '\''; + } + if (strlen((string) $ldi_escaped) > 0) { + $sql .= ' ESCAPED BY \'' + . $GLOBALS['dbi']->escapeString($ldi_escaped) . '\''; + } + if (strlen((string) $ldi_new_line) > 0) { + if ($ldi_new_line == 'auto') { + $ldi_new_line + = (PHP_EOL == "\n") + ? '\n' + : '\r\n'; + } + $sql .= ' LINES TERMINATED BY \'' . $ldi_new_line . '\''; + } + if ($skip_queries > 0) { + $sql .= ' IGNORE ' . $skip_queries . ' LINES'; + $skip_queries = 0; + } + if (strlen((string) $ldi_columns) > 0) { + $sql .= ' ('; + $tmp = preg_split('/,( ?)/', $ldi_columns); + $cnt_tmp = count($tmp); + for ($i = 0; $i < $cnt_tmp; $i++) { + if ($i > 0) { + $sql .= ', '; + } + /* Trim also `, if user already included backquoted fields */ + $sql .= Util::backquote( + trim($tmp[$i], " \t\r\n\0\x0B`") + ); + } // end for + $sql .= ')'; + } + + $this->import->runQuery($sql, $sql, $sql_data); + $this->import->runQuery('', '', $sql_data); + $finished = true; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportMediawiki.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportMediawiki.php new file mode 100644 index 0000000..9723daf --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportMediawiki.php @@ -0,0 +1,604 @@ +setProperties(); + } + + /** + * Sets the import plugin properties. + * Called in the constructor. + * + * @return void + */ + protected function setProperties() + { + $this->_setAnalyze(false); + if ($GLOBALS['plugin_param'] !== 'table') { + $this->_setAnalyze(true); + } + + $importPluginProperties = new ImportPluginProperties(); + $importPluginProperties->setText(__('MediaWiki Table')); + $importPluginProperties->setExtension('txt'); + $importPluginProperties->setMimeType('text/plain'); + $importPluginProperties->setOptions([]); + $importPluginProperties->setOptionsText(__('Options')); + + $this->properties = $importPluginProperties; + } + + /** + * Handles the whole import logic + * + * @param array $sql_data 2-element array with sql data + * + * @return void + */ + public function doImport(array &$sql_data = []) + { + global $error, $timeout_passed, $finished; + + // Defaults for parser + + // The buffer that will be used to store chunks read from the imported file + $buffer = ''; + + // Used as storage for the last part of the current chunk data + // Will be appended to the first line of the next chunk, if there is one + $last_chunk_line = ''; + + // Remembers whether the current buffer line is part of a comment + $inside_comment = false; + // Remembers whether the current buffer line is part of a data comment + $inside_data_comment = false; + // Remembers whether the current buffer line is part of a structure comment + $inside_structure_comment = false; + + // MediaWiki only accepts "\n" as row terminator + $mediawiki_new_line = "\n"; + + // Initialize the name of the current table + $cur_table_name = ""; + + while (! $finished && ! $error && ! $timeout_passed) { + $data = $this->import->getNextChunk(); + + if ($data === false) { + // Subtract data we didn't handle yet and stop processing + $GLOBALS['offset'] -= mb_strlen($buffer); + break; + } elseif ($data !== true) { + // Append new data to buffer + $buffer = $data; + unset($data); + // Don't parse string if we're not at the end + // and don't have a new line inside + if (mb_strpos($buffer, $mediawiki_new_line) === false) { + continue; + } + } + + // Because of reading chunk by chunk, the first line from the buffer + // contains only a portion of an actual line from the imported file. + // Therefore, we have to append it to the last line from the previous + // chunk. If we are at the first chunk, $last_chunk_line should be empty. + $buffer = $last_chunk_line . $buffer; + + // Process the buffer line by line + $buffer_lines = explode($mediawiki_new_line, $buffer); + + $full_buffer_lines_count = count($buffer_lines); + // If the reading is not finalised, the final line of the current chunk + // will not be complete + if (! $finished) { + $last_chunk_line = $buffer_lines[--$full_buffer_lines_count]; + } + + for ($line_nr = 0; $line_nr < $full_buffer_lines_count; ++$line_nr) { + $cur_buffer_line = trim($buffer_lines[$line_nr]); + + // If the line is empty, go to the next one + if ($cur_buffer_line === '') { + continue; + } + + $first_character = $cur_buffer_line[0]; + $matches = []; + + // Check beginning of comment + if (! strcmp(mb_substr($cur_buffer_line, 0, 4), "") + ) { + // Only data comments are closed. The structure comments + // will be closed when a data comment begins (in order to + // skip structure tables) + if ($inside_data_comment) { + $inside_data_comment = false; + } + + // End comments that are not related to table structure + if (! $inside_structure_comment) { + $inside_comment = false; + } + } else { + // Check table name + $match_table_name = []; + if (preg_match( + "/^Table data for `(.*)`$/", + $cur_buffer_line, + $match_table_name + ) + ) { + $cur_table_name = $match_table_name[1]; + $inside_data_comment = true; + + $inside_structure_comment + = $this->_mngInsideStructComm( + $inside_structure_comment + ); + } elseif (preg_match( + "/^Table structure for `(.*)`$/", + $cur_buffer_line, + $match_table_name + ) + ) { + // The structure comments will be ignored + $inside_structure_comment = true; + } + } + continue; + } elseif (preg_match('/^\{\|(.*)$/', $cur_buffer_line, $matches)) { + // Check start of table + + // This will store all the column info on all rows from + // the current table read from the buffer + $cur_temp_table = []; + + // Will be used as storage for the current row in the buffer + // Once all its columns are read, it will be added to + // $cur_temp_table and then it will be emptied + $cur_temp_line = []; + + // Helps us differentiate the header columns + // from the normal columns + $in_table_header = false; + // End processing because the current line does not + // contain any column information + } elseif (mb_substr($cur_buffer_line, 0, 2) === '|-' + || mb_substr($cur_buffer_line, 0, 2) === '|+' + || mb_substr($cur_buffer_line, 0, 2) === '|}' + ) { + // Check begin row or end table + + // Add current line to the values storage + if (! empty($cur_temp_line)) { + // If the current line contains header cells + // ( marked with '!' ), + // it will be marked as table header + if ($in_table_header) { + // Set the header columns + $cur_temp_table_headers = $cur_temp_line; + } else { + // Normal line, add it to the table + $cur_temp_table[] = $cur_temp_line; + } + } + + // Empty the temporary buffer + $cur_temp_line = []; + + // No more processing required at the end of the table + if (mb_substr($cur_buffer_line, 0, 2) === '|}') { + $current_table = [ + $cur_table_name, + $cur_temp_table_headers, + $cur_temp_table, + ]; + + // Import the current table data into the database + $this->_importDataOneTable($current_table, $sql_data); + + // Reset table name + $cur_table_name = ""; + } + // What's after the row tag is now only attributes + } elseif (($first_character === '|') || ($first_character === '!')) { + // Check cell elements + + // Header cells + if ($first_character === '!') { + // Mark as table header, but treat as normal row + $cur_buffer_line = str_replace('!!', '||', $cur_buffer_line); + // Will be used to set $cur_temp_line as table header + $in_table_header = true; + } else { + $in_table_header = false; + } + + // Loop through each table cell + $cells = $this->_explodeMarkup($cur_buffer_line); + foreach ($cells as $cell) { + $cell = $this->_getCellData($cell); + + // Delete the beginning of the column, if there is one + $cell = trim($cell); + $col_start_chars = [ + "|", + "!", + ]; + foreach ($col_start_chars as $col_start_char) { + $cell = $this->_getCellContent($cell, $col_start_char); + } + + // Add the cell to the row + $cur_temp_line[] = $cell; + } // foreach $cells + } else { + // If it's none of the above, then the current line has a bad + // format + $message = Message::error( + __('Invalid format of mediawiki input on line:
    %s.') + ); + $message->addParam($cur_buffer_line); + $error = true; + } + } // End treating full buffer lines + } // while - finished parsing buffer + } + + /** + * Imports data from a single table + * + * @param array $table containing all table info: + * $table[0] - string + * containing table name + * $table[1] - array[] of + * table headers $table[2] - + * array[][] of table content + * rows + * + * @param array $sql_data 2-element array with sql data + * + * @global bool $analyze whether to scan for column types + * + * @return void + */ + private function _importDataOneTable(array $table, array &$sql_data) + { + $analyze = $this->_getAnalyze(); + if ($analyze) { + // Set the table name + $this->_setTableName($table[0]); + + // Set generic names for table headers if they don't exist + $this->_setTableHeaders($table[1], $table[2][0]); + + // Create the tables array to be used in Import::buildSql() + $tables = []; + $tables[] = [ + $table[0], + $table[1], + $table[2], + ]; + + // Obtain the best-fit MySQL types for each column + $analyses = []; + $analyses[] = $this->import->analyzeTable($tables[0]); + + $this->_executeImportTables($tables, $analyses, $sql_data); + } + + // Commit any possible data in buffers + $this->import->runQuery('', '', $sql_data); + } + + /** + * Sets the table name + * + * @param string $table_name reference to the name of the table + * + * @return void + */ + private function _setTableName(&$table_name) + { + if (empty($table_name)) { + $result = $GLOBALS['dbi']->fetchResult('SHOW TABLES'); + // todo check if the name below already exists + $table_name = 'TABLE ' . (count($result) + 1); + } + } + + /** + * Set generic names for table headers, if they don't exist + * + * @param array $table_headers reference to the array containing the headers + * of a table + * @param array $table_row array containing the first content row + * + * @return void + */ + private function _setTableHeaders(array &$table_headers, array $table_row) + { + if (empty($table_headers)) { + // The first table row should contain the number of columns + // If they are not set, generic names will be given (COL 1, COL 2, etc) + $num_cols = count($table_row); + for ($i = 0; $i < $num_cols; ++$i) { + $table_headers[$i] = 'COL ' . ($i + 1); + } + } + } + + /** + * Sets the database name and additional options and calls Import::buildSql() + * Used in PMA_importDataAllTables() and $this->_importDataOneTable() + * + * @param array $tables structure: + * array( + * array(table_name, array() column_names, array()() + * rows) + * ) + * @param array $analyses structure: + * $analyses = array( + * array(array() column_types, array() column_sizes) + * ) + * @param array $sql_data 2-element array with sql data + * + * @global string $db name of the database to import in + * + * @return void + */ + private function _executeImportTables(array &$tables, array &$analyses, array &$sql_data) + { + global $db; + + // $db_name : The currently selected database name, if applicable + // No backquotes + // $options : An associative array of options + list($db_name, $options) = $this->getDbnameAndOptions($db, 'mediawiki_DB'); + + // Array of SQL strings + // Non-applicable parameters + $create = null; + + // Create and execute necessary SQL statements from data + $this->import->buildSql($db_name, $tables, $analyses, $create, $options, $sql_data); + } + + /** + * Replaces all instances of the '||' separator between delimiters + * in a given string + * + * @param string $replace the string to be replaced with + * @param string $subject the text to be replaced + * + * @return string with replacements + */ + private function _delimiterReplace($replace, $subject) + { + // String that will be returned + $cleaned = ""; + // Possible states of current character + $inside_tag = false; + $inside_attribute = false; + // Attributes can be declared with either " or ' + $start_attribute_character = false; + + // The full separator is "||"; + // This remembers if the previous character was '|' + $partial_separator = false; + + // Parse text char by char + for ($i = 0, $iMax = strlen($subject); $i < $iMax; $i++) { + $cur_char = $subject[$i]; + // Check for separators + if ($cur_char == '|') { + // If we're not inside a tag, then this is part of a real separator, + // so we append it to the current segment + if (! $inside_attribute) { + $cleaned .= $cur_char; + if ($partial_separator) { + $inside_tag = false; + $inside_attribute = false; + } + } elseif ($partial_separator) { + // If we are inside a tag, we replace the current char with + // the placeholder and append that to the current segment + $cleaned .= $replace; + } + + // If the previous character was also '|', then this ends a + // full separator. If not, this may be the beginning of one + $partial_separator = ! $partial_separator; + } else { + // If we're inside a tag attribute and the current character is + // not '|', but the previous one was, it means that the single '|' + // was not appended, so we append it now + if ($partial_separator && $inside_attribute) { + $cleaned .= "|"; + } + // If the char is different from "|", no separator can be formed + $partial_separator = false; + + // any other character should be appended to the current segment + $cleaned .= $cur_char; + + if ($cur_char == '<' && ! $inside_attribute) { + // start of a tag + $inside_tag = true; + } elseif ($cur_char == '>' && ! $inside_attribute) { + // end of a tag + $inside_tag = false; + } elseif (($cur_char == '"' || $cur_char == "'") && $inside_tag) { + // start or end of an attribute + if (! $inside_attribute) { + $inside_attribute = true; + // remember the attribute`s declaration character (" or ') + $start_attribute_character = $cur_char; + } else { + if ($cur_char == $start_attribute_character) { + $inside_attribute = false; + // unset attribute declaration character + $start_attribute_character = false; + } + } + } + } + } // end for each character in $subject + + return $cleaned; + } + + /** + * Separates a string into items, similarly to explode + * Uses the '||' separator (which is standard in the mediawiki format) + * and ignores any instances of it inside markup tags + * Used in parsing buffer lines containing data cells + * + * @param string $text text to be split + * + * @return array + */ + private function _explodeMarkup($text) + { + $separator = "||"; + $placeholder = "\x00"; + + // Remove placeholder instances + $text = str_replace($placeholder, '', $text); + + // Replace instances of the separator inside HTML-like + // tags with the placeholder + $cleaned = $this->_delimiterReplace($placeholder, $text); + // Explode, then put the replaced separators back in + $items = explode($separator, $cleaned); + foreach ($items as $i => $str) { + $items[$i] = str_replace($placeholder, $separator, $str); + } + + return $items; + } + + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Returns true if the table should be analyzed, false otherwise + * + * @return bool + */ + private function _getAnalyze() + { + return $this->_analyze; + } + + /** + * Sets to true if the table should be analyzed, false otherwise + * + * @param bool $analyze status + * + * @return void + */ + private function _setAnalyze($analyze) + { + $this->_analyze = $analyze; + } + + /** + * Get cell + * + * @param string $cell Cell + * + * @return mixed + */ + private function _getCellData($cell) + { + // A cell could contain both parameters and data + $cell_data = explode('|', $cell, 2); + + // A '|' inside an invalid link should not + // be mistaken as delimiting cell parameters + if (mb_strpos($cell_data[0], '[[') === false) { + return $cell; + } + + if (count($cell_data) === 1) { + return $cell_data[0]; + } + + return $cell_data[1]; + } + + /** + * Manage $inside_structure_comment + * + * @param boolean $inside_structure_comment Value to test + * + * @return bool + */ + private function _mngInsideStructComm($inside_structure_comment) + { + // End ignoring structure rows + if ($inside_structure_comment) { + $inside_structure_comment = false; + } + + return $inside_structure_comment; + } + + /** + * Get cell content + * + * @param string $cell Cell + * @param string $col_start_char Start char + * + * @return string + */ + private function _getCellContent($cell, $col_start_char) + { + if (mb_strpos($cell, $col_start_char) === 0) { + $cell = trim(mb_substr($cell, 1)); + } + + return $cell; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportOds.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportOds.php new file mode 100644 index 0000000..8e68692 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportOds.php @@ -0,0 +1,427 @@ +setProperties(); + } + + /** + * Sets the import plugin properties. + * Called in the constructor. + * + * @return void + */ + protected function setProperties() + { + $importPluginProperties = new ImportPluginProperties(); + $importPluginProperties->setText('OpenDocument Spreadsheet'); + $importPluginProperties->setExtension('ods'); + $importPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $importPluginProperties + // this will be shown as "Format specific options" + $importSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new OptionsPropertyMainGroup("general_opts"); + // create primary items and add them to the group + $leaf = new BoolPropertyItem( + "col_names", + __( + 'The first line of the file contains the table column names' + . ' (if this is unchecked, the first line will become part' + . ' of the data)' + ) + ); + $generalOptions->addProperty($leaf); + $leaf = new BoolPropertyItem( + "empty_rows", + __('Do not import empty rows') + ); + $generalOptions->addProperty($leaf); + $leaf = new BoolPropertyItem( + "recognize_percentages", + __( + 'Import percentages as proper decimals (ex. 12.00% to .12)' + ) + ); + $generalOptions->addProperty($leaf); + $leaf = new BoolPropertyItem( + "recognize_currency", + __('Import currencies (ex. $5.00 to 5.00)') + ); + $generalOptions->addProperty($leaf); + + // add the main group to the root group + $importSpecificOptions->addProperty($generalOptions); + + // set the options for the import plugin property item + $importPluginProperties->setOptions($importSpecificOptions); + $this->properties = $importPluginProperties; + } + + /** + * Handles the whole import logic + * + * @param array $sql_data 2-element array with sql data + * + * @return void + */ + public function doImport(array &$sql_data = []) + { + global $db, $error, $timeout_passed, $finished; + + $i = 0; + $len = 0; + $buffer = ""; + + /** + * Read in the file via Import::getNextChunk so that + * it can process compressed files + */ + while (! ($finished && $i >= $len) && ! $error && ! $timeout_passed) { + $data = $this->import->getNextChunk(); + if ($data === false) { + /* subtract data we didn't handle yet and stop processing */ + $GLOBALS['offset'] -= strlen($buffer); + break; + } elseif ($data !== true) { + /* Append new data to buffer */ + $buffer .= $data; + unset($data); + } + } + + unset($data); + + /** + * Disable loading of external XML entities. + */ + libxml_disable_entity_loader(); + + /** + * Load the XML string + * + * The option LIBXML_COMPACT is specified because it can + * result in increased performance without the need to + * alter the code in any way. It's basically a freebee. + */ + $xml = @simplexml_load_string($buffer, "SimpleXMLElement", LIBXML_COMPACT); + + unset($buffer); + + if ($xml === false) { + $sheets = []; + $GLOBALS['message'] = Message::error( + __( + 'The XML file specified was either malformed or incomplete.' + . ' Please correct the issue and try again.' + ) + ); + $GLOBALS['error'] = true; + } else { + /** @var SimpleXMLElement $root */ + $root = $xml->children('office', true)->{'body'}->{'spreadsheet'}; + if (empty($root)) { + $sheets = []; + $GLOBALS['message'] = Message::error( + __('Could not parse OpenDocument Spreadsheet!') + ); + $GLOBALS['error'] = true; + } else { + $sheets = $root->children('table', true); + } + } + + $tables = []; + + $max_cols = 0; + + $col_count = 0; + $col_names = []; + + $tempRow = []; + $tempRows = []; + $rows = []; + + /* Iterate over tables */ + /** @var SimpleXMLElement $sheet */ + foreach ($sheets as $sheet) { + $col_names_in_first_row = isset($_REQUEST['ods_col_names']); + + /* Iterate over rows */ + /** @var SimpleXMLElement $row */ + foreach ($sheet as $row) { + $type = $row->getName(); + if (strcmp('table-row', $type)) { + continue; + } + /* Iterate over columns */ + $cellCount = count($row); + $a = 0; + /** @var SimpleXMLElement $cell */ + foreach ($row as $cell) { + $a++; + $text = $cell->children('text', true); + $cell_attrs = $cell->attributes('office', true); + + if (count($text) != 0) { + $attr = $cell->attributes('table', true); + $num_repeat = (int) $attr['number-columns-repeated']; + $num_iterations = $num_repeat ?: 1; + + for ($k = 0; $k < $num_iterations; $k++) { + $value = $this->getValue($cell_attrs, $text); + if (! $col_names_in_first_row) { + $tempRow[] = $value; + } else { + // MySQL column names can't end with a space + // character. + $col_names[] = rtrim($value); + } + + ++$col_count; + } + continue; + } + + // skip empty repeats in the last row + if ($a == $cellCount) { + continue; + } + + $attr = $cell->attributes('table', true); + $num_null = (int) $attr['number-columns-repeated']; + + if ($num_null) { + if (! $col_names_in_first_row) { + for ($i = 0; $i < $num_null; ++$i) { + $tempRow[] = 'NULL'; + ++$col_count; + } + } else { + for ($i = 0; $i < $num_null; ++$i) { + $col_names[] = $this->import->getColumnAlphaName( + $col_count + 1 + ); + ++$col_count; + } + } + } else { + if (! $col_names_in_first_row) { + $tempRow[] = 'NULL'; + } else { + $col_names[] = $this->import->getColumnAlphaName( + $col_count + 1 + ); + } + + ++$col_count; + } + } //Endforeach + + /* Find the widest row */ + if ($col_count > $max_cols) { + $max_cols = $col_count; + } + + /* Don't include a row that is full of NULL values */ + if (! $col_names_in_first_row) { + if ($_REQUEST['ods_empty_rows']) { + foreach ($tempRow as $cell) { + if (strcmp('NULL', $cell)) { + $tempRows[] = $tempRow; + break; + } + } + } else { + $tempRows[] = $tempRow; + } + } + + $col_count = 0; + $col_names_in_first_row = false; + $tempRow = []; + } + + /* Skip over empty sheets */ + if (count($tempRows) == 0 || count($tempRows[0]) === 0) { + $col_names = []; + $tempRow = []; + $tempRows = []; + continue; + } + + /** + * Fill out each row as necessary to make + * every one exactly as wide as the widest + * row. This included column names. + */ + + /* Fill out column names */ + for ($i = count($col_names); $i < $max_cols; ++$i) { + $col_names[] = $this->import->getColumnAlphaName($i + 1); + } + + /* Fill out all rows */ + $num_rows = count($tempRows); + for ($i = 0; $i < $num_rows; ++$i) { + for ($j = count($tempRows[$i]); $j < $max_cols; ++$j) { + $tempRows[$i][] = 'NULL'; + } + } + + /* Store the table name so we know where to place the row set */ + $tbl_attr = $sheet->attributes('table', true); + $tables[] = [(string) $tbl_attr['name']]; + + /* Store the current sheet in the accumulator */ + $rows[] = [ + (string) $tbl_attr['name'], + $col_names, + $tempRows, + ]; + $tempRows = []; + $col_names = []; + $max_cols = 0; + } + + unset($tempRow); + unset($tempRows); + unset($col_names); + unset($sheets); + unset($xml); + + /** + * Bring accumulated rows into the corresponding table + */ + $num_tables = count($tables); + for ($i = 0; $i < $num_tables; ++$i) { + $num_rows = count($rows); + for ($j = 0; $j < $num_rows; ++$j) { + if (strcmp($tables[$i][Import::TBL_NAME], $rows[$j][Import::TBL_NAME])) { + continue; + } + + if (! isset($tables[$i][Import::COL_NAMES])) { + $tables[$i][] = $rows[$j][Import::COL_NAMES]; + } + + $tables[$i][Import::ROWS] = $rows[$j][Import::ROWS]; + } + } + + /* No longer needed */ + unset($rows); + + /* Obtain the best-fit MySQL types for each column */ + $analyses = []; + + $len = count($tables); + for ($i = 0; $i < $len; ++$i) { + $analyses[] = $this->import->analyzeTable($tables[$i]); + } + + /** + * string $db_name (no backquotes) + * + * array $table = array(table_name, array() column_names, array()() rows) + * array $tables = array of "$table"s + * + * array $analysis = array(array() column_types, array() column_sizes) + * array $analyses = array of "$analysis"s + * + * array $create = array of SQL strings + * + * array $options = an associative array of options + */ + + /* Set database name to the currently selected one, if applicable */ + list($db_name, $options) = $this->getDbnameAndOptions($db, 'ODS_DB'); + + /* Non-applicable parameters */ + $create = null; + + /* Created and execute necessary SQL statements from data */ + $this->import->buildSql($db_name, $tables, $analyses, $create, $options, $sql_data); + + unset($tables); + unset($analyses); + + /* Commit any possible data in buffers */ + $this->import->runQuery('', '', $sql_data); + } + + /** + * Get value + * + * @param array $cell_attrs Cell attributes + * @param array $text Texts + * + * @return float|string + */ + protected function getValue($cell_attrs, $text) + { + if ($_REQUEST['ods_recognize_percentages'] + && ! strcmp( + 'percentage', + (string) $cell_attrs['value-type'] + ) + ) { + $value = (double) $cell_attrs['value']; + + return $value; + } elseif ($_REQUEST['ods_recognize_currency'] + && ! strcmp('currency', (string) $cell_attrs['value-type']) + ) { + $value = (double) $cell_attrs['value']; + + return $value; + } + + /* We need to concatenate all paragraphs */ + $values = []; + foreach ($text as $paragraph) { + $values[] = (string) $paragraph; + } + $value = implode("\n", $values); + + return $value; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportShp.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportShp.php new file mode 100644 index 0000000..614cb9e --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportShp.php @@ -0,0 +1,335 @@ +setProperties(); + if (extension_loaded('zip')) { + $this->zipExtension = new ZipExtension(); + } + } + + /** + * Sets the import plugin properties. + * Called in the constructor. + * + * @return void + */ + protected function setProperties() + { + $importPluginProperties = new ImportPluginProperties(); + $importPluginProperties->setText(__('ESRI Shape File')); + $importPluginProperties->setExtension('shp'); + $importPluginProperties->setOptions([]); + $importPluginProperties->setOptionsText(__('Options')); + + $this->properties = $importPluginProperties; + } + + /** + * Handles the whole import logic + * + * @param array $sql_data 2-element array with sql data + * + * @return void + */ + public function doImport(array &$sql_data = []) + { + global $db, $error, $finished, + $import_file, $local_import_file, $message; + + $GLOBALS['finished'] = false; + + $compression = $GLOBALS['import_handle']->getCompression(); + + $shp = new ShapeFileImport(1); + // If the zip archive has more than one file, + // get the correct content to the buffer from .shp file. + if ($compression == 'application/zip' + && $this->zipExtension->getNumberOfFiles($import_file) > 1 + ) { + if ($GLOBALS['import_handle']->openZip('/^.*\.shp$/i') === false) { + $message = Message::error( + __('There was an error importing the ESRI shape file: "%s".') + ); + $message->addParam($GLOBALS['import_handle']->getError()); + + return; + } + } + + $temp_dbf_file = false; + // We need dbase extension to handle .dbf file + if (extension_loaded('dbase')) { + $temp = $GLOBALS['PMA_Config']->getTempDir('shp'); + // If we can extract the zip archive to 'TempDir' + // and use the files in it for import + if ($compression == 'application/zip' && $temp !== null) { + $dbf_file_name = $this->zipExtension->findFile( + $import_file, + '/^.*\.dbf$/i' + ); + // If the corresponding .dbf file is in the zip archive + if ($dbf_file_name) { + // Extract the .dbf file and point to it. + $extracted = $this->zipExtension->extract( + $import_file, + $dbf_file_name + ); + if ($extracted !== false) { + // remove filename extension, e.g. + // dresden_osm.shp/gis.osm_transport_a_v06.dbf + // to + // dresden_osm.shp/gis.osm_transport_a_v06 + $path_parts = pathinfo($dbf_file_name); + $dbf_file_name = $path_parts['dirname'] . '/' . $path_parts['filename']; + + // sanitize filename + $dbf_file_name = Sanitize::sanitizeFilename($dbf_file_name, true); + + // concat correct filename and extension + $dbf_file_path = $temp . '/' . $dbf_file_name . '.dbf'; + + if (file_put_contents($dbf_file_path, $extracted, LOCK_EX) !== false) { + $temp_dbf_file = true; + + // Replace the .dbf with .*, as required by the bsShapeFiles library. + $shp->FileName = substr($dbf_file_path, 0, -4) . '.*'; + } + } + } + } elseif (! empty($local_import_file) + && ! empty($GLOBALS['cfg']['UploadDir']) + && $compression == 'none' + ) { + // If file is in UploadDir, use .dbf file in the same UploadDir + // to load extra data. + // Replace the .shp with .*, + // so the bsShapeFiles library correctly locates .dbf file. + $file_name = mb_substr( + $import_file, + 0, + mb_strlen($import_file) - 4 + ) . '.*'; + $shp->FileName = $file_name; + } + } + + // It should load data before file being deleted + $shp->loadFromFile(''); + + // Delete the .dbf file extracted to 'TempDir' + if ($temp_dbf_file + && isset($dbf_file_path) + && @file_exists($dbf_file_path) + ) { + unlink($dbf_file_path); + } + + if ($shp->lastError != '') { + $error = true; + $message = Message::error( + __('There was an error importing the ESRI shape file: "%s".') + ); + $message->addParam($shp->lastError); + + return; + } + + switch ($shp->shapeType) { + // ESRI Null Shape + case 0: + break; + // ESRI Point + case 1: + $gis_type = 'point'; + break; + // ESRI PolyLine + case 3: + $gis_type = 'multilinestring'; + break; + // ESRI Polygon + case 5: + $gis_type = 'multipolygon'; + break; + // ESRI MultiPoint + case 8: + $gis_type = 'multipoint'; + break; + default: + $error = true; + $message = Message::error( + __('MySQL Spatial Extension does not support ESRI type "%s".') + ); + $message->addParam($shp->getShapeName()); + return; + } + + if (isset($gis_type)) { + /** @var GisMultiLineString|GisMultiPoint|GisPoint|GisPolygon $gis_obj */ + $gis_obj = GisFactory::factory($gis_type); + } else { + $gis_obj = null; + } + + $num_rows = count($shp->records); + // If .dbf file is loaded, the number of extra data columns + $num_data_cols = $shp->getDBFHeader() !== null ? count($shp->getDBFHeader()) : 0; + + $rows = []; + $col_names = []; + if ($num_rows != 0) { + foreach ($shp->records as $record) { + $tempRow = []; + if ($gis_obj == null) { + $tempRow[] = null; + } else { + $tempRow[] = "GeomFromText('" + . $gis_obj->getShape($record->SHPData) . "')"; + } + + if ($shp->getDBFHeader() !== null) { + foreach ($shp->getDBFHeader() as $c) { + $cell = trim((string) $record->DBFData[$c[0]]); + + if (! strcmp($cell, '')) { + $cell = 'NULL'; + } + + $tempRow[] = $cell; + } + } + $rows[] = $tempRow; + } + } + + if (count($rows) === 0) { + $error = true; + $message = Message::error( + __('The imported file does not contain any data!') + ); + + return; + } + + // Column names for spatial column and the rest of the columns, + // if they are available + $col_names[] = 'SPATIAL'; + for ($n = 0; $n < $num_data_cols; $n++) { + $col_names[] = $shp->getDBFHeader()[$n][0]; + } + + // Set table name based on the number of tables + if (strlen((string) $db) > 0) { + $result = $GLOBALS['dbi']->fetchResult('SHOW TABLES'); + $table_name = 'TABLE ' . (count($result) + 1); + } else { + $table_name = 'TBL_NAME'; + } + $tables = [ + [ + $table_name, + $col_names, + $rows, + ], + ]; + + // Use data from shape file to chose best-fit MySQL types for each column + $analyses = []; + $analyses[] = $this->import->analyzeTable($tables[0]); + + $table_no = 0; + $spatial_col = 0; + $analyses[$table_no][Import::TYPES][$spatial_col] = Import::GEOMETRY; + $analyses[$table_no][Import::FORMATTEDSQL][$spatial_col] = true; + + // Set database name to the currently selected one, if applicable + if (strlen((string) $db) > 0) { + $db_name = $db; + $options = ['create_db' => false]; + } else { + $db_name = 'SHP_DB'; + $options = null; + } + + // Created and execute necessary SQL statements from data + $null_param = null; + $this->import->buildSql($db_name, $tables, $analyses, $null_param, $options, $sql_data); + + unset($tables); + unset($analyses); + + $finished = true; + $error = false; + + // Commit any possible data in buffers + $this->import->runQuery('', '', $sql_data); + } + + /** + * Returns specified number of bytes from the buffer. + * Buffer automatically fetches next chunk of data when the buffer + * falls short. + * Sets $eof when $GLOBALS['finished'] is set and the buffer falls short. + * + * @param int $length number of bytes + * + * @return string + */ + public static function readFromBuffer($length) + { + global $buffer, $eof; + + $import = new Import(); + + if (strlen((string) $buffer) < $length) { + if ($GLOBALS['finished']) { + $eof = true; + } else { + $buffer .= $import->getNextChunk(); + } + } + $result = substr($buffer, 0, $length); + $buffer = substr($buffer, $length); + + return $result; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportSql.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportSql.php new file mode 100644 index 0000000..599db37 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportSql.php @@ -0,0 +1,200 @@ +setProperties(); + } + + /** + * Sets the import plugin properties. + * Called in the constructor. + * + * @return void + */ + protected function setProperties() + { + $importPluginProperties = new ImportPluginProperties(); + $importPluginProperties->setText('SQL'); + $importPluginProperties->setExtension('sql'); + $importPluginProperties->setOptionsText(__('Options')); + + $compats = $GLOBALS['dbi']->getCompatibilities(); + if (count($compats) > 0) { + $values = []; + foreach ($compats as $val) { + $values[$val] = $val; + } + + // create the root group that will be the options field for + // $importPluginProperties + // this will be shown as "Format specific options" + $importSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new OptionsPropertyMainGroup("general_opts"); + // create primary items and add them to the group + $leaf = new SelectPropertyItem( + "compatibility", + __('SQL compatibility mode:') + ); + $leaf->setValues($values); + $leaf->setDoc( + [ + 'manual_MySQL_Database_Administration', + 'Server_SQL_mode', + ] + ); + $generalOptions->addProperty($leaf); + $leaf = new BoolPropertyItem( + "no_auto_value_on_zero", + __('Do not use AUTO_INCREMENT for zero values') + ); + $leaf->setDoc( + [ + 'manual_MySQL_Database_Administration', + 'Server_SQL_mode', + 'sqlmode_no_auto_value_on_zero', + ] + ); + $generalOptions->addProperty($leaf); + + // add the main group to the root group + $importSpecificOptions->addProperty($generalOptions); + // set the options for the import plugin property item + $importPluginProperties->setOptions($importSpecificOptions); + } + + $this->properties = $importPluginProperties; + } + + /** + * Handles the whole import logic + * + * @param array $sql_data 2-element array with sql data + * + * @return void + */ + public function doImport(array &$sql_data = []) + { + global $error, $timeout_passed; + + // Handle compatibility options. + $this->_setSQLMode($GLOBALS['dbi'], $_REQUEST); + + $bq = new BufferedQuery(); + if (isset($_POST['sql_delimiter'])) { + $bq->setDelimiter($_POST['sql_delimiter']); + } + + /** + * Will be set in Import::getNextChunk(). + * + * @global bool $GLOBALS ['finished'] + */ + $GLOBALS['finished'] = false; + + while ((! $error) && (! $timeout_passed)) { + // Getting the first statement, the remaining data and the last + // delimiter. + $statement = $bq->extract(); + + // If there is no full statement, we are looking for more data. + if (empty($statement)) { + // Importing new data. + $newData = $this->import->getNextChunk(); + + // Subtract data we didn't handle yet and stop processing. + if ($newData === false) { + $GLOBALS['offset'] -= mb_strlen($bq->query); + break; + } + + // Checking if the input buffer has finished. + if ($newData === true) { + $GLOBALS['finished'] = true; + break; + } + + // Convert CR (but not CRLF) to LF otherwise all queries may + // not get executed on some platforms. + $bq->query .= preg_replace("/\r($|[^\n])/", "\n$1", $newData); + + continue; + } + + // Executing the query. + $this->import->runQuery($statement, $statement, $sql_data); + } + + // Extracting remaining statements. + while (! $error && ! $timeout_passed && ! empty($bq->query)) { + $statement = $bq->extract(true); + if (! empty($statement)) { + $this->import->runQuery($statement, $statement, $sql_data); + } + } + + // Finishing. + $this->import->runQuery('', '', $sql_data); + } + + /** + * Handle compatibility options + * + * @param DatabaseInterface $dbi Database interface + * @param array $request Request array + * + * @return void + */ + private function _setSQLMode($dbi, array $request) + { + $sql_modes = []; + if (isset($request['sql_compatibility']) + && 'NONE' != $request['sql_compatibility'] + ) { + $sql_modes[] = $request['sql_compatibility']; + } + if (isset($request['sql_no_auto_value_on_zero'])) { + $sql_modes[] = 'NO_AUTO_VALUE_ON_ZERO'; + } + if (count($sql_modes) > 0) { + $dbi->tryQuery( + 'SET SQL_MODE="' . implode(',', $sql_modes) . '"' + ); + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportXml.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportXml.php new file mode 100644 index 0000000..833d4e4 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ImportXml.php @@ -0,0 +1,375 @@ +setProperties(); + } + + /** + * Sets the import plugin properties. + * Called in the constructor. + * + * @return void + */ + protected function setProperties() + { + $importPluginProperties = new ImportPluginProperties(); + $importPluginProperties->setText(__('XML')); + $importPluginProperties->setExtension('xml'); + $importPluginProperties->setMimeType('text/xml'); + $importPluginProperties->setOptions([]); + $importPluginProperties->setOptionsText(__('Options')); + + $this->properties = $importPluginProperties; + } + + /** + * Handles the whole import logic + * + * @param array $sql_data 2-element array with sql data + * + * @return void + */ + public function doImport(array &$sql_data = []) + { + global $error, $timeout_passed, $finished, $db; + + $i = 0; + $len = 0; + $buffer = ""; + + /** + * Read in the file via Import::getNextChunk so that + * it can process compressed files + */ + while (! ($finished && $i >= $len) && ! $error && ! $timeout_passed) { + $data = $this->import->getNextChunk(); + if ($data === false) { + /* subtract data we didn't handle yet and stop processing */ + $GLOBALS['offset'] -= strlen($buffer); + break; + } elseif ($data !== true) { + /* Append new data to buffer */ + $buffer .= $data; + unset($data); + } + } + + unset($data); + + /** + * Disable loading of external XML entities. + */ + libxml_disable_entity_loader(); + + /** + * Load the XML string + * + * The option LIBXML_COMPACT is specified because it can + * result in increased performance without the need to + * alter the code in any way. It's basically a freebee. + */ + $xml = @simplexml_load_string($buffer, "SimpleXMLElement", LIBXML_COMPACT); + + unset($buffer); + + /** + * The XML was malformed + */ + if ($xml === false) { + Message::error( + __( + 'The XML file specified was either malformed or incomplete.' + . ' Please correct the issue and try again.' + ) + ) + ->display(); + unset($xml); + $GLOBALS['finished'] = false; + + return; + } + + /** + * Table accumulator + */ + $tables = []; + /** + * Row accumulator + */ + $rows = []; + + /** + * Temp arrays + */ + $tempRow = []; + $tempCells = []; + + /** + * CREATE code included (by default: no) + */ + $struct_present = false; + + /** + * Analyze the data in each table + */ + $namespaces = $xml->getNamespaces(true); + + /** + * Get the database name, collation and charset + */ + $db_attr = $xml->children(isset($namespaces['pma']) ? $namespaces['pma'] : null) + ->{'structure_schemas'}->{'database'}; + + if ($db_attr instanceof SimpleXMLElement) { + $db_attr = $db_attr->attributes(); + $db_name = (string) $db_attr['name']; + $collation = (string) $db_attr['collation']; + $charset = (string) $db_attr['charset']; + } else { + /** + * If the structure section is not present + * get the database name from the data section + */ + $db_attr = $xml->children() + ->attributes(); + $db_name = (string) $db_attr['name']; + $collation = null; + $charset = null; + } + + /** + * The XML was malformed + */ + if ($db_name === null) { + Message::error( + __( + 'The XML file specified was either malformed or incomplete.' + . ' Please correct the issue and try again.' + ) + ) + ->display(); + unset($xml); + $GLOBALS['finished'] = false; + + return; + } + + /** + * Retrieve the structure information + */ + if (isset($namespaces['pma'])) { + /** + * Get structures for all tables + * + * @var SimpleXMLElement $struct + */ + $struct = $xml->children($namespaces['pma']); + + $create = []; + + /** @var SimpleXMLElement $val1 */ + foreach ($struct as $val1) { + /** @var SimpleXMLElement $val2 */ + foreach ($val1 as $val2) { + // Need to select the correct database for the creation of + // tables, views, triggers, etc. + /** + * @todo Generating a USE here blocks importing of a table + * into another database. + */ + $attrs = $val2->attributes(); + $create[] = "USE " + . Util::backquote( + $attrs["name"] + ); + + foreach ($val2 as $val3) { + /** + * Remove the extra cosmetic spacing + */ + $val3 = str_replace(" ", "", (string) $val3); + $create[] = $val3; + } + } + } + + $struct_present = true; + } + + /** + * Move down the XML tree to the actual data + */ + $xml = $xml->children() + ->children(); + + $data_present = false; + + /** + * Only attempt to analyze/collect data if there is data present + */ + if ($xml && @count($xml->children())) { + $data_present = true; + + /** + * Process all database content + */ + foreach ($xml as $v1) { + $tbl_attr = $v1->attributes(); + + $isInTables = false; + $num_tables = count($tables); + for ($i = 0; $i < $num_tables; ++$i) { + if (! strcmp($tables[$i][Import::TBL_NAME], (string) $tbl_attr['name'])) { + $isInTables = true; + break; + } + } + + if (! $isInTables) { + $tables[] = [(string) $tbl_attr['name']]; + } + + foreach ($v1 as $v2) { + $row_attr = $v2->attributes(); + if (! array_search((string) $row_attr['name'], $tempRow)) { + $tempRow[] = (string) $row_attr['name']; + } + $tempCells[] = (string) $v2; + } + + $rows[] = [ + (string) $tbl_attr['name'], + $tempRow, + $tempCells, + ]; + + $tempRow = []; + $tempCells = []; + } + + unset($tempRow); + unset($tempCells); + unset($xml); + + /** + * Bring accumulated rows into the corresponding table + */ + $num_tables = count($tables); + for ($i = 0; $i < $num_tables; ++$i) { + $num_rows = count($rows); + for ($j = 0; $j < $num_rows; ++$j) { + if (! strcmp($tables[$i][Import::TBL_NAME], $rows[$j][Import::TBL_NAME])) { + if (! isset($tables[$i][Import::COL_NAMES])) { + $tables[$i][] = $rows[$j][Import::COL_NAMES]; + } + + $tables[$i][Import::ROWS][] = $rows[$j][Import::ROWS]; + } + } + } + + unset($rows); + + if (! $struct_present) { + $analyses = []; + + $len = count($tables); + for ($i = 0; $i < $len; ++$i) { + $analyses[] = $this->import->analyzeTable($tables[$i]); + } + } + } + + unset($xml); + unset($tempCells); + unset($rows); + + /** + * Only build SQL from data if there is data present + */ + if ($data_present) { + /** + * Set values to NULL if they were not present + * to maintain Import::buildSql() call integrity + */ + if (! isset($analyses)) { + $analyses = null; + if (! $struct_present) { + $create = null; + } + } + } + + /** + * string $db_name (no backquotes) + * + * array $table = array(table_name, array() column_names, array()() rows) + * array $tables = array of "$table"s + * + * array $analysis = array(array() column_types, array() column_sizes) + * array $analyses = array of "$analysis"s + * + * array $create = array of SQL strings + * + * array $options = an associative array of options + */ + + /* Set database name to the currently selected one, if applicable */ + if (strlen((string) $db)) { + /* Override the database name in the XML file, if one is selected */ + $db_name = $db; + $options = ['create_db' => false]; + } else { + if ($db_name === null) { + $db_name = 'XML_DB'; + } + + /* Set database collation/charset */ + $options = [ + 'db_collation' => $collation, + 'db_charset' => $charset, + ]; + } + + /* Created and execute necessary SQL statements from data */ + $this->import->buildSql($db_name, $tables, $analyses, $create, $options, $sql_data); + + unset($analyses); + unset($tables); + unset($create); + + /* Commit any possible data in buffers */ + $this->import->runQuery('', '', $sql_data); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/README b/srcs/phpmyadmin/libraries/classes/Plugins/Import/README new file mode 100644 index 0000000..e240556 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/README @@ -0,0 +1,156 @@ +This directory holds import plugins for phpMyAdmin. Any new plugin should +basically follow the structure presented here. The messages must use our +gettext mechanism, see https://wiki.phpmyadmin.net/pma/Gettext_for_developers. + +setProperties(); + } + + /** + * Sets the import plugin properties. + * Called in the constructor. + * + * @return void + */ + protected function setProperties() + { + $importPluginProperties = new PhpMyAdmin\Properties\Plugins\ImportPluginProperties(); + $importPluginProperties->setText('[name]'); // the name of your plug-in + $importPluginProperties->setExtension('[ext]'); // extension this plug-in can handle + $importPluginProperties->setOptionsText(__('Options')); + + // create the root group that will be the options field for + // $importPluginProperties + // this will be shown as "Format specific options" + $importSpecificOptions = new + PhpMyAdmin\Properties\Options\Groups\OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // general options main group + $generalOptions = new PhpMyAdmin\Properties\Options\Groups\OptionsPropertyMainGroup( + "general_opts" + ); + + // optional : + // create primary items and add them to the group + // type - one of the classes listed in libraries/properties/options/items/ + // name - form element name + // text - description in GUI + // size - size of text element + // len - maximal size of input + // values - possible values of the item + $leaf = new PhpMyAdmin\Properties\Options\Items\RadioPropertyItem( + "structure_or_data" + ); + $leaf->setValues( + array( + 'structure' => __('structure'), + 'data' => __('data'), + 'structure_and_data' => __('structure and data') + ) + ); + $generalOptions->addProperty($leaf); + + // add the main group to the root group + $importSpecificOptions->addProperty($generalOptions); + + // set the options for the import plugin property item + $importPluginProperties->setOptions($importSpecificOptions); + $this->properties = $importPluginProperties; + } + + /** + * Handles the whole import logic + * + * @param array &$sql_data 2-element array with sql data + * + * @return void + */ + public function doImport(&$sql_data = array()) + { + // get globals (others are optional) + global $error, $timeout_passed, $finished; + + $buffer = ''; + while (! ($finished && $i >= $len) && ! $error && ! $timeout_passed) { + $data = $this->import->getNextChunk(); + if ($data === false) { + // subtract data we didn't handle yet and stop processing + $GLOBALS['offset'] -= strlen($buffer); + break; + } elseif ($data === true) { + // Handle rest of buffer + } else { + // Append new data to buffer + $buffer .= $data; + } + // PARSE $buffer here, post sql queries using: + $this->import->runQuery($sql, $verbose_sql_with_comments, $sql_data); + } // End of import loop + // Commit any possible data in buffers + $this->import->runQuery('', '', $sql_data); + } + + + // optional: + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + + /** + * Getter description + * + * @return type + */ + private function _getMyOptionalVariable() + { + return $this->_myOptionalVariable; + } + + /** + * Setter description + * + * @param type $my_optional_variable description + * + * @return void + */ + private function _setMyOptionalVariable($my_optional_variable) + { + $this->_myOptionalVariable = $my_optional_variable; + } +} +?> diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/ShapeFileImport.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ShapeFileImport.php new file mode 100644 index 0000000..8af3f90 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/ShapeFileImport.php @@ -0,0 +1,46 @@ + $id, + 'finished' => false, + 'percent' => 0, + 'total' => 0, + 'complete' => 0, + 'plugin' => UploadApc::getIdKey(), + ]; + } + $ret = $_SESSION[$SESSION_KEY][$id]; + + if (! ImportAjax::apcCheck() || $ret['finished']) { + return $ret; + } + $status = apc_fetch('upload_' . $id); + + if ($status) { + $ret['finished'] = (bool) $status['done']; + $ret['total'] = $status['total']; + $ret['complete'] = $status['current']; + + if ($ret['total'] > 0) { + $ret['percent'] = $ret['complete'] / $ret['total'] * 100; + } + + if ($ret['percent'] == 100) { + $ret['finished'] = (bool) true; + } + + $_SESSION[$SESSION_KEY][$id] = $ret; + } + + return $ret; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadNoplugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadNoplugin.php new file mode 100644 index 0000000..087b1e1 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadNoplugin.php @@ -0,0 +1,60 @@ + $id, + 'finished' => false, + 'percent' => 0, + 'total' => 0, + 'complete' => 0, + 'plugin' => UploadNoplugin::getIdKey(), + ]; + } + return $_SESSION[$SESSION_KEY][$id]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadProgress.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadProgress.php new file mode 100644 index 0000000..13578e0 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadProgress.php @@ -0,0 +1,97 @@ + $id, + 'finished' => false, + 'percent' => 0, + 'total' => 0, + 'complete' => 0, + 'plugin' => UploadProgress::getIdKey(), + ]; + } + $ret = $_SESSION[$SESSION_KEY][$id]; + + if (! ImportAjax::progressCheck() || $ret['finished']) { + return $ret; + } + + $status = null; + if (function_exists('uploadprogress_get_info')) { + $status = uploadprogress_get_info($id); + } + + if ($status) { + if ($status['bytes_uploaded'] == $status['bytes_total']) { + $ret['finished'] = true; + } else { + $ret['finished'] = false; + } + $ret['total'] = $status['bytes_total']; + $ret['complete'] = $status['bytes_uploaded']; + + if ($ret['total'] > 0) { + $ret['percent'] = $ret['complete'] / $ret['total'] * 100; + } + } else { + $ret = [ + 'id' => $id, + 'finished' => true, + 'percent' => 100, + 'total' => $ret['total'], + 'complete' => $ret['total'], + 'plugin' => UploadProgress::getIdKey(), + ]; + } + + $_SESSION[$SESSION_KEY][$id] = $ret; + + return $ret; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadSession.php b/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadSession.php new file mode 100644 index 0000000..133b11a --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Import/Upload/UploadSession.php @@ -0,0 +1,95 @@ + $id, + 'finished' => false, + 'percent' => 0, + 'total' => 0, + 'complete' => 0, + 'plugin' => UploadSession::getIdKey(), + ]; + } + $ret = $_SESSION[$SESSION_KEY][$id]; + + if (! ImportAjax::sessionCheck() || $ret['finished']) { + return $ret; + } + + $status = false; + $sessionkey = ini_get('session.upload_progress.prefix') . $id; + + if (isset($_SESSION[$sessionkey])) { + $status = $_SESSION[$sessionkey]; + } + + if ($status) { + $ret['finished'] = $status['done']; + $ret['total'] = $status['content_length']; + $ret['complete'] = $status['bytes_processed']; + + if ($ret['total'] > 0) { + $ret['percent'] = $ret['complete'] / $ret['total'] * 100; + } + } else { + $ret = [ + 'id' => $id, + 'finished' => true, + 'percent' => 100, + 'total' => $ret['total'], + 'complete' => $ret['total'], + 'plugin' => UploadSession::getIdKey(), + ]; + } + + $_SESSION[$SESSION_KEY][$id] = $ret; + + return $ret; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/ImportPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/ImportPlugin.php new file mode 100644 index 0000000..788bd42 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/ImportPlugin.php @@ -0,0 +1,96 @@ +import = new Import(); + } + + /** + * Handles the whole import logic + * + * @param array $sql_data 2-element array with sql data + * + * @return void + */ + abstract public function doImport(array &$sql_data = []); + + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the import specific format plugin properties + * + * @return ImportPluginProperties + */ + public function getProperties() + { + return $this->properties; + } + + /** + * Sets the export plugins properties and is implemented by each import + * plugin + * + * @return void + */ + abstract protected function setProperties(); + + /** + * Define DB name and options + * + * @param string $currentDb DB + * @param string $defaultDb Default DB name + * + * @return array DB name and options (an associative array of options) + */ + protected function getDbnameAndOptions($currentDb, $defaultDb) + { + if (strlen((string) $currentDb) > 0) { + $db_name = $currentDb; + $options = ['create_db' => false]; + } else { + $db_name = $defaultDb; + $options = null; + } + + return [ + $db_name, + $options, + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Dia/Dia.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Dia/Dia.php new file mode 100644 index 0000000..b9941a5 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Dia/Dia.php @@ -0,0 +1,190 @@ +openMemory(); + /* + * Set indenting using three spaces, + * so output is formatted + */ + $this->setIndent(true); + $this->setIndentString(' '); + /* + * Create the XML document + */ + $this->startDocument('1.0', 'UTF-8'); + } + + /** + * Starts Dia Document + * + * dia document starts by first initializing dia:diagram tag + * then dia:diagramdata contains all the attributes that needed + * to define the document, then finally a Layer starts which + * holds all the objects. + * + * @param string $paper the size of the paper/document + * @param float $topMargin top margin of the paper/document in cm + * @param float $bottomMargin bottom margin of the paper/document in cm + * @param float $leftMargin left margin of the paper/document in cm + * @param float $rightMargin right margin of the paper/document in cm + * @param string $orientation orientation of the document, portrait or landscape + * + * @return void + * + * @access public + * @see XMLWriter::startElement(),XMLWriter::writeAttribute(), + * XMLWriter::writeRaw() + */ + public function startDiaDoc( + $paper, + $topMargin, + $bottomMargin, + $leftMargin, + $rightMargin, + $orientation + ) { + if ($orientation == 'P') { + $isPortrait = 'true'; + } else { + $isPortrait = 'false'; + } + $this->startElement('dia:diagram'); + $this->writeAttribute('xmlns:dia', 'http://www.lysator.liu.se/~alla/dia/'); + $this->startElement('dia:diagramdata'); + $this->writeRaw( + ' + + + + + + + + + #' . $paper . '# + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ' + ); + $this->endElement(); + $this->startElement('dia:layer'); + $this->writeAttribute('name', 'Background'); + $this->writeAttribute('visible', 'true'); + $this->writeAttribute('active', 'true'); + } + + /** + * Ends Dia Document + * + * @return void + * @access public + * @see XMLWriter::endElement(),XMLWriter::endDocument() + */ + public function endDiaDoc() + { + $this->endElement(); + $this->endDocument(); + } + + /** + * Output Dia Document for download + * + * @param string $fileName name of the dia document + * + * @return void + * @access public + * @see XMLWriter::flush() + */ + public function showOutput($fileName) + { + if (ob_get_clean()) { + ob_end_clean(); + } + $output = $this->flush(); + Response::getInstance()->disable(); + Core::downloadHeader( + $fileName, + 'application/x-dia-diagram', + strlen($output) + ); + print $output; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Dia/DiaRelationSchema.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Dia/DiaRelationSchema.php new file mode 100644 index 0000000..ee04f0d --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Dia/DiaRelationSchema.php @@ -0,0 +1,238 @@ +setShowColor(isset($_REQUEST['dia_show_color'])); + $this->setShowKeys(isset($_REQUEST['dia_show_keys'])); + $this->setOrientation($_REQUEST['dia_orientation']); + $this->setPaper($_REQUEST['dia_paper']); + + $this->diagram->startDiaDoc( + $this->paper, + $this->_topMargin, + $this->_bottomMargin, + $this->_leftMargin, + $this->_rightMargin, + $this->orientation + ); + + $alltables = $this->getTablesFromRequest(); + + foreach ($alltables as $table) { + if (! isset($this->_tables[$table])) { + $this->_tables[$table] = new TableStatsDia( + $this->diagram, + $this->db, + $table, + $this->pageNumber, + $this->showKeys, + $this->offline + ); + } + } + + $seen_a_relation = false; + foreach ($alltables as $one_table) { + $exist_rel = $this->relation->getForeigners($this->db, $one_table, '', 'both'); + if (! $exist_rel) { + continue; + } + + $seen_a_relation = true; + foreach ($exist_rel as $master_field => $rel) { + /* put the foreign table on the schema only if selected + * by the user + * (do not use array_search() because we would have to + * to do a === false and this is not PHP3 compatible) + */ + if ($master_field != 'foreign_keys_data') { + if (in_array($rel['foreign_table'], $alltables)) { + $this->_addRelation( + $one_table, + $master_field, + $rel['foreign_table'], + $rel['foreign_field'], + $this->showKeys + ); + } + continue; + } + + foreach ($rel as $one_key) { + if (! in_array($one_key['ref_table_name'], $alltables)) { + continue; + } + + foreach ($one_key['index_list'] as $index => $one_field) { + $this->_addRelation( + $one_table, + $one_field, + $one_key['ref_table_name'], + $one_key['ref_index_list'][$index], + $this->showKeys + ); + } + } + } + } + $this->_drawTables(); + + if ($seen_a_relation) { + $this->_drawRelations(); + } + $this->diagram->endDiaDoc(); + } + + /** + * Output Dia Document for download + * + * @return void + * @access public + */ + public function showOutput() + { + $this->diagram->showOutput($this->getFileName('.dia')); + } + + /** + * Defines relation objects + * + * @param string $masterTable The master table name + * @param string $masterField The relation field in the master table + * @param string $foreignTable The foreign table name + * @param string $foreignField The relation field in the foreign table + * @param bool $showKeys Whether to display ONLY keys or not + * + * @return void + * + * @access private + * @see TableStatsDia::__construct(),RelationStatsDia::__construct() + */ + private function _addRelation( + $masterTable, + $masterField, + $foreignTable, + $foreignField, + $showKeys + ) { + if (! isset($this->_tables[$masterTable])) { + $this->_tables[$masterTable] = new TableStatsDia( + $this->diagram, + $this->db, + $masterTable, + $this->pageNumber, + $showKeys + ); + } + if (! isset($this->_tables[$foreignTable])) { + $this->_tables[$foreignTable] = new TableStatsDia( + $this->diagram, + $this->db, + $foreignTable, + $this->pageNumber, + $showKeys + ); + } + $this->_relations[] = new RelationStatsDia( + $this->diagram, + $this->_tables[$masterTable], + $masterField, + $this->_tables[$foreignTable], + $foreignField + ); + } + + /** + * Draws relation references + * + * connects master table's master field to + * foreign table's foreign field using Dia object + * type Database - Reference + * + * @return void + * + * @access private + * @see RelationStatsDia::relationDraw() + */ + private function _drawRelations() + { + foreach ($this->_relations as $relation) { + $relation->relationDraw($this->showColor); + } + } + + /** + * Draws tables + * + * Tables are generated using Dia object type Database - Table + * primary fields are underlined and bold in tables + * + * @return void + * + * @access private + * @see TableStatsDia::tableDraw() + */ + private function _drawTables() + { + foreach ($this->_tables as $table) { + $table->tableDraw($this->showColor); + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Dia/RelationStatsDia.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Dia/RelationStatsDia.php new file mode 100644 index 0000000..bd44532 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Dia/RelationStatsDia.php @@ -0,0 +1,228 @@ +diagram = $diagram; + $src_pos = $this->_getXy($master_table, $master_field); + $dest_pos = $this->_getXy($foreign_table, $foreign_field); + $this->srcConnPointsLeft = $src_pos[0]; + $this->srcConnPointsRight = $src_pos[1]; + $this->destConnPointsLeft = $dest_pos[0]; + $this->destConnPointsRight = $dest_pos[1]; + $this->masterTablePos = $src_pos[2]; + $this->foreignTablePos = $dest_pos[2]; + $this->masterTableId = $master_table->tableId; + $this->foreignTableId = $foreign_table->tableId; + } + + /** + * Each Table object have connection points + * which is used to connect to other objects in Dia + * we detect the position of key in fields and + * then determines its left and right connection + * points. + * + * @param TableStatsDia $table The current table name + * @param string $column The relation column name + * + * @return array Table right,left connection points and key position + * + * @access private + */ + private function _getXy($table, $column) + { + $pos = array_search($column, $table->fields); + // left, right, position + $value = 12; + if ($pos != 0) { + return [ + $pos + $value + $pos, + $pos + $value + $pos + 1, + $pos, + ]; + } + return [ + $pos + $value, + $pos + $value + 1, + $pos, + ]; + } + + /** + * Draws relation references + * + * connects master table's master field to foreign table's + * foreign field using Dia object type Database - Reference + * Dia object is used to generate the XML of Dia Document. + * Database reference Object and their attributes are involved + * in the combination of displaying Database - reference on Dia Document. + * + * @param boolean $showColor Whether to use one color per relation or not + * if showColor is true then an array of $listOfColors + * will be used to choose the random colors for + * references lines. we can change/add more colors to + * this + * + * @return boolean|void + * + * @access public + * @see PDF + */ + public function relationDraw($showColor) + { + ++DiaRelationSchema::$objectId; + /* + * if source connection points and destination connection + * points are same then return it false and don't draw that + * relation + */ + if ($this->srcConnPointsRight == $this->destConnPointsRight) { + if ($this->srcConnPointsLeft == $this->destConnPointsLeft) { + return false; + } + } + + if ($showColor) { + $listOfColors = [ + 'FF0000', + '000099', + '00FF00', + ]; + shuffle($listOfColors); + $this->referenceColor = '#' . $listOfColors[0] . ''; + } else { + $this->referenceColor = '#000000'; + } + + $this->diagram->writeRaw( + ' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #1# + + + #n# + + + + + + + + + + + + ' + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Dia/TableStatsDia.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Dia/TableStatsDia.php new file mode 100644 index 0000000..b3ae5a0 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Dia/TableStatsDia.php @@ -0,0 +1,231 @@ +tableId = ++DiaRelationSchema::$objectId; + } + + /** + * Displays an error when the table cannot be found. + * + * @return void + */ + protected function showMissingTableError() + { + ExportRelationSchema::dieSchema( + $this->pageNumber, + "DIA", + sprintf(__('The %s table doesn\'t exist!'), $this->tableName) + ); + } + + /** + * Do draw the table + * + * Tables are generated using object type Database - Table + * primary fields are underlined in tables. Dia object + * is used to generate the XML of Dia Document. Database Table + * Object and their attributes are involved in the combination + * of displaying Database - Table on Dia Document. + * + * @param boolean $showColor Whether to show color for tables text or not + * if showColor is true then an array of $listOfColors + * will be used to choose the random colors for tables + * text we can change/add more colors to this array + * + * @return void + * + * @access public + * @see Dia + */ + public function tableDraw($showColor) + { + if ($showColor) { + $listOfColors = [ + 'FF0000', + '000099', + '00FF00', + ]; + shuffle($listOfColors); + $this->tableColor = '#' . $listOfColors[0] . ''; + } else { + $this->tableColor = '#000000'; + } + + $factor = 0.1; + + $this->diagram->startElement('dia:object'); + $this->diagram->writeAttribute('type', 'Database - Table'); + $this->diagram->writeAttribute('version', '0'); + $this->diagram->writeAttribute('id', '' . $this->tableId . ''); + $this->diagram->writeRaw( + ' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #' . $this->tableName . '# + + + ## + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ' + ); + + $this->diagram->startElement('dia:attribute'); + $this->diagram->writeAttribute('name', 'attributes'); + + foreach ($this->fields as $field) { + $this->diagram->writeRaw( + ' + + #' . $field . '# + + + ## + + + ## + ' + ); + unset($pm); + $pm = 'false'; + if (in_array($field, $this->primary)) { + $pm = 'true'; + } + if ($field == $this->displayfield) { + $pm = 'false'; + } + $this->diagram->writeRaw( + ' + + + + + + + + + ' + ); + } + $this->diagram->endElement(); + $this->diagram->endElement(); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Eps/Eps.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Eps/Eps.php new file mode 100644 index 0000000..0679709 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Eps/Eps.php @@ -0,0 +1,280 @@ +stringCommands = ""; + $this->stringCommands .= "%!PS-Adobe-3.0 EPSF-3.0 \n"; + } + + /** + * Set document title + * + * @param string $value sets the title text + * + * @return void + */ + public function setTitle($value) + { + $this->stringCommands .= '%%Title: ' . $value . "\n"; + } + + /** + * Set document author + * + * @param string $value sets the author + * + * @return void + */ + public function setAuthor($value) + { + $this->stringCommands .= '%%Creator: ' . $value . "\n"; + } + + /** + * Set document creation date + * + * @param string $value sets the date + * + * @return void + */ + public function setDate($value) + { + $this->stringCommands .= '%%CreationDate: ' . $value . "\n"; + } + + /** + * Set document orientation + * + * @param string $orientation sets the orientation + * + * @return void + */ + public function setOrientation($orientation) + { + $this->stringCommands .= "%%PageOrder: Ascend \n"; + if ($orientation == "L") { + $orientation = "Landscape"; + $this->stringCommands .= '%%Orientation: ' . $orientation . "\n"; + } else { + $orientation = "Portrait"; + $this->stringCommands .= '%%Orientation: ' . $orientation . "\n"; + } + $this->stringCommands .= "%%EndComments \n"; + $this->stringCommands .= "%%Pages 1 \n"; + $this->stringCommands .= "%%BoundingBox: 72 150 144 170 \n"; + } + + /** + * Set the font and size + * + * font can be set whenever needed in EPS + * + * @param string $value sets the font name e.g Arial + * @param integer $size sets the size of the font e.g 10 + * + * @return void + */ + public function setFont($value, $size) + { + $this->font = $value; + $this->fontSize = $size; + $this->stringCommands .= "/" . $value . " findfont % Get the basic font\n"; + $this->stringCommands .= "" + . $size . " scalefont % Scale the font to $size points\n"; + $this->stringCommands + .= "setfont % Make it the current font\n"; + } + + /** + * Get the font + * + * @return string return the font name e.g Arial + */ + public function getFont() + { + return $this->font; + } + + /** + * Get the font Size + * + * @return string return the size of the font e.g 10 + */ + public function getFontSize() + { + return $this->fontSize; + } + + /** + * Draw the line + * + * drawing the lines from x,y source to x,y destination and set the + * width of the line. lines helps in showing relationships of tables + * + * @param integer $x_from The x_from attribute defines the start + * left position of the element + * @param integer $y_from The y_from attribute defines the start + * right position of the element + * @param integer $x_to The x_to attribute defines the end + * left position of the element + * @param integer $y_to The y_to attribute defines the end + * right position of the element + * @param integer $lineWidth Sets the width of the line e.g 2 + * + * @return void + */ + public function line( + $x_from = 0, + $y_from = 0, + $x_to = 0, + $y_to = 0, + $lineWidth = 0 + ) { + $this->stringCommands .= $lineWidth . " setlinewidth \n"; + $this->stringCommands .= $x_from . ' ' . $y_from . " moveto \n"; + $this->stringCommands .= $x_to . ' ' . $y_to . " lineto \n"; + $this->stringCommands .= "stroke \n"; + } + + /** + * Draw the rectangle + * + * drawing the rectangle from x,y source to x,y destination and set the + * width of the line. rectangles drawn around the text shown of fields + * + * @param integer $x_from The x_from attribute defines the start + * left position of the element + * @param integer $y_from The y_from attribute defines the start + * right position of the element + * @param integer $x_to The x_to attribute defines the end + * left position of the element + * @param integer $y_to The y_to attribute defines the end + * right position of the element + * @param integer $lineWidth Sets the width of the line e.g 2 + * + * @return void + */ + public function rect($x_from, $y_from, $x_to, $y_to, $lineWidth) + { + $this->stringCommands .= $lineWidth . " setlinewidth \n"; + $this->stringCommands .= "newpath \n"; + $this->stringCommands .= $x_from . " " . $y_from . " moveto \n"; + $this->stringCommands .= "0 " . $y_to . " rlineto \n"; + $this->stringCommands .= $x_to . " 0 rlineto \n"; + $this->stringCommands .= "0 -" . $y_to . " rlineto \n"; + $this->stringCommands .= "closepath \n"; + $this->stringCommands .= "stroke \n"; + } + + /** + * Set the current point + * + * The moveto operator takes two numbers off the stack and treats + * them as x and y coordinates to which to move. The coordinates + * specified become the current point. + * + * @param integer $x The x attribute defines the left position of the element + * @param integer $y The y attribute defines the right position of the element + * + * @return void + */ + public function moveTo($x, $y) + { + $this->stringCommands .= $x . ' ' . $y . " moveto \n"; + } + + /** + * Output/Display the text + * + * @param string $text The string to be displayed + * + * @return void + */ + public function show($text) + { + $this->stringCommands .= '(' . $text . ") show \n"; + } + + /** + * Output the text at specified co-ordinates + * + * @param string $text String to be displayed + * @param integer $x X attribute defines the left position of the element + * @param integer $y Y attribute defines the right position of the element + * + * @return void + */ + public function showXY($text, $x, $y) + { + $this->moveTo($x, $y); + $this->show($text); + } + + /** + * Ends EPS Document + * + * @return void + */ + public function endEpsDoc() + { + $this->stringCommands .= "showpage \n"; + } + + /** + * Output EPS Document for download + * + * @param string $fileName name of the eps document + * + * @return void + */ + public function showOutput($fileName) + { + // if(ob_get_clean()){ + //ob_end_clean(); + //} + $output = $this->stringCommands; + Response::getInstance() + ->disable(); + Core::downloadHeader( + $fileName, + 'image/x-eps', + strlen($output) + ); + print $output; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Eps/EpsRelationSchema.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Eps/EpsRelationSchema.php new file mode 100644 index 0000000..238af1b --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Eps/EpsRelationSchema.php @@ -0,0 +1,254 @@ +setShowColor(isset($_REQUEST['eps_show_color'])); + $this->setShowKeys(isset($_REQUEST['eps_show_keys'])); + $this->setTableDimension(isset($_REQUEST['eps_show_table_dimension'])); + $this->setAllTablesSameWidth(isset($_REQUEST['eps_all_tables_same_width'])); + $this->setOrientation($_REQUEST['eps_orientation']); + + $this->diagram->setTitle( + sprintf( + __('Schema of the %s database - Page %s'), + $this->db, + $this->pageNumber + ) + ); + $this->diagram->setAuthor('phpMyAdmin ' . PMA_VERSION); + $this->diagram->setDate(date("j F Y, g:i a")); + $this->diagram->setOrientation($this->orientation); + $this->diagram->setFont('Verdana', '10'); + + $alltables = $this->getTablesFromRequest(); + + foreach ($alltables as $table) { + if (! isset($this->_tables[$table])) { + $this->_tables[$table] = new TableStatsEps( + $this->diagram, + $this->db, + $table, + $this->diagram->getFont(), + $this->diagram->getFontSize(), + $this->pageNumber, + $this->_tablewidth, + $this->showKeys, + $this->tableDimension, + $this->offline + ); + } + + if ($this->sameWide) { + $this->_tables[$table]->width = $this->_tablewidth; + } + } + + $seen_a_relation = false; + foreach ($alltables as $one_table) { + $exist_rel = $this->relation->getForeigners($this->db, $one_table, '', 'both'); + if (! $exist_rel) { + continue; + } + + $seen_a_relation = true; + foreach ($exist_rel as $master_field => $rel) { + /* put the foreign table on the schema only if selected + * by the user + * (do not use array_search() because we would have to + * to do a === false and this is not PHP3 compatible) + */ + if ($master_field != 'foreign_keys_data') { + if (in_array($rel['foreign_table'], $alltables)) { + $this->_addRelation( + $one_table, + $this->diagram->getFont(), + $this->diagram->getFontSize(), + $master_field, + $rel['foreign_table'], + $rel['foreign_field'], + $this->tableDimension + ); + } + continue; + } + + foreach ($rel as $one_key) { + if (! in_array($one_key['ref_table_name'], $alltables)) { + continue; + } + + foreach ($one_key['index_list'] as $index => $one_field) { + $this->_addRelation( + $one_table, + $this->diagram->getFont(), + $this->diagram->getFontSize(), + $one_field, + $one_key['ref_table_name'], + $one_key['ref_index_list'][$index], + $this->tableDimension + ); + } + } + } + } + if ($seen_a_relation) { + $this->_drawRelations(); + } + + $this->_drawTables(); + $this->diagram->endEpsDoc(); + } + + /** + * Output Eps Document for download + * + * @return void + */ + public function showOutput() + { + $this->diagram->showOutput($this->getFileName('.eps')); + } + + /** + * Defines relation objects + * + * @param string $masterTable The master table name + * @param string $font The font + * @param int $fontSize The font size + * @param string $masterField The relation field in the master table + * @param string $foreignTable The foreign table name + * @param string $foreignField The relation field in the foreign table + * @param boolean $tableDimension Whether to display table position or not + * + * @return void + * + * @see _setMinMax,Table_Stats_Eps::__construct(), + * PhpMyAdmin\Plugins\Schema\Eps\RelationStatsEps::__construct() + */ + private function _addRelation( + $masterTable, + $font, + $fontSize, + $masterField, + $foreignTable, + $foreignField, + $tableDimension + ) { + if (! isset($this->_tables[$masterTable])) { + $this->_tables[$masterTable] = new TableStatsEps( + $this->diagram, + $this->db, + $masterTable, + $font, + $fontSize, + $this->pageNumber, + $this->_tablewidth, + false, + $tableDimension + ); + } + if (! isset($this->_tables[$foreignTable])) { + $this->_tables[$foreignTable] = new TableStatsEps( + $this->diagram, + $this->db, + $foreignTable, + $font, + $fontSize, + $this->pageNumber, + $this->_tablewidth, + false, + $tableDimension + ); + } + $this->_relations[] = new RelationStatsEps( + $this->diagram, + $this->_tables[$masterTable], + $masterField, + $this->_tables[$foreignTable], + $foreignField + ); + } + + /** + * Draws relation arrows and lines connects master table's master field to + * foreign table's foreign field + * + * @return void + * + * @see Relation_Stats_Eps::relationDraw() + */ + private function _drawRelations() + { + foreach ($this->_relations as $relation) { + $relation->relationDraw(); + } + } + + /** + * Draws tables + * + * @return void + * + * @see Table_Stats_Eps::Table_Stats_tableDraw() + */ + private function _drawTables() + { + foreach ($this->_tables as $table) { + $table->tableDraw($this->showColor); + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Eps/RelationStatsEps.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Eps/RelationStatsEps.php new file mode 100644 index 0000000..4c50131 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Eps/RelationStatsEps.php @@ -0,0 +1,120 @@ +wTick = 10; + parent::__construct( + $diagram, + $master_table, + $master_field, + $foreign_table, + $foreign_field + ); + $this->ySrc += 10; + $this->yDest += 10; + } + + /** + * draws relation links and arrows + * shows foreign key relations + * + * @see PMA_EPS + * + * @return void + */ + public function relationDraw() + { + // draw a line like -- to foreign field + $this->diagram->line( + $this->xSrc, + $this->ySrc, + $this->xSrc + $this->srcDir * $this->wTick, + $this->ySrc, + 1 + ); + // draw a line like -- to master field + $this->diagram->line( + $this->xDest + $this->destDir * $this->wTick, + $this->yDest, + $this->xDest, + $this->yDest, + 1 + ); + // draw a line that connects to master field line and foreign field line + $this->diagram->line( + $this->xSrc + $this->srcDir * $this->wTick, + $this->ySrc, + $this->xDest + $this->destDir * $this->wTick, + $this->yDest, + 1 + ); + $root2 = 2 * sqrt(2); + $this->diagram->line( + $this->xSrc + $this->srcDir * $this->wTick * 0.75, + $this->ySrc, + $this->xSrc + $this->srcDir * (0.75 - 1 / $root2) * $this->wTick, + $this->ySrc + $this->wTick / $root2, + 1 + ); + $this->diagram->line( + $this->xSrc + $this->srcDir * $this->wTick * 0.75, + $this->ySrc, + $this->xSrc + $this->srcDir * (0.75 - 1 / $root2) * $this->wTick, + $this->ySrc - $this->wTick / $root2, + 1 + ); + $this->diagram->line( + $this->xDest + $this->destDir * $this->wTick / 2, + $this->yDest, + $this->xDest + $this->destDir * (0.5 + 1 / $root2) * $this->wTick, + $this->yDest + $this->wTick / $root2, + 1 + ); + $this->diagram->line( + $this->xDest + $this->destDir * $this->wTick / 2, + $this->yDest, + $this->xDest + $this->destDir * (0.5 + 1 / $root2) * $this->wTick, + $this->yDest - $this->wTick / $root2, + 1 + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Eps/TableStatsEps.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Eps/TableStatsEps.php new file mode 100644 index 0000000..904d96a --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Eps/TableStatsEps.php @@ -0,0 +1,183 @@ +_setHeightTable($fontSize); + // setWidth must me after setHeight, because title + // can include table height which changes table width + $this->_setWidthTable($font, $fontSize); + if ($same_wide_width < $this->width) { + $same_wide_width = $this->width; + } + } + + /** + * Displays an error when the table cannot be found. + * + * @return void + */ + protected function showMissingTableError() + { + ExportRelationSchema::dieSchema( + $this->pageNumber, + "EPS", + sprintf(__('The %s table doesn\'t exist!'), $this->tableName) + ); + } + + /** + * Sets the width of the table + * + * @param string $font The font name + * @param integer $fontSize The font size + * + * @return void + * + * @see PMA_EPS + */ + private function _setWidthTable($font, $fontSize) + { + foreach ($this->fields as $field) { + $this->width = max( + $this->width, + $this->font->getStringWidth($field, $font, (int) $fontSize) + ); + } + $this->width += $this->font->getStringWidth( + ' ', + $font, + (int) $fontSize + ); + /* + * it is unknown what value must be added, because + * table title is affected by the table width value + */ + while ($this->width + < $this->font->getStringWidth( + $this->getTitle(), + $font, + (int) $fontSize + )) { + $this->width += 7; + } + } + + /** + * Sets the height of the table + * + * @param integer $fontSize The font size + * + * @return void + */ + private function _setHeightTable($fontSize) + { + $this->heightCell = $fontSize + 4; + $this->height = (count($this->fields) + 1) * $this->heightCell; + } + + /** + * Draw the table + * + * @param boolean $showColor Whether to display color + * + * @return void + * + * @see PMA_EPS,PMA_EPS::line,PMA_EPS::rect + */ + public function tableDraw($showColor) + { + $this->diagram->rect( + $this->x, + $this->y + 12, + $this->width, + $this->heightCell, + 1 + ); + $this->diagram->showXY($this->getTitle(), $this->x + 5, $this->y + 14); + foreach ($this->fields as $field) { + $this->currentCell += $this->heightCell; + $this->diagram->rect( + $this->x, + $this->y + 12 + $this->currentCell, + $this->width, + $this->heightCell, + 1 + ); + $this->diagram->showXY( + $field, + $this->x + 5, + $this->y + 14 + $this->currentCell + ); + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/ExportRelationSchema.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/ExportRelationSchema.php new file mode 100644 index 0000000..c5209c4 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/ExportRelationSchema.php @@ -0,0 +1,310 @@ +db = $db; + $this->diagram = $diagram; + $this->setPageNumber($_REQUEST['page_number']); + $this->setOffline(isset($_REQUEST['offline_export'])); + $this->relation = new Relation($GLOBALS['dbi']); + } + + /** + * Set Page Number + * + * @param integer $value Page Number of the document to be created + * + * @return void + */ + public function setPageNumber($value) + { + $this->pageNumber = intval($value); + } + + /** + * Returns the schema page number + * + * @return integer schema page number + */ + public function getPageNumber() + { + return $this->pageNumber; + } + + /** + * Sets showColor + * + * @param boolean $value whether to show colors + * + * @return void + */ + public function setShowColor($value) + { + $this->showColor = $value; + } + + /** + * Returns whether to show colors + * + * @return boolean whether to show colors + */ + public function isShowColor() + { + return $this->showColor; + } + + /** + * Set Table Dimension + * + * @param boolean $value show table co-ordinates or not + * + * @return void + */ + public function setTableDimension($value) + { + $this->tableDimension = $value; + } + + /** + * Returns whether to show table dimensions + * + * @return boolean whether to show table dimensions + */ + public function isTableDimension() + { + return $this->tableDimension; + } + + /** + * Set same width of All Tables + * + * @param boolean $value set same width of all tables or not + * + * @return void + */ + public function setAllTablesSameWidth($value) + { + $this->sameWide = $value; + } + + /** + * Returns whether to use same width for all tables or not + * + * @return boolean whether to use same width for all tables or not + */ + public function isAllTableSameWidth() + { + return $this->sameWide; + } + + /** + * Set Show only keys + * + * @param boolean $value show only keys or not + * + * @return void + * + * @access public + */ + public function setShowKeys($value) + { + $this->showKeys = $value; + } + + /** + * Returns whether to show keys + * + * @return boolean whether to show keys + */ + public function isShowKeys() + { + return $this->showKeys; + } + + /** + * Set Orientation + * + * @param string $value Orientation will be portrait or landscape + * + * @return void + * + * @access public + */ + public function setOrientation($value) + { + $this->orientation = $value == 'P' ? 'P' : 'L'; + } + + /** + * Returns orientation + * + * @return string orientation + */ + public function getOrientation() + { + return $this->orientation; + } + + /** + * Set type of paper + * + * @param string $value paper type can be A4 etc + * + * @return void + * + * @access public + */ + public function setPaper($value) + { + $this->paper = $value; + } + + /** + * Returns the paper size + * + * @return string paper size + */ + public function getPaper() + { + return $this->paper; + } + + /** + * Set whether the document is generated from client side DB + * + * @param boolean $value offline or not + * + * @return void + * + * @access public + */ + public function setOffline($value) + { + $this->offline = $value; + } + + /** + * Returns whether the client side database is used + * + * @return boolean + * + * @access public + */ + public function isOffline() + { + return $this->offline; + } + + /** + * Get the table names from the request + * + * @return array an array of table names + */ + protected function getTablesFromRequest() + { + $tables = []; + if (isset($_POST['t_tbl'])) { + foreach ($_POST['t_tbl'] as $table) { + $tables[] = rawurldecode($table); + } + } + return $tables; + } + + /** + * Returns the file name + * + * @param String $extension file extension + * + * @return string file name + */ + protected function getFileName($extension) + { + $filename = $this->db . $extension; + // Get the name of this page to use as filename + if ($this->pageNumber != -1 && ! $this->offline) { + $_name_sql = 'SELECT page_descr FROM ' + . Util::backquote($GLOBALS['cfgRelation']['db']) . '.' + . Util::backquote($GLOBALS['cfgRelation']['pdf_pages']) + . ' WHERE page_nr = ' . $this->pageNumber; + $_name_rs = $this->relation->queryAsControlUser($_name_sql); + $_name_row = $GLOBALS['dbi']->fetchRow($_name_rs); + $filename = $_name_row[0] . $extension; + } + + return $filename; + } + + /** + * Displays an error message + * + * @param integer $pageNumber ID of the chosen page + * @param string $type Schema Type + * @param string $error_message The error message + * + * @access public + * + * @return void + */ + public static function dieSchema($pageNumber, $type = '', $error_message = '') + { + echo "

    " , __("SCHEMA ERROR: ") , $type , "

    " , "\n"; + if (! empty($error_message)) { + $error_message = htmlspecialchars($error_message); + } + echo '

    ' , "\n"; + echo ' ' , $error_message , "\n"; + echo '

    ' , "\n"; + echo '' , __('Back') , ''; + echo "\n"; + exit; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Pdf/Pdf.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Pdf/Pdf.php new file mode 100644 index 0000000..e02a953 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Pdf/Pdf.php @@ -0,0 +1,422 @@ +_pageNumber = $pageNumber; + $this->_withDoc = $withDoc; + $this->_db = $db; + $this->relation = new Relation($GLOBALS['dbi']); + } + + /** + * Sets the value for margins + * + * @param float $c_margin margin + * + * @return void + */ + public function setCMargin($c_margin) + { + $this->cMargin = $c_margin; + } + + /** + * Sets the scaling factor, defines minimum coordinates and margins + * + * @param float|int $scale The scaling factor + * @param float|int $xMin The minimum X coordinate + * @param float|int $yMin The minimum Y coordinate + * @param float|int $leftMargin The left margin + * @param float|int $topMargin The top margin + * + * @return void + */ + public function setScale( + $scale = 1, + $xMin = 0, + $yMin = 0, + $leftMargin = -1, + $topMargin = -1 + ) { + $this->scale = $scale; + $this->_xMin = $xMin; + $this->_yMin = $yMin; + if ($this->leftMargin != -1) { + $this->leftMargin = $leftMargin; + } + if ($this->topMargin != -1) { + $this->topMargin = $topMargin; + } + } + + /** + * Outputs a scaled cell + * + * @param float|int $w The cell width + * @param float|int $h The cell height + * @param string $txt The text to output + * @param mixed $border Whether to add borders or not + * @param integer $ln Where to put the cursor once the output is done + * @param string $align Align mode + * @param integer $fill Whether to fill the cell with a color or not + * @param string $link Link + * + * @return void + * + * @see TCPDF::Cell() + */ + public function cellScale( + $w, + $h = 0, + $txt = '', + $border = 0, + $ln = 0, + $align = '', + $fill = 0, + $link = '' + ) { + $h /= $this->scale; + $w /= $this->scale; + $this->Cell($w, $h, $txt, $border, $ln, $align, $fill, $link); + } + + /** + * Draws a scaled line + * + * @param float $x1 The horizontal position of the starting point + * @param float $y1 The vertical position of the starting point + * @param float $x2 The horizontal position of the ending point + * @param float $y2 The vertical position of the ending point + * + * @return void + * + * @see TCPDF::Line() + */ + public function lineScale($x1, $y1, $x2, $y2) + { + $x1 = ($x1 - $this->_xMin) / $this->scale + $this->leftMargin; + $y1 = ($y1 - $this->_yMin) / $this->scale + $this->topMargin; + $x2 = ($x2 - $this->_xMin) / $this->scale + $this->leftMargin; + $y2 = ($y2 - $this->_yMin) / $this->scale + $this->topMargin; + $this->Line($x1, $y1, $x2, $y2); + } + + /** + * Sets x and y scaled positions + * + * @param float $x The x position + * @param float $y The y position + * + * @return void + * + * @see TCPDF::SetXY() + */ + public function setXyScale($x, $y) + { + $x = ($x - $this->_xMin) / $this->scale + $this->leftMargin; + $y = ($y - $this->_yMin) / $this->scale + $this->topMargin; + $this->SetXY($x, $y); + } + + /** + * Sets the X scaled positions + * + * @param float $x The x position + * + * @return void + * + * @see TCPDF::SetX() + */ + public function setXScale($x) + { + $x = ($x - $this->_xMin) / $this->scale + $this->leftMargin; + $this->SetX($x); + } + + /** + * Sets the scaled font size + * + * @param float $size The font size (in points) + * + * @return void + * + * @see TCPDF::SetFontSize() + */ + public function setFontSizeScale($size) + { + // Set font size in points + $size /= $this->scale; + $this->SetFontSize($size); + } + + /** + * Sets the scaled line width + * + * @param float $width The line width + * + * @return void + * + * @see TCPDF::SetLineWidth() + */ + public function setLineWidthScale($width) + { + $width /= $this->scale; + $this->SetLineWidth($width); + } + + /** + * This method is used to render the page header. + * + * @return void + * + * @see TCPDF::Header() + */ + // @codingStandardsIgnoreLine + public function Header() + { + // We only show this if we find something in the new pdf_pages table + + // This function must be named "Header" to work with the TCPDF library + if ($this->_withDoc) { + if ($this->_offline || $this->_pageNumber == -1) { + $pg_name = __("PDF export page"); + } else { + $test_query = 'SELECT * FROM ' + . Util::backquote($GLOBALS['cfgRelation']['db']) . '.' + . Util::backquote($GLOBALS['cfgRelation']['pdf_pages']) + . ' WHERE db_name = \'' . $GLOBALS['dbi']->escapeString($this->_db) + . '\' AND page_nr = \'' . $this->_pageNumber . '\''; + $test_rs = $this->relation->queryAsControlUser($test_query); + $pages = @$GLOBALS['dbi']->fetchAssoc($test_rs); + $pg_name = ucfirst($pages['page_descr']); + } + + $this->SetFont($this->_ff, 'B', 14); + $this->Cell(0, 6, $pg_name, 'B', 1, 'C'); + $this->SetFont($this->_ff, ''); + $this->Ln(); + } + } + + /** + * This function must be named "Footer" to work with the TCPDF library + * + * @return void + * + * @see PDF::Footer() + */ + // @codingStandardsIgnoreLine + public function Footer() + { + if ($this->_withDoc) { + parent::Footer(); + } + } + + /** + * Sets widths + * + * @param array $w array of widths + * + * @return void + */ + public function setWidths(array $w) + { + // column widths + $this->widths = $w; + } + + /** + * Generates table row. + * + * @param array $data Data for table + * @param array $links Links for table cells + * + * @return void + */ + public function row(array $data, array $links) + { + // line height + $nb = 0; + $data_cnt = count($data); + for ($i = 0; $i < $data_cnt; $i++) { + $nb = max($nb, $this->numLines($this->widths[$i], $data[$i])); + } + $il = $this->FontSize; + $h = ($il + 1) * $nb; + // page break if necessary + $this->checkPageBreak($h); + // draw the cells + $data_cnt = count($data); + for ($i = 0; $i < $data_cnt; $i++) { + $w = $this->widths[$i]; + // save current position + $x = $this->GetX(); + $y = $this->GetY(); + // draw the border + $this->Rect($x, $y, $w, $h); + if (isset($links[$i])) { + $this->Link($x, $y, $w, $h, $links[$i]); + } + // print text + $this->MultiCell($w, $il + 1, $data[$i], 0, 'L'); + // go to right side + $this->SetXY($x + $w, $y); + } + // go to line + $this->Ln($h); + } + + /** + * Compute number of lines used by a multicell of width w + * + * @param int $w width + * @param string $txt text + * + * @return int + */ + public function numLines($w, $txt) + { + $cw = &$this->CurrentFont['cw']; + if ($w == 0) { + $w = $this->w - $this->rMargin - $this->x; + } + $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; + $s = str_replace("\r", '', $txt); + $nb = strlen($s); + if ($nb > 0 && $s[$nb - 1] == "\n") { + $nb--; + } + $sep = -1; + $i = 0; + $j = 0; + $l = 0; + $nl = 1; + while ($i < $nb) { + $c = $s[$i]; + if ($c == "\n") { + $i++; + $sep = -1; + $j = $i; + $l = 0; + $nl++; + continue; + } + if ($c == ' ') { + $sep = $i; + } + $l += isset($cw[mb_ord($c)]) ? $cw[mb_ord($c)] : 0 ; + if ($l > $wmax) { + if ($sep == -1) { + if ($i == $j) { + $i++; + } + } else { + $i = $sep + 1; + } + $sep = -1; + $j = $i; + $l = 0; + $nl++; + } else { + $i++; + } + } + return $nl; + } + + /** + * Set whether the document is generated from client side DB + * + * @param string $value whether offline + * + * @return void + * + * @access private + */ + public function setOffline($value) + { + $this->_offline = $value; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Pdf/PdfRelationSchema.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Pdf/PdfRelationSchema.php new file mode 100644 index 0000000..fa67885 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Pdf/PdfRelationSchema.php @@ -0,0 +1,798 @@ +transformations = new Transformations(); + + $this->setShowGrid(isset($_REQUEST['pdf_show_grid'])); + $this->setShowColor(isset($_REQUEST['pdf_show_color'])); + $this->setShowKeys(isset($_REQUEST['pdf_show_keys'])); + $this->setTableDimension(isset($_REQUEST['pdf_show_table_dimension'])); + $this->setAllTablesSameWidth(isset($_REQUEST['pdf_all_tables_same_width'])); + $this->setWithDataDictionary(isset($_REQUEST['pdf_with_doc'])); + $this->setTableOrder($_REQUEST['pdf_table_order']); + $this->setOrientation($_REQUEST['pdf_orientation']); + $this->setPaper($_REQUEST['pdf_paper']); + + // Initializes a new document + parent::__construct( + $db, + new Pdf( + $this->orientation, + 'mm', + $this->paper, + $this->pageNumber, + $this->_withDoc, + $db + ) + ); + $this->diagram->SetTitle( + sprintf( + __('Schema of the %s database'), + $this->db + ) + ); + $this->diagram->setCMargin(0); + $this->diagram->Open(); + $this->diagram->SetAutoPageBreak('auto'); + $this->diagram->setOffline($this->offline); + + $alltables = $this->getTablesFromRequest(); + if ($this->getTableOrder() == 'name_asc') { + sort($alltables); + } elseif ($this->getTableOrder() == 'name_desc') { + rsort($alltables); + } + + if ($this->_withDoc) { + $this->diagram->SetAutoPageBreak('auto', 15); + $this->diagram->setCMargin(1); + $this->dataDictionaryDoc($alltables); + $this->diagram->SetAutoPageBreak('auto'); + $this->diagram->setCMargin(0); + } + + $this->diagram->AddPage(); + + if ($this->_withDoc) { + $this->diagram->SetLink($this->diagram->PMA_links['RT']['-'], -1); + $this->diagram->Bookmark(__('Relational schema')); + $this->diagram->setAlias('{00}', $this->diagram->PageNo()); + $this->_topMargin = 28; + $this->_bottomMargin = 28; + } + + /* snip */ + foreach ($alltables as $table) { + if (! isset($this->_tables[$table])) { + $this->_tables[$table] = new TableStatsPdf( + $this->diagram, + $this->db, + $table, + null, + $this->pageNumber, + $this->_tablewidth, + $this->showKeys, + $this->tableDimension, + $this->offline + ); + } + if ($this->sameWide) { + $this->_tables[$table]->width = $this->_tablewidth; + } + $this->_setMinMax($this->_tables[$table]); + } + + // Defines the scale factor + $innerWidth = $this->diagram->getPageWidth() - $this->_rightMargin + - $this->_leftMargin; + $innerHeight = $this->diagram->getPageHeight() - $this->_topMargin + - $this->_bottomMargin; + $this->_scale = ceil( + max( + ($this->_xMax - $this->_xMin) / $innerWidth, + ($this->_yMax - $this->_yMin) / $innerHeight + ) * 100 + ) / 100; + + $this->diagram->setScale( + $this->_scale, + $this->_xMin, + $this->_yMin, + $this->_leftMargin, + $this->_topMargin + ); + // Builds and save the PDF document + $this->diagram->setLineWidthScale(0.1); + + if ($this->_showGrid) { + $this->diagram->SetFontSize(10); + $this->_strokeGrid(); + } + $this->diagram->setFontSizeScale(14); + // previous logic was checking master tables and foreign tables + // but I think that looping on every table of the pdf page as a master + // and finding its foreigns is OK (then we can support innodb) + $seen_a_relation = false; + foreach ($alltables as $one_table) { + $exist_rel = $this->relation->getForeigners($this->db, $one_table, '', 'both'); + if (! $exist_rel) { + continue; + } + + $seen_a_relation = true; + foreach ($exist_rel as $master_field => $rel) { + // put the foreign table on the schema only if selected + // by the user + // (do not use array_search() because we would have to + // to do a === false and this is not PHP3 compatible) + if ($master_field != 'foreign_keys_data') { + if (in_array($rel['foreign_table'], $alltables)) { + $this->_addRelation( + $one_table, + $master_field, + $rel['foreign_table'], + $rel['foreign_field'] + ); + } + continue; + } + + foreach ($rel as $one_key) { + if (! in_array($one_key['ref_table_name'], $alltables)) { + continue; + } + + foreach ($one_key['index_list'] as $index => $one_field) { + $this->_addRelation( + $one_table, + $one_field, + $one_key['ref_table_name'], + $one_key['ref_index_list'][$index] + ); + } + } + } // end while + } // end while + + if ($seen_a_relation) { + $this->_drawRelations(); + } + $this->_drawTables(); + } + + /** + * Set Show Grid + * + * @param boolean $value show grid of the document or not + * + * @return void + */ + public function setShowGrid($value) + { + $this->_showGrid = $value; + } + + /** + * Returns whether to show grid + * + * @return boolean whether to show grid + */ + public function isShowGrid() + { + return $this->_showGrid; + } + + /** + * Set Data Dictionary + * + * @param boolean $value show selected database data dictionary or not + * + * @return void + */ + public function setWithDataDictionary($value) + { + $this->_withDoc = $value; + } + + /** + * Return whether to show selected database data dictionary or not + * + * @return boolean whether to show selected database data dictionary or not + */ + public function isWithDataDictionary() + { + return $this->_withDoc; + } + + /** + * Sets the order of the table in data dictionary + * + * @param string $value table order + * + * @return void + */ + public function setTableOrder($value) + { + $this->_tableOrder = $value; + } + + /** + * Returns the order of the table in data dictionary + * + * @return string table order + */ + public function getTableOrder() + { + return $this->_tableOrder; + } + + /** + * Output Pdf Document for download + * + * @return void + */ + public function showOutput() + { + $this->diagram->download($this->getFileName('.pdf')); + } + + /** + * Sets X and Y minimum and maximum for a table cell + * + * @param TableStatsPdf $table The table name of which sets XY co-ordinates + * + * @return void + */ + private function _setMinMax($table) + { + $this->_xMax = max($this->_xMax, $table->x + $table->width); + $this->_yMax = max($this->_yMax, $table->y + $table->height); + $this->_xMin = min($this->_xMin, $table->x); + $this->_yMin = min($this->_yMin, $table->y); + } + + /** + * Defines relation objects + * + * @param string $masterTable The master table name + * @param string $masterField The relation field in the master table + * @param string $foreignTable The foreign table name + * @param string $foreignField The relation field in the foreign table + * + * @return void + * + * @see _setMinMax + */ + private function _addRelation( + $masterTable, + $masterField, + $foreignTable, + $foreignField + ) { + if (! isset($this->_tables[$masterTable])) { + $this->_tables[$masterTable] = new TableStatsPdf( + $this->diagram, + $this->db, + $masterTable, + null, + $this->pageNumber, + $this->_tablewidth, + $this->showKeys, + $this->tableDimension + ); + $this->_setMinMax($this->_tables[$masterTable]); + } + if (! isset($this->_tables[$foreignTable])) { + $this->_tables[$foreignTable] = new TableStatsPdf( + $this->diagram, + $this->db, + $foreignTable, + null, + $this->pageNumber, + $this->_tablewidth, + $this->showKeys, + $this->tableDimension + ); + $this->_setMinMax($this->_tables[$foreignTable]); + } + $this->relations[] = new RelationStatsPdf( + $this->diagram, + $this->_tables[$masterTable], + $masterField, + $this->_tables[$foreignTable], + $foreignField + ); + } + + /** + * Draws the grid + * + * @return void + * + * @see PMA_Schema_PDF + */ + private function _strokeGrid() + { + $gridSize = 10; + $labelHeight = 4; + $labelWidth = 5; + if ($this->_withDoc) { + $topSpace = 6; + $bottomSpace = 15; + } else { + $topSpace = 0; + $bottomSpace = 0; + } + + $this->diagram->SetMargins(0, 0); + $this->diagram->SetDrawColor(200, 200, 200); + // Draws horizontal lines + $innerHeight = $this->diagram->getPageHeight() - $topSpace - $bottomSpace; + for ($l = 0, $size = intval($innerHeight / $gridSize); $l <= $size; $l++) { + $this->diagram->line( + 0, + $l * $gridSize + $topSpace, + $this->diagram->getPageWidth(), + $l * $gridSize + $topSpace + ); + // Avoid duplicates + if ($l > 0 + && $l <= intval(($innerHeight - $labelHeight) / $gridSize) + ) { + $this->diagram->SetXY(0, $l * $gridSize + $topSpace); + $label = (string) sprintf( + '%.0f', + ($l * $gridSize + $topSpace - $this->_topMargin) + * $this->_scale + $this->_yMin + ); + $this->diagram->Cell($labelWidth, $labelHeight, ' ' . $label); + } // end if + } // end for + // Draws vertical lines + for ($j = 0, $size = intval($this->diagram->getPageWidth() / $gridSize); $j <= $size; $j++) { + $this->diagram->line( + $j * $gridSize, + $topSpace, + $j * $gridSize, + $this->diagram->getPageHeight() - $bottomSpace + ); + $this->diagram->SetXY($j * $gridSize, $topSpace); + $label = (string) sprintf( + '%.0f', + ($j * $gridSize - $this->_leftMargin) * $this->_scale + $this->_xMin + ); + $this->diagram->Cell($labelWidth, $labelHeight, $label); + } + } + + /** + * Draws relation arrows + * + * @return void + * + * @see Relation_Stats_Pdf::relationdraw() + */ + private function _drawRelations() + { + $i = 0; + foreach ($this->relations as $relation) { + $relation->relationDraw($this->showColor, $i); + $i++; + } + } + + /** + * Draws tables + * + * @return void + * + * @see Table_Stats_Pdf::tableDraw() + */ + private function _drawTables() + { + foreach ($this->_tables as $table) { + $table->tableDraw(null, $this->_withDoc, $this->showColor); + } + } + + /** + * Generates data dictionary pages. + * + * @param array $alltables Tables to document. + * + * @return void + */ + public function dataDictionaryDoc(array $alltables) + { + // TOC + $this->diagram->AddPage($this->orientation); + $this->diagram->Cell(0, 9, __('Table of contents'), 1, 0, 'C'); + $this->diagram->Ln(15); + $i = 1; + foreach ($alltables as $table) { + $this->diagram->PMA_links['doc'][$table]['-'] + = $this->diagram->AddLink(); + $this->diagram->SetX(10); + // $this->diagram->Ln(1); + $this->diagram->Cell( + 0, + 6, + __('Page number:') . ' {' . sprintf("%02d", $i) . '}', + 0, + 0, + 'R', + 0, + $this->diagram->PMA_links['doc'][$table]['-'] + ); + $this->diagram->SetX(10); + $this->diagram->Cell( + 0, + 6, + $i . ' ' . $table, + 0, + 1, + 'L', + 0, + $this->diagram->PMA_links['doc'][$table]['-'] + ); + // $this->diagram->Ln(1); + $fields = $GLOBALS['dbi']->getColumns($this->db, $table); + foreach ($fields as $row) { + $this->diagram->SetX(20); + $field_name = $row['Field']; + $this->diagram->PMA_links['doc'][$table][$field_name] + = $this->diagram->AddLink(); + //$this->diagram->Cell( + // 0, 6, $field_name, 0, 1, + // 'L', 0, $this->diagram->PMA_links['doc'][$table][$field_name] + //); + } + $i++; + } + $this->diagram->PMA_links['RT']['-'] = $this->diagram->AddLink(); + $this->diagram->SetX(10); + $this->diagram->Cell( + 0, + 6, + __('Page number:') . ' {00}', + 0, + 0, + 'R', + 0, + $this->diagram->PMA_links['RT']['-'] + ); + $this->diagram->SetX(10); + $this->diagram->Cell( + 0, + 6, + $i . ' ' . __('Relational schema'), + 0, + 1, + 'L', + 0, + $this->diagram->PMA_links['RT']['-'] + ); + $z = 0; + foreach ($alltables as $table) { + $z++; + $this->diagram->SetAutoPageBreak(true, 15); + $this->diagram->AddPage($this->orientation); + $this->diagram->Bookmark($table); + $this->diagram->setAlias( + '{' . sprintf("%02d", $z) . '}', + $this->diagram->PageNo() + ); + $this->diagram->PMA_links['RT'][$table]['-'] + = $this->diagram->AddLink(); + $this->diagram->SetLink( + $this->diagram->PMA_links['doc'][$table]['-'], + -1 + ); + $this->diagram->SetFont($this->_ff, 'B', 18); + $this->diagram->Cell( + 0, + 8, + $z . ' ' . $table, + 1, + 1, + 'C', + 0, + $this->diagram->PMA_links['RT'][$table]['-'] + ); + $this->diagram->SetFont($this->_ff, '', 8); + $this->diagram->Ln(); + + $cfgRelation = $this->relation->getRelationsParam(); + $comments = $this->relation->getComments($this->db, $table); + if ($cfgRelation['mimework']) { + $mime_map = $this->transformations->getMime($this->db, $table, true); + } + + /** + * Gets table information + */ + $showtable = $GLOBALS['dbi']->getTable($this->db, $table) + ->getStatusInfo(); + $show_comment = isset($showtable['Comment']) + ? $showtable['Comment'] + : ''; + $create_time = isset($showtable['Create_time']) + ? Util::localisedDate( + strtotime($showtable['Create_time']) + ) + : ''; + $update_time = isset($showtable['Update_time']) + ? Util::localisedDate( + strtotime($showtable['Update_time']) + ) + : ''; + $check_time = isset($showtable['Check_time']) + ? Util::localisedDate( + strtotime($showtable['Check_time']) + ) + : ''; + + /** + * Gets fields properties + */ + $columns = $GLOBALS['dbi']->getColumns($this->db, $table); + + // Find which tables are related with the current one and write it in + // an array + $res_rel = $this->relation->getForeigners($this->db, $table); + + /** + * Displays the comments of the table if MySQL >= 3.23 + */ + + $break = false; + if (! empty($show_comment)) { + $this->diagram->Cell( + 0, + 3, + __('Table comments:') . ' ' . $show_comment, + 0, + 1 + ); + $break = true; + } + + if (! empty($create_time)) { + $this->diagram->Cell( + 0, + 3, + __('Creation:') . ' ' . $create_time, + 0, + 1 + ); + $break = true; + } + + if (! empty($update_time)) { + $this->diagram->Cell( + 0, + 3, + __('Last update:') . ' ' . $update_time, + 0, + 1 + ); + $break = true; + } + + if (! empty($check_time)) { + $this->diagram->Cell( + 0, + 3, + __('Last check:') . ' ' . $check_time, + 0, + 1 + ); + $break = true; + } + + if ($break == true) { + $this->diagram->Cell(0, 3, '', 0, 1); + $this->diagram->Ln(); + } + + $this->diagram->SetFont($this->_ff, 'B'); + if (isset($this->orientation) && $this->orientation == 'L') { + $this->diagram->Cell(25, 8, __('Column'), 1, 0, 'C'); + $this->diagram->Cell(20, 8, __('Type'), 1, 0, 'C'); + $this->diagram->Cell(20, 8, __('Attributes'), 1, 0, 'C'); + $this->diagram->Cell(10, 8, __('Null'), 1, 0, 'C'); + $this->diagram->Cell(20, 8, __('Default'), 1, 0, 'C'); + $this->diagram->Cell(25, 8, __('Extra'), 1, 0, 'C'); + $this->diagram->Cell(45, 8, __('Links to'), 1, 0, 'C'); + + if ($this->paper == 'A4') { + $comments_width = 67; + } else { + // this is really intended for 'letter' + /** + * @todo find optimal width for all formats + */ + $comments_width = 50; + } + $this->diagram->Cell($comments_width, 8, __('Comments'), 1, 0, 'C'); + $this->diagram->Cell(45, 8, 'MIME', 1, 1, 'C'); + $this->diagram->setWidths( + [ + 25, + 20, + 20, + 10, + 20, + 25, + 45, + $comments_width, + 45, + ] + ); + } else { + $this->diagram->Cell(20, 8, __('Column'), 1, 0, 'C'); + $this->diagram->Cell(20, 8, __('Type'), 1, 0, 'C'); + $this->diagram->Cell(20, 8, __('Attributes'), 1, 0, 'C'); + $this->diagram->Cell(10, 8, __('Null'), 1, 0, 'C'); + $this->diagram->Cell(15, 8, __('Default'), 1, 0, 'C'); + $this->diagram->Cell(15, 8, __('Extra'), 1, 0, 'C'); + $this->diagram->Cell(30, 8, __('Links to'), 1, 0, 'C'); + $this->diagram->Cell(30, 8, __('Comments'), 1, 0, 'C'); + $this->diagram->Cell(30, 8, 'MIME', 1, 1, 'C'); + $this->diagram->setWidths([20, 20, 20, 10, 15, 15, 30, 30, 30]); + } + $this->diagram->SetFont($this->_ff, ''); + + foreach ($columns as $row) { + $extracted_columnspec + = Util::extractColumnSpec($row['Type']); + $type = $extracted_columnspec['print_type']; + $attribute = $extracted_columnspec['attribute']; + if (! isset($row['Default'])) { + if ($row['Null'] != '' && $row['Null'] != 'NO') { + $row['Default'] = 'NULL'; + } + } + $field_name = $row['Field']; + // $this->diagram->Ln(); + $this->diagram->PMA_links['RT'][$table][$field_name] + = $this->diagram->AddLink(); + $this->diagram->Bookmark($field_name, 1, -1); + $this->diagram->SetLink( + $this->diagram->PMA_links['doc'][$table][$field_name], + -1 + ); + $foreigner = $this->relation->searchColumnInForeigners($res_rel, $field_name); + + $linksTo = ''; + if ($foreigner) { + $linksTo = '-> '; + if ($foreigner['foreign_db'] != $this->db) { + $linksTo .= $foreigner['foreign_db'] . '.'; + } + $linksTo .= $foreigner['foreign_table'] + . '.' . $foreigner['foreign_field']; + + if (isset($foreigner['on_update'])) { // not set for internal + $linksTo .= "\n" . 'ON UPDATE ' . $foreigner['on_update']; + $linksTo .= "\n" . 'ON DELETE ' . $foreigner['on_delete']; + } + } + + $diagram_row = [ + $field_name, + $type, + $attribute, + ($row['Null'] == '' || $row['Null'] == 'NO') + ? __('No') + : __('Yes'), + isset($row['Default']) ? $row['Default'] : '', + $row['Extra'], + $linksTo, + isset($comments[$field_name]) + ? $comments[$field_name] + : '', + isset($mime_map) && isset($mime_map[$field_name]) + ? str_replace('_', '/', $mime_map[$field_name]['mimetype']) + : '', + ]; + $links = []; + $links[0] = $this->diagram->PMA_links['RT'][$table][$field_name]; + if ($foreigner + && isset($this->diagram->PMA_links['doc'][$foreigner['foreign_table']][$foreigner['foreign_field']]) + ) { + $links[6] = $this->diagram->PMA_links['doc'][$foreigner['foreign_table']][$foreigner['foreign_field']]; + } else { + unset($links[6]); + } + $this->diagram->row($diagram_row, $links); + } // end foreach + $this->diagram->SetFont($this->_ff, '', 14); + } //end each + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Pdf/RelationStatsPdf.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Pdf/RelationStatsPdf.php new file mode 100644 index 0000000..b422ce5 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Pdf/RelationStatsPdf.php @@ -0,0 +1,163 @@ +wTick = 5; + parent::__construct( + $diagram, + $master_table, + $master_field, + $foreign_table, + $foreign_field + ); + } + + /** + * draws relation links and arrows shows foreign key relations + * + * @param boolean $showColor Whether to use one color per relation or not + * @param integer $i The id of the link to draw + * + * @access public + * + * @return void + * + * @see Pdf + */ + public function relationDraw($showColor, $i) + { + if ($showColor) { + $d = $i % 6; + $j = ($i - $d) / 6; + $j %= 4; + $j++; + $case = [ + [ + 1, + 0, + 0, + ], + [ + 0, + 1, + 0, + ], + [ + 0, + 0, + 1, + ], + [ + 1, + 1, + 0, + ], + [ + 1, + 0, + 1, + ], + [ + 0, + 1, + 1, + ], + ]; + list ($a, $b, $c) = $case[$d]; + $e = (1 - ($j - 1) / 6); + $this->diagram->SetDrawColor($a * 255 * $e, $b * 255 * $e, $c * 255 * $e); + } else { + $this->diagram->SetDrawColor(0); + } + $this->diagram->setLineWidthScale(0.2); + $this->diagram->lineScale( + $this->xSrc, + $this->ySrc, + $this->xSrc + $this->srcDir * $this->wTick, + $this->ySrc + ); + $this->diagram->lineScale( + $this->xDest + $this->destDir * $this->wTick, + $this->yDest, + $this->xDest, + $this->yDest + ); + $this->diagram->setLineWidthScale(0.1); + $this->diagram->lineScale( + $this->xSrc + $this->srcDir * $this->wTick, + $this->ySrc, + $this->xDest + $this->destDir * $this->wTick, + $this->yDest + ); + /* + * Draws arrows -> + */ + $root2 = 2 * sqrt(2); + $this->diagram->lineScale( + $this->xSrc + $this->srcDir * $this->wTick * 0.75, + $this->ySrc, + $this->xSrc + $this->srcDir * (0.75 - 1 / $root2) * $this->wTick, + $this->ySrc + $this->wTick / $root2 + ); + $this->diagram->lineScale( + $this->xSrc + $this->srcDir * $this->wTick * 0.75, + $this->ySrc, + $this->xSrc + $this->srcDir * (0.75 - 1 / $root2) * $this->wTick, + $this->ySrc - $this->wTick / $root2 + ); + + $this->diagram->lineScale( + $this->xDest + $this->destDir * $this->wTick / 2, + $this->yDest, + $this->xDest + $this->destDir * (0.5 + 1 / $root2) * $this->wTick, + $this->yDest + $this->wTick / $root2 + ); + $this->diagram->lineScale( + $this->xDest + $this->destDir * $this->wTick / 2, + $this->yDest, + $this->xDest + $this->destDir * (0.5 + 1 / $root2) * $this->wTick, + $this->yDest - $this->wTick / $root2 + ); + $this->diagram->SetDrawColor(0); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Pdf/TableStatsPdf.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Pdf/TableStatsPdf.php new file mode 100644 index 0000000..999894c --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Pdf/TableStatsPdf.php @@ -0,0 +1,233 @@ +heightCell = 6; + $this->_setHeight(); + /* + * setWidth must me after setHeight, because title + * can include table height which changes table width + */ + $this->_setWidth($fontSize); + if ($sameWideWidth < $this->width) { + $sameWideWidth = $this->width; + } + } + + /** + * Displays an error when the table cannot be found. + * + * @return void + */ + protected function showMissingTableError() + { + ExportRelationSchema::dieSchema( + $this->pageNumber, + "PDF", + sprintf(__('The %s table doesn\'t exist!'), $this->tableName) + ); + } + + /** + * Returns title of the current table, + * title can have the dimensions of the table + * + * @return string + */ + protected function getTitle() + { + $ret = ''; + if ($this->tableDimension) { + $ret = sprintf('%.0fx%0.f', $this->width, $this->height); + } + + return $ret . ' ' . $this->tableName; + } + + /** + * Sets the width of the table + * + * @param integer $fontSize The font size + * + * @access private + * + * @return void + * + * @see PMA_Schema_PDF + */ + private function _setWidth($fontSize) + { + foreach ($this->fields as $field) { + $this->width = max($this->width, $this->diagram->GetStringWidth($field)); + } + $this->width += $this->diagram->GetStringWidth(' '); + $this->diagram->SetFont($this->_ff, 'B', $fontSize); + /* + * it is unknown what value must be added, because + * table title is affected by the table width value + */ + while ($this->width < $this->diagram->GetStringWidth($this->getTitle())) { + $this->width += 5; + } + $this->diagram->SetFont($this->_ff, '', $fontSize); + } + + /** + * Sets the height of the table + * + * @return void + * + * @access private + */ + private function _setHeight() + { + $this->height = (count($this->fields) + 1) * $this->heightCell; + } + + /** + * Do draw the table + * + * @param integer $fontSize The font size + * @param boolean $withDoc Whether to include links to documentation + * @param boolean|integer $setColor Whether to display color + * + * @access public + * + * @return void + * + * @see PMA_Schema_PDF + */ + public function tableDraw($fontSize, $withDoc, $setColor = 0) + { + $this->diagram->setXyScale($this->x, $this->y); + $this->diagram->SetFont($this->_ff, 'B', $fontSize); + if ($setColor) { + $this->diagram->SetTextColor(200); + $this->diagram->SetFillColor(0, 0, 128); + } + if ($withDoc) { + $this->diagram->SetLink( + $this->diagram->PMA_links['RT'][$this->tableName]['-'], + -1 + ); + } else { + $this->diagram->PMA_links['doc'][$this->tableName]['-'] = ''; + } + + $this->diagram->cellScale( + $this->width, + $this->heightCell, + $this->getTitle(), + 1, + 1, + 'C', + $setColor, + $this->diagram->PMA_links['doc'][$this->tableName]['-'] + ); + $this->diagram->setXScale($this->x); + $this->diagram->SetFont($this->_ff, '', $fontSize); + $this->diagram->SetTextColor(0); + $this->diagram->SetFillColor(255); + + foreach ($this->fields as $field) { + if ($setColor) { + if (in_array($field, $this->primary)) { + $this->diagram->SetFillColor(215, 121, 123); + } + if ($field == $this->displayfield) { + $this->diagram->SetFillColor(142, 159, 224); + } + } + if ($withDoc) { + $this->diagram->SetLink( + $this->diagram->PMA_links['RT'][$this->tableName][$field], + -1 + ); + } else { + $this->diagram->PMA_links['doc'][$this->tableName][$field] = ''; + } + + $this->diagram->cellScale( + $this->width, + $this->heightCell, + ' ' . $field, + 1, + 1, + 'L', + $setColor, + $this->diagram->PMA_links['doc'][$this->tableName][$field] + ); + $this->diagram->setXScale($this->x); + $this->diagram->SetFillColor(255); + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/RelationStats.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/RelationStats.php new file mode 100644 index 0000000..848fbf4 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/RelationStats.php @@ -0,0 +1,120 @@ +diagram = $diagram; + + $src_pos = $this->_getXy($master_table, $master_field); + $dest_pos = $this->_getXy($foreign_table, $foreign_field); + /* + * [0] is x-left + * [1] is x-right + * [2] is y + */ + $src_left = $src_pos[0] - $this->wTick; + $src_right = $src_pos[1] + $this->wTick; + $dest_left = $dest_pos[0] - $this->wTick; + $dest_right = $dest_pos[1] + $this->wTick; + + $d1 = abs($src_left - $dest_left); + $d2 = abs($src_right - $dest_left); + $d3 = abs($src_left - $dest_right); + $d4 = abs($src_right - $dest_right); + $d = min($d1, $d2, $d3, $d4); + + if ($d == $d1) { + $this->xSrc = $src_pos[0]; + $this->srcDir = -1; + $this->xDest = $dest_pos[0]; + $this->destDir = -1; + } elseif ($d == $d2) { + $this->xSrc = $src_pos[1]; + $this->srcDir = 1; + $this->xDest = $dest_pos[0]; + $this->destDir = -1; + } elseif ($d == $d3) { + $this->xSrc = $src_pos[0]; + $this->srcDir = -1; + $this->xDest = $dest_pos[1]; + $this->destDir = 1; + } else { + $this->xSrc = $src_pos[1]; + $this->srcDir = 1; + $this->xDest = $dest_pos[1]; + $this->destDir = 1; + } + $this->ySrc = $src_pos[2]; + $this->yDest = $dest_pos[2]; + } + + /** + * Gets arrows coordinates + * + * @param TableStats $table The table + * @param string $column The relation column name + * + * @return array Arrows coordinates + * + * @access private + */ + private function _getXy($table, $column) + { + $pos = array_search($column, $table->fields); + + // x_left, x_right, y + return [ + $table->x, + $table->x + $table->width, + $table->y + ($pos + 1.5) * $table->heightCell, + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/SchemaDia.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/SchemaDia.php new file mode 100644 index 0000000..8c328d5 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/SchemaDia.php @@ -0,0 +1,100 @@ +setProperties(); + } + + /** + * Sets the schema export Dia properties + * + * @return void + */ + protected function setProperties() + { + $schemaPluginProperties = new SchemaPluginProperties(); + $schemaPluginProperties->setText('Dia'); + $schemaPluginProperties->setExtension('dia'); + $schemaPluginProperties->setMimeType('application/dia'); + + // create the root group that will be the options field for + // $schemaPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // specific options main group + $specificOptions = new OptionsPropertyMainGroup("general_opts"); + // add options common to all plugins + $this->addCommonOptions($specificOptions); + + $leaf = new SelectPropertyItem( + "orientation", + __('Orientation') + ); + $leaf->setValues( + [ + 'L' => __('Landscape'), + 'P' => __('Portrait'), + ] + ); + $specificOptions->addProperty($leaf); + + $leaf = new SelectPropertyItem( + "paper", + __('Paper size') + ); + $leaf->setValues($this->getPaperSizeArray()); + $specificOptions->addProperty($leaf); + + // add the main group to the root group + $exportSpecificOptions->addProperty($specificOptions); + + // set the options for the schema export plugin property item + $schemaPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $schemaPluginProperties; + } + + /** + * Exports the schema into DIA format. + * + * @param string $db database name + * + * @return bool Whether it succeeded + */ + public function exportSchema($db) + { + $export = new DiaRelationSchema($db); + $export->showOutput(); + return true; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/SchemaEps.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/SchemaEps.php new file mode 100644 index 0000000..4218376 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/SchemaEps.php @@ -0,0 +1,101 @@ +setProperties(); + } + + /** + * Sets the schema export EPS properties + * + * @return void + */ + protected function setProperties() + { + $schemaPluginProperties = new SchemaPluginProperties(); + $schemaPluginProperties->setText('EPS'); + $schemaPluginProperties->setExtension('eps'); + $schemaPluginProperties->setMimeType('application/eps'); + + // create the root group that will be the options field for + // $schemaPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // specific options main group + $specificOptions = new OptionsPropertyMainGroup("general_opts"); + // add options common to all plugins + $this->addCommonOptions($specificOptions); + + // create leaf items and add them to the group + $leaf = new BoolPropertyItem( + 'all_tables_same_width', + __('Same width for all tables') + ); + $specificOptions->addProperty($leaf); + + $leaf = new SelectPropertyItem( + "orientation", + __('Orientation') + ); + $leaf->setValues( + [ + 'L' => __('Landscape'), + 'P' => __('Portrait'), + ] + ); + $specificOptions->addProperty($leaf); + + // add the main group to the root group + $exportSpecificOptions->addProperty($specificOptions); + + // set the options for the schema export plugin property item + $schemaPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $schemaPluginProperties; + } + + /** + * Exports the schema into EPS format. + * + * @param string $db database name + * + * @return bool Whether it succeeded + */ + public function exportSchema($db) + { + $export = new EpsRelationSchema($db); + $export->showOutput(); + return true; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/SchemaPdf.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/SchemaPdf.php new file mode 100644 index 0000000..c57fcee --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/SchemaPdf.php @@ -0,0 +1,133 @@ +setProperties(); + } + + /** + * Sets the schema export PDF properties + * + * @return void + */ + protected function setProperties() + { + $schemaPluginProperties = new SchemaPluginProperties(); + $schemaPluginProperties->setText('PDF'); + $schemaPluginProperties->setExtension('pdf'); + $schemaPluginProperties->setMimeType('application/pdf'); + + // create the root group that will be the options field for + // $schemaPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // specific options main group + $specificOptions = new OptionsPropertyMainGroup("general_opts"); + // add options common to all plugins + $this->addCommonOptions($specificOptions); + + // create leaf items and add them to the group + $leaf = new BoolPropertyItem( + 'all_tables_same_width', + __('Same width for all tables') + ); + $specificOptions->addProperty($leaf); + + $leaf = new SelectPropertyItem( + "orientation", + __('Orientation') + ); + $leaf->setValues( + [ + 'L' => __('Landscape'), + 'P' => __('Portrait'), + ] + ); + $specificOptions->addProperty($leaf); + + $leaf = new SelectPropertyItem( + "paper", + __('Paper size') + ); + $leaf->setValues($this->getPaperSizeArray()); + $specificOptions->addProperty($leaf); + + $leaf = new BoolPropertyItem( + 'show_grid', + __('Show grid') + ); + $specificOptions->addProperty($leaf); + + $leaf = new BoolPropertyItem( + 'with_doc', + __('Data dictionary') + ); + $specificOptions->addProperty($leaf); + + $leaf = new SelectPropertyItem( + "table_order", + __('Order of the tables') + ); + $leaf->setValues( + [ + '' => __('None'), + 'name_asc' => __('Name (Ascending)'), + 'name_desc' => __('Name (Descending)'), + ] + ); + $specificOptions->addProperty($leaf); + + // add the main group to the root group + $exportSpecificOptions->addProperty($specificOptions); + + // set the options for the schema export plugin property item + $schemaPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $schemaPluginProperties; + } + + /** + * Exports the schema into PDF format. + * + * @param string $db database name + * + * @return bool Whether it succeeded + */ + public function exportSchema($db) + { + $export = new PdfRelationSchema($db); + $export->showOutput(); + return true; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/SchemaSvg.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/SchemaSvg.php new file mode 100644 index 0000000..9466f64 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/SchemaSvg.php @@ -0,0 +1,88 @@ +setProperties(); + } + + /** + * Sets the schema export SVG properties + * + * @return void + */ + protected function setProperties() + { + $schemaPluginProperties = new SchemaPluginProperties(); + $schemaPluginProperties->setText('SVG'); + $schemaPluginProperties->setExtension('svg'); + $schemaPluginProperties->setMimeType('application/svg'); + + // create the root group that will be the options field for + // $schemaPluginProperties + // this will be shown as "Format specific options" + $exportSpecificOptions = new OptionsPropertyRootGroup( + "Format Specific Options" + ); + + // specific options main group + $specificOptions = new OptionsPropertyMainGroup("general_opts"); + // add options common to all plugins + $this->addCommonOptions($specificOptions); + + // create leaf items and add them to the group + $leaf = new BoolPropertyItem( + 'all_tables_same_width', + __('Same width for all tables') + ); + $specificOptions->addProperty($leaf); + + // add the main group to the root group + $exportSpecificOptions->addProperty($specificOptions); + + // set the options for the schema export plugin property item + $schemaPluginProperties->setOptions($exportSpecificOptions); + $this->properties = $schemaPluginProperties; + } + + /** + * Exports the schema into SVG format. + * + * @param string $db database name + * + * @return bool Whether it succeeded + */ + public function exportSchema($db) + { + $export = new SvgRelationSchema($db); + $export->showOutput(); + return true; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Svg/RelationStatsSvg.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Svg/RelationStatsSvg.php new file mode 100644 index 0000000..2e323fc --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Svg/RelationStatsSvg.php @@ -0,0 +1,140 @@ +wTick = 10; + parent::__construct( + $diagram, + $master_table, + $master_field, + $foreign_table, + $foreign_field + ); + } + + /** + * draws relation links and arrows shows foreign key relations + * + * @param boolean $showColor Whether to use one color per relation or not + * + * @return void + * @access public + * + * @see PMA_SVG + */ + public function relationDraw($showColor) + { + if ($showColor) { + $listOfColors = [ + '#c00', + '#bbb', + '#333', + '#cb0', + '#0b0', + '#0bf', + '#b0b', + ]; + shuffle($listOfColors); + $color = $listOfColors[0]; + } else { + $color = '#333'; + } + + $this->diagram->printElementLine( + 'line', + $this->xSrc, + $this->ySrc, + $this->xSrc + $this->srcDir * $this->wTick, + $this->ySrc, + 'stroke:' . $color . ';stroke-width:1;' + ); + $this->diagram->printElementLine( + 'line', + $this->xDest + $this->destDir * $this->wTick, + $this->yDest, + $this->xDest, + $this->yDest, + 'stroke:' . $color . ';stroke-width:1;' + ); + $this->diagram->printElementLine( + 'line', + $this->xSrc + $this->srcDir * $this->wTick, + $this->ySrc, + $this->xDest + $this->destDir * $this->wTick, + $this->yDest, + 'stroke:' . $color . ';stroke-width:1;' + ); + $root2 = 2 * sqrt(2); + $this->diagram->printElementLine( + 'line', + $this->xSrc + $this->srcDir * $this->wTick * 0.75, + $this->ySrc, + $this->xSrc + $this->srcDir * (0.75 - 1 / $root2) * $this->wTick, + $this->ySrc + $this->wTick / $root2, + 'stroke:' . $color . ';stroke-width:2;' + ); + $this->diagram->printElementLine( + 'line', + $this->xSrc + $this->srcDir * $this->wTick * 0.75, + $this->ySrc, + $this->xSrc + $this->srcDir * (0.75 - 1 / $root2) * $this->wTick, + $this->ySrc - $this->wTick / $root2, + 'stroke:' . $color . ';stroke-width:2;' + ); + $this->diagram->printElementLine( + 'line', + $this->xDest + $this->destDir * $this->wTick / 2, + $this->yDest, + $this->xDest + $this->destDir * (0.5 + 1 / $root2) * $this->wTick, + $this->yDest + $this->wTick / $root2, + 'stroke:' . $color . ';stroke-width:2;' + ); + $this->diagram->printElementLine( + 'line', + $this->xDest + $this->destDir * $this->wTick / 2, + $this->yDest, + $this->xDest + $this->destDir * (0.5 + 1 / $root2) * $this->wTick, + $this->yDest - $this->wTick / $root2, + 'stroke:' . $color . ';stroke-width:2;' + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Svg/Svg.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Svg/Svg.php new file mode 100644 index 0000000..4404574 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Svg/Svg.php @@ -0,0 +1,281 @@ +openMemory(); + /* + * Set indenting using three spaces, + * so output is formatted + */ + + $this->setIndent(true); + $this->setIndentString(' '); + /* + * Create the XML document + */ + + $this->startDocument('1.0', 'UTF-8'); + $this->startDtd( + 'svg', + '-//W3C//DTD SVG 1.1//EN', + 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' + ); + $this->endDtd(); + } + + /** + * Set document title + * + * @param string $value sets the title text + * + * @return void + */ + public function setTitle($value) + { + $this->title = $value; + } + + /** + * Set document author + * + * @param string $value sets the author + * + * @return void + */ + public function setAuthor($value) + { + $this->author = $value; + } + + /** + * Set document font + * + * @param string $value sets the font e.g Arial, Sans-serif etc + * + * @return void + */ + public function setFont($value) + { + $this->font = $value; + } + + /** + * Get document font + * + * @return string returns the font name + */ + public function getFont() + { + return $this->font; + } + + /** + * Set document font size + * + * @param integer $value sets the font size in pixels + * + * @return void + */ + public function setFontSize($value) + { + $this->fontSize = $value; + } + + /** + * Get document font size + * + * @return integer returns the font size + */ + public function getFontSize() + { + return $this->fontSize; + } + + /** + * Starts RelationStatsSvg Document + * + * svg document starts by first initializing svg tag + * which contains all the attributes and namespace that needed + * to define the svg document + * + * @param integer $width total width of the RelationStatsSvg document + * @param integer $height total height of the RelationStatsSvg document + * @param integer $x min-x of the view box + * @param integer $y min-y of the view box + * + * @return void + * + * @see XMLWriter::startElement(),XMLWriter::writeAttribute() + */ + public function startSvgDoc($width, $height, $x = 0, $y = 0) + { + $this->startElement('svg'); + + if (! is_int($width)) { + $width = intval($width); + } + + if (! is_int($height)) { + $height = intval($height); + } + + if ($x != 0 || $y != 0) { + $this->writeAttribute('viewBox', "$x $y $width $height"); + } + $this->writeAttribute('width', ($width - $x) . 'px'); + $this->writeAttribute('height', ($height - $y) . 'px'); + $this->writeAttribute('xmlns', 'http://www.w3.org/2000/svg'); + $this->writeAttribute('version', '1.1'); + } + + /** + * Ends RelationStatsSvg Document + * + * @return void + * @see XMLWriter::endElement(),XMLWriter::endDocument() + */ + public function endSvgDoc() + { + $this->endElement(); + $this->endDocument(); + } + + /** + * output RelationStatsSvg Document + * + * svg document prompted to the user for download + * RelationStatsSvg document saved in .svg extension and can be + * easily changeable by using any svg IDE + * + * @param string $fileName file name + * + * @return void + * @see XMLWriter::startElement(),XMLWriter::writeAttribute() + */ + public function showOutput($fileName) + { + //ob_get_clean(); + $output = $this->flush(); + Response::getInstance()->disable(); + Core::downloadHeader( + $fileName, + 'image/svg+xml', + strlen($output) + ); + print $output; + } + + /** + * Draws RelationStatsSvg elements + * + * SVG has some predefined shape elements like rectangle & text + * and other elements who have x,y co-ordinates are drawn. + * specify their width and height and can give styles too. + * + * @param string $name RelationStatsSvg element name + * @param int $x The x attr defines the left position of the element + * (e.g. x="0" places the element 0 pixels from the + * left of the browser window) + * @param integer $y The y attribute defines the top position of the + * element (e.g. y="0" places the element 0 pixels + * from the top of the browser window) + * @param int|string $width The width attribute defines the width the element + * @param int|string $height The height attribute defines the height the element + * @param string|null $text The text attribute defines the text the element + * @param string $styles The style attribute defines the style the element + * styles can be defined like CSS styles + * + * @return void + * + * @see XMLWriter::startElement(), XMLWriter::writeAttribute(), + * XMLWriter::text(), XMLWriter::endElement() + */ + public function printElement( + $name, + $x, + $y, + $width = '', + $height = '', + ?string $text = '', + $styles = '' + ) { + $this->startElement($name); + $this->writeAttribute('width', (string) $width); + $this->writeAttribute('height', (string) $height); + $this->writeAttribute('x', (string) $x); + $this->writeAttribute('y', (string) $y); + $this->writeAttribute('style', (string) $styles); + if (isset($text)) { + $this->writeAttribute('font-family', (string) $this->font); + $this->writeAttribute('font-size', $this->fontSize . 'px'); + $this->text($text); + } + $this->endElement(); + } + + /** + * Draws RelationStatsSvg Line element + * + * RelationStatsSvg line element is drawn for connecting the tables. + * arrows are also drawn by specify its start and ending + * co-ordinates + * + * @param string $name RelationStatsSvg element name i.e line + * @param integer $x1 Defines the start of the line on the x-axis + * @param integer $y1 Defines the start of the line on the y-axis + * @param integer $x2 Defines the end of the line on the x-axis + * @param integer $y2 Defines the end of the line on the y-axis + * @param string $styles The style attribute defines the style the element + * styles can be defined like CSS styles + * + * @return void + * + * @see XMLWriter::startElement(), XMLWriter::writeAttribute(), + * XMLWriter::endElement() + */ + public function printElementLine($name, $x1, $y1, $x2, $y2, $styles) + { + $this->startElement($name); + $this->writeAttribute('x1', $x1); + $this->writeAttribute('y1', $y1); + $this->writeAttribute('x2', $x2); + $this->writeAttribute('y2', $y2); + $this->writeAttribute('style', $styles); + $this->endElement(); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Svg/SvgRelationSchema.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Svg/SvgRelationSchema.php new file mode 100644 index 0000000..9a18b6e --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Svg/SvgRelationSchema.php @@ -0,0 +1,284 @@ +setShowColor(isset($_REQUEST['svg_show_color'])); + $this->setShowKeys(isset($_REQUEST['svg_show_keys'])); + $this->setTableDimension(isset($_REQUEST['svg_show_table_dimension'])); + $this->setAllTablesSameWidth(isset($_REQUEST['svg_all_tables_same_width'])); + + $this->diagram->setTitle( + sprintf( + __('Schema of the %s database - Page %s'), + $this->db, + $this->pageNumber + ) + ); + $this->diagram->SetAuthor('phpMyAdmin ' . PMA_VERSION); + $this->diagram->setFont('Arial'); + $this->diagram->setFontSize(16); + + $alltables = $this->getTablesFromRequest(); + + foreach ($alltables as $table) { + if (! isset($this->_tables[$table])) { + $this->_tables[$table] = new TableStatsSvg( + $this->diagram, + $this->db, + $table, + $this->diagram->getFont(), + $this->diagram->getFontSize(), + $this->pageNumber, + $this->_tablewidth, + $this->showKeys, + $this->tableDimension, + $this->offline + ); + } + + if ($this->sameWide) { + $this->_tables[$table]->width = &$this->_tablewidth; + } + $this->_setMinMax($this->_tables[$table]); + } + + $border = 15; + $this->diagram->startSvgDoc( + $this->_xMax + $border, + $this->_yMax + $border, + $this->_xMin - $border, + $this->_yMin - $border + ); + + $seen_a_relation = false; + foreach ($alltables as $one_table) { + $exist_rel = $this->relation->getForeigners($this->db, $one_table, '', 'both'); + if (! $exist_rel) { + continue; + } + + $seen_a_relation = true; + foreach ($exist_rel as $master_field => $rel) { + /* put the foreign table on the schema only if selected + * by the user + * (do not use array_search() because we would have to + * to do a === false and this is not PHP3 compatible) + */ + if ($master_field != 'foreign_keys_data') { + if (in_array($rel['foreign_table'], $alltables)) { + $this->_addRelation( + $one_table, + $this->diagram->getFont(), + $this->diagram->getFontSize(), + $master_field, + $rel['foreign_table'], + $rel['foreign_field'], + $this->tableDimension + ); + } + continue; + } + + foreach ($rel as $one_key) { + if (! in_array($one_key['ref_table_name'], $alltables)) { + continue; + } + + foreach ($one_key['index_list'] as $index => $one_field) { + $this->_addRelation( + $one_table, + $this->diagram->getFont(), + $this->diagram->getFontSize(), + $one_field, + $one_key['ref_table_name'], + $one_key['ref_index_list'][$index], + $this->tableDimension + ); + } + } + } + } + if ($seen_a_relation) { + $this->_drawRelations(); + } + + $this->_drawTables(); + $this->diagram->endSvgDoc(); + } + + /** + * Output RelationStatsSvg Document for download + * + * @return void + */ + public function showOutput() + { + $this->diagram->showOutput($this->getFileName('.svg')); + } + + /** + * Sets X and Y minimum and maximum for a table cell + * + * @param TableStatsSvg $table The table + * + * @return void + */ + private function _setMinMax($table) + { + $this->_xMax = max($this->_xMax, $table->x + $table->width); + $this->_yMax = max($this->_yMax, $table->y + $table->height); + $this->_xMin = min($this->_xMin, $table->x); + $this->_yMin = min($this->_yMin, $table->y); + } + + /** + * Defines relation objects + * + * @param string $masterTable The master table name + * @param string $font The font face + * @param int $fontSize Font size + * @param string $masterField The relation field in the master table + * @param string $foreignTable The foreign table name + * @param string $foreignField The relation field in the foreign table + * @param boolean $tableDimension Whether to display table position or not + * + * @return void + * + * @see _setMinMax,Table_Stats_Svg::__construct(), + * PhpMyAdmin\Plugins\Schema\Svg\RelationStatsSvg::__construct() + */ + private function _addRelation( + $masterTable, + $font, + $fontSize, + $masterField, + $foreignTable, + $foreignField, + $tableDimension + ) { + if (! isset($this->_tables[$masterTable])) { + $this->_tables[$masterTable] = new TableStatsSvg( + $this->diagram, + $this->db, + $masterTable, + $font, + $fontSize, + $this->pageNumber, + $this->_tablewidth, + false, + $tableDimension + ); + $this->_setMinMax($this->_tables[$masterTable]); + } + if (! isset($this->_tables[$foreignTable])) { + $this->_tables[$foreignTable] = new TableStatsSvg( + $this->diagram, + $this->db, + $foreignTable, + $font, + $fontSize, + $this->pageNumber, + $this->_tablewidth, + false, + $tableDimension + ); + $this->_setMinMax($this->_tables[$foreignTable]); + } + $this->_relations[] = new RelationStatsSvg( + $this->diagram, + $this->_tables[$masterTable], + $masterField, + $this->_tables[$foreignTable], + $foreignField + ); + } + + /** + * Draws relation arrows and lines + * connects master table's master field to + * foreign table's foreign field + * + * @return void + * + * @see Relation_Stats_Svg::relationDraw() + */ + private function _drawRelations() + { + foreach ($this->_relations as $relation) { + $relation->relationDraw($this->showColor); + } + } + + /** + * Draws tables + * + * @return void + * + * @see Table_Stats_Svg::Table_Stats_tableDraw() + */ + private function _drawTables() + { + foreach ($this->_tables as $table) { + $table->tableDraw($this->showColor); + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Svg/TableStatsSvg.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Svg/TableStatsSvg.php new file mode 100644 index 0000000..13b1a82 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/Svg/TableStatsSvg.php @@ -0,0 +1,204 @@ +_setHeightTable($fontSize); + // setWidth must me after setHeight, because title + // can include table height which changes table width + $this->_setWidthTable($font, $fontSize); + if ($same_wide_width < $this->width) { + $same_wide_width = $this->width; + } + } + + /** + * Displays an error when the table cannot be found. + * + * @return void + */ + protected function showMissingTableError() + { + ExportRelationSchema::dieSchema( + $this->pageNumber, + "SVG", + sprintf(__('The %s table doesn\'t exist!'), $this->tableName) + ); + } + + /** + * Sets the width of the table + * + * @param string $font The font size + * @param integer $fontSize The font size + * + * @return void + * @access private + * + * @see PMA_SVG + */ + private function _setWidthTable($font, $fontSize) + { + foreach ($this->fields as $field) { + $this->width = max( + $this->width, + $this->font->getStringWidth($field, $font, $fontSize) + ); + } + $this->width += $this->font->getStringWidth(' ', $font, $fontSize); + + /* + * it is unknown what value must be added, because + * table title is affected by the table width value + */ + while ($this->width + < $this->font->getStringWidth($this->getTitle(), $font, $fontSize) + ) { + $this->width += 7; + } + } + + /** + * Sets the height of the table + * + * @param integer $fontSize font size + * + * @return void + */ + private function _setHeightTable($fontSize) + { + $this->heightCell = $fontSize + 4; + $this->height = (count($this->fields) + 1) * $this->heightCell; + } + + /** + * draw the table + * + * @param boolean $showColor Whether to display color + * + * @access public + * @return void + * + * @see PMA_SVG,PMA_SVG::printElement + */ + public function tableDraw($showColor) + { + $this->diagram->printElement( + 'rect', + $this->x, + $this->y, + $this->width, + $this->heightCell, + null, + 'fill:#007;stroke:black;' + ); + $this->diagram->printElement( + 'text', + $this->x + 5, + $this->y + 14, + $this->width, + $this->heightCell, + $this->getTitle(), + 'fill:#fff;' + ); + foreach ($this->fields as $field) { + $this->currentCell += $this->heightCell; + $fillColor = 'none'; + if ($showColor) { + if (in_array($field, $this->primary)) { + $fillColor = '#aea'; + } + if ($field == $this->displayfield) { + $fillColor = 'none'; + } + } + $this->diagram->printElement( + 'rect', + $this->x, + $this->y + $this->currentCell, + $this->width, + $this->heightCell, + null, + 'fill:' . $fillColor . ';stroke:black;' + ); + $this->diagram->printElement( + 'text', + $this->x + 5, + $this->y + 14 + $this->currentCell, + $this->width, + $this->heightCell, + $field, + 'fill:black;' + ); + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Schema/TableStats.php b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/TableStats.php new file mode 100644 index 0000000..a3d3b5c --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Schema/TableStats.php @@ -0,0 +1,208 @@ +diagram = $diagram; + $this->db = $db; + $this->pageNumber = $pageNumber; + $this->tableName = $tableName; + + $this->showKeys = $showKeys; + $this->tableDimension = $tableDimension; + + $this->offline = $offline; + + $this->relation = new Relation($GLOBALS['dbi']); + $this->font = new Font(); + + // checks whether the table exists + // and loads fields + $this->validateTableAndLoadFields(); + // load table coordinates + $this->loadCoordinates(); + // loads display field + $this->loadDisplayField(); + // loads primary keys + $this->loadPrimaryKey(); + } + + /** + * Validate whether the table exists. + * + * @return void + */ + protected function validateTableAndLoadFields() + { + $sql = 'DESCRIBE ' . Util::backquote($this->tableName); + $result = $GLOBALS['dbi']->tryQuery( + $sql, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + if (! $result || ! $GLOBALS['dbi']->numRows($result)) { + $this->showMissingTableError(); + } + + if ($this->showKeys) { + $indexes = Index::getFromTable($this->tableName, $this->db); + $all_columns = []; + foreach ($indexes as $index) { + $all_columns = array_merge( + $all_columns, + array_flip(array_keys($index->getColumns())) + ); + } + $this->fields = array_keys($all_columns); + } else { + while ($row = $GLOBALS['dbi']->fetchRow($result)) { + $this->fields[] = $row[0]; + } + } + } + + /** + * Displays an error when the table cannot be found. + * + * @return void + * @abstract + */ + abstract protected function showMissingTableError(); + + /** + * Loads coordinates of a table + * + * @return void + */ + protected function loadCoordinates() + { + if (isset($_POST['t_h'])) { + foreach ($_POST['t_h'] as $key => $value) { + $db = rawurldecode($_POST['t_db'][$key]); + $tbl = rawurldecode($_POST['t_tbl'][$key]); + if ($this->db . '.' . $this->tableName === $db . '.' . $tbl) { + $this->x = (double) $_POST['t_x'][$key]; + $this->y = (double) $_POST['t_y'][$key]; + break; + } + } + } + } + + /** + * Loads the table's display field + * + * @return void + */ + protected function loadDisplayField() + { + $this->displayfield = $this->relation->getDisplayField($this->db, $this->tableName); + } + + /** + * Loads the PRIMARY key. + * + * @return void + */ + protected function loadPrimaryKey() + { + $result = $GLOBALS['dbi']->query( + 'SHOW INDEX FROM ' . Util::backquote($this->tableName) . ';', + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + if ($GLOBALS['dbi']->numRows($result) > 0) { + while ($row = $GLOBALS['dbi']->fetchAssoc($result)) { + if ($row['Key_name'] == 'PRIMARY') { + $this->primary[] = $row['Column_name']; + } + } + } + } + + /** + * Returns title of the current table, + * title can have the dimensions/co-ordinates of the table + * + * @return string title of the current table + */ + protected function getTitle() + { + return ($this->tableDimension + ? sprintf('%.0fx%0.f', $this->width, $this->heightCell) + : '' + ) + . ' ' . $this->tableName; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/SchemaPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/SchemaPlugin.php new file mode 100644 index 0000000..12cbd40 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/SchemaPlugin.php @@ -0,0 +1,90 @@ +properties; + } + + /** + * Sets the export plugins properties and is implemented by + * each schema export plugin + * + * @return void + */ + abstract protected function setProperties(); + + /** + * Exports the schema into the specified format. + * + * @param string $db database name + * + * @return bool Whether it succeeded + */ + abstract public function exportSchema($db); + + /** + * Adds export options common to all plugins. + * + * @param OptionsPropertyMainGroup $propertyGroup property group + * + * @return void + */ + protected function addCommonOptions(OptionsPropertyMainGroup $propertyGroup) + { + $leaf = new BoolPropertyItem('show_color', __('Show color')); + $propertyGroup->addProperty($leaf); + $leaf = new BoolPropertyItem('show_keys', __('Only show keys')); + $propertyGroup->addProperty($leaf); + } + + /** + * Returns the array of paper sizes + * + * @return array array of paper sizes + */ + protected function getPaperSizeArray() + { + $ret = []; + foreach ($GLOBALS['cfg']['PDFPageSizes'] as $val) { + $ret[$val] = $val; + } + + return $ret; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/Bool2TextTransformationsPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/Bool2TextTransformationsPlugin.php new file mode 100644 index 0000000..696aa64 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/Bool2TextTransformationsPlugin.php @@ -0,0 +1,69 @@ +getOptions($options, $cfg['DefaultTransformations']['Bool2Text']); + + if ($buffer == '0') { + return $options[1]; // return false label + } + + return $options[0]; // or true one if nonzero + } + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the transformation name of the specific plugin + * + * @return string + */ + public static function getName() + { + return "Bool2Text"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/CodeMirrorEditorTransformationPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/CodeMirrorEditorTransformationPlugin.php new file mode 100644 index 0000000..bbd1fff --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/CodeMirrorEditorTransformationPlugin.php @@ -0,0 +1,75 @@ +'; + } + $class = 'transform_' . strtolower(static::getName()) . '_editor'; + $html .= ''; + + return $html; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/DateFormatTransformationsPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/DateFormatTransformationsPlugin.php new file mode 100644 index 0000000..32ed494 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/DateFormatTransformationsPlugin.php @@ -0,0 +1,158 @@ +getOptions($options, $cfg['DefaultTransformations']['DateFormat']); + + // further operations on $buffer using the $options[] array. + $options[2] = mb_strtolower($options[2]); + + if (empty($options[1])) { + if ($options[2] == 'local') { + $options[1] = __('%B %d, %Y at %I:%M %p'); + } else { + $options[1] = 'Y-m-d H:i:s'; + } + } + + $timestamp = -1; + + // INT columns will be treated as UNIX timestamps + // and need to be detected before the verification for + // MySQL TIMESTAMP + if ($meta->type == 'int') { + $timestamp = $buffer; + + // Detect TIMESTAMP(6 | 8 | 10 | 12 | 14) + // TIMESTAMP (2 | 4) not supported here. + // (Note: prior to MySQL 4.1, TIMESTAMP has a display size + // for example TIMESTAMP(8) means YYYYMMDD) + } else { + if (preg_match('/^(\d{2}){3,7}$/', $buffer)) { + if (mb_strlen($buffer) == 14 || mb_strlen($buffer) == 8) { + $offset = 4; + } else { + $offset = 2; + } + + $aDate = []; + $aDate['year'] = (int) mb_substr($buffer, 0, $offset); + $aDate['month'] = (int) mb_substr($buffer, $offset, 2); + $aDate['day'] = (int) mb_substr($buffer, $offset + 2, 2); + $aDate['hour'] = (int) mb_substr($buffer, $offset + 4, 2); + $aDate['minute'] = (int) mb_substr($buffer, $offset + 6, 2); + $aDate['second'] = (int) mb_substr($buffer, $offset + 8, 2); + + if (checkdate($aDate['month'], $aDate['day'], $aDate['year'])) { + $timestamp = mktime( + $aDate['hour'], + $aDate['minute'], + $aDate['second'], + $aDate['month'], + $aDate['day'], + $aDate['year'] + ); + } + // If all fails, assume one of the dozens of valid strtime() syntaxes + // (https://www.gnu.org/manual/tar-1.12/html_chapter/tar_7.html) + } else { + if (preg_match('/^[0-9]\d{1,9}$/', $buffer)) { + $timestamp = (int) $buffer; + } else { + $timestamp = strtotime($buffer); + } + } + } + + // If all above failed, maybe it's a Unix timestamp already? + if ($timestamp < 0 && preg_match('/^[1-9]\d{1,9}$/', $buffer)) { + $timestamp = $buffer; + } + + // Reformat a valid timestamp + if ($timestamp >= 0) { + $timestamp -= (int) $options[0] * 60 * 60; + $source = $buffer; + if ($options[2] == 'local') { + $text = Util::localisedDate( + $timestamp, + $options[1] + ); + } elseif ($options[2] == 'utc') { + $text = gmdate($options[1], $timestamp); + } else { + $text = 'INVALID DATE TYPE'; + } + return '' . htmlspecialchars((string) $text) . ''; + } + + return htmlspecialchars((string) $buffer); + } + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the transformation name of the specific plugin + * + * @return string + */ + public static function getName() + { + return "Date Format"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/DownloadTransformationsPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/DownloadTransformationsPlugin.php new file mode 100644 index 0000000..d6c21d6 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/DownloadTransformationsPlugin.php @@ -0,0 +1,93 @@ + $val) { + if ($val->name == $options[1]) { + $pos = $key; + break; + } + } + if (isset($pos)) { + $cn = $row[$pos]; + } + } + if (empty($cn)) { + $cn = 'binary_file.dat'; + } + } + + return sprintf( + '%s', + $options['wrapper_link'], + htmlspecialchars(urlencode($cn)), + htmlspecialchars($cn), + htmlspecialchars($cn) + ); + } + + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the transformation name of the specific plugin + * + * @return string + */ + public static function getName() + { + return "Download"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/ExternalTransformationsPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/ExternalTransformationsPlugin.php new file mode 100644 index 0000000..006ee86 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/ExternalTransformationsPlugin.php @@ -0,0 +1,160 @@ +getOptions( + $options, + $cfg['DefaultTransformations']['External'] + ); + + if (isset($allowed_programs[$options[0]])) { + $program = $allowed_programs[$options[0]]; + } else { + $program = $allowed_programs[0]; + } + + // needs PHP >= 4.3.0 + $newstring = ''; + $descriptorspec = [ + 0 => [ + "pipe", + "r", + ], + 1 => [ + "pipe", + "w", + ], + ]; + $process = proc_open($program . ' ' . $options[1], $descriptorspec, $pipes); + if (is_resource($process)) { + fwrite($pipes[0], $buffer); + fclose($pipes[0]); + + while (! feof($pipes[1])) { + $newstring .= fgets($pipes[1], 1024); + } + fclose($pipes[1]); + // we don't currently use the return value + proc_close($process); + } + + if ($options[2] == 1 || $options[2] == '2') { + $retstring = htmlspecialchars($newstring); + } else { + $retstring = $newstring; + } + + return $retstring; + } + + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the transformation name of the specific plugin + * + * @return string + */ + public static function getName() + { + return "External"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/FormattedTransformationsPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/FormattedTransformationsPlugin.php new file mode 100644 index 0000000..b40ffe3 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/FormattedTransformationsPlugin.php @@ -0,0 +1,65 @@ +'; + } + + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the transformation name of the specific plugin + * + * @return string + */ + public static function getName() + { + return "Formatted"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/HexTransformationsPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/HexTransformationsPlugin.php new file mode 100644 index 0000000..69ce92d --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/HexTransformationsPlugin.php @@ -0,0 +1,71 @@ +getOptions($options, $cfg['DefaultTransformations']['Hex']); + $options[0] = intval($options[0]); + + if ($options[0] < 1) { + return bin2hex($buffer); + } else { + return chunk_split(bin2hex($buffer), $options[0], ' '); + } + } + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the transformation name of the specific plugin + * + * @return string + */ + public static function getName() + { + return "Hex"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/ImageLinkTransformationsPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/ImageLinkTransformationsPlugin.php new file mode 100644 index 0000000..efcfdc5 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/ImageLinkTransformationsPlugin.php @@ -0,0 +1,63 @@ +[BLOB]'; + } + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the transformation name of the specific plugin + * + * @return string + */ + public static function getName() + { + return "ImageLink"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/ImageUploadTransformationsPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/ImageUploadTransformationsPlugin.php new file mode 100644 index 0000000..0e6c0fd --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/ImageUploadTransformationsPlugin.php @@ -0,0 +1,121 @@ +'; + $html .= ''; + $src = 'transformation_wrapper.php' . $options['wrapper_link']; + } + $html .= ''
+            . __('Image preview here') . ''; + $html .= '
    '; + + return $html; + } + + /** + * Returns the array of scripts (filename) required for plugin + * initialization and handling + * + * @return array javascripts to be included + */ + public function getScripts() + { + return [ + 'transformations/image_upload.js', + ]; + } + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the transformation name of the specific plugin + * + * @return string + */ + public static function getName() + { + return "Image upload"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/InlineTransformationsPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/InlineTransformationsPlugin.php new file mode 100644 index 0000000..102dce9 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/InlineTransformationsPlugin.php @@ -0,0 +1,78 @@ +getOptions($options, $cfg['DefaultTransformations']['Inline']); + + if (PMA_IS_GD2) { + return '[' . htmlspecialchars($buffer) . ']'; + } else { + return '[' . htmlspecialchars($buffer) . ']'; + } + } + + + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the transformation name of the specific plugin + * + * @return string + */ + public static function getName() + { + return "Inline"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/LongToIPv4TransformationsPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/LongToIPv4TransformationsPlugin.php new file mode 100644 index 0000000..03767dc --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/LongToIPv4TransformationsPlugin.php @@ -0,0 +1,66 @@ + 4294967295) { + return htmlspecialchars($buffer); + } + + return long2ip((int) $buffer); + } + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the transformation name of the specific plugin + * + * @return string + */ + public static function getName() + { + return "Long To IPv4"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/PreApPendTransformationsPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/PreApPendTransformationsPlugin.php new file mode 100644 index 0000000..3dbf9d8 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/PreApPendTransformationsPlugin.php @@ -0,0 +1,68 @@ +getOptions($options, $cfg['DefaultTransformations']['PreApPend']); + + //just prepend and/or append the options to the original text + return htmlspecialchars($options[0]) . htmlspecialchars($buffer) + . htmlspecialchars($options[1]); + } + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the transformation name of the specific plugin + * + * @return string + */ + public static function getName() + { + return "PreApPend"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/RegexValidationTransformationsPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/RegexValidationTransformationsPlugin.php new file mode 100644 index 0000000..33f0ccd --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/RegexValidationTransformationsPlugin.php @@ -0,0 +1,74 @@ +reset(); + if (! empty($options[0]) && ! preg_match($options[0], $buffer)) { + $this->success = false; + $this->error = sprintf( + __('Validation failed for the input string %s.'), + htmlspecialchars($buffer) + ); + } + + return $buffer; + } + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the transformation name of the specific plugin + * + * @return string + */ + public static function getName() + { + return "Regex Validation"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/SQLTransformationsPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/SQLTransformationsPlugin.php new file mode 100644 index 0000000..616a24e --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/SQLTransformationsPlugin.php @@ -0,0 +1,62 @@ +getOptions($options, $cfg['DefaultTransformations']['Substring']); + + if ($options[1] != 'all') { + $newtext = mb_substr( + $buffer, + $options[0], + $options[1] + ); + } else { + $newtext = mb_substr($buffer, $options[0]); + } + + $length = mb_strlen($newtext); + $baselength = mb_strlen($buffer); + if ($length != $baselength) { + if ($options[0] != 0) { + $newtext = $options[2] . $newtext; + } + + if (($length + (int) $options[0]) != $baselength) { + $newtext .= $options[2]; + } + } + + return htmlspecialchars($newtext); + } + + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the transformation name of the specific plugin + * + * @return string + */ + public static function getName() + { + return "Substring"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/TextFileUploadTransformationsPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/TextFileUploadTransformationsPlugin.php new file mode 100644 index 0000000..76021d8 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/TextFileUploadTransformationsPlugin.php @@ -0,0 +1,103 @@ +'; + $html .= ''; + } + $html .= ''; + + return $html; + } + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the transformation name of the specific plugin + * + * @return string + */ + public static function getName() + { + return "Text file upload"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/TextImageLinkTransformationsPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/TextImageLinkTransformationsPlugin.php new file mode 100644 index 0000000..71fdd6a --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/TextImageLinkTransformationsPlugin.php @@ -0,0 +1,75 @@ +getOptions($options, $cfg['DefaultTransformations']['TextImageLink']); + $url = $options[0] . $buffer; + /* Do not allow javascript links */ + if (! Sanitize::checkLink($url, true, true)) { + return htmlspecialchars($url); + } + return '' + . htmlspecialchars($buffer) . ''; + } + + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the transformation name of the specific plugin + * + * @return string + */ + public static function getName() + { + return "Image Link"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/TextLinkTransformationsPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/TextLinkTransformationsPlugin.php new file mode 100644 index 0000000..e29ff2c --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Abs/TextLinkTransformationsPlugin.php @@ -0,0 +1,77 @@ +getOptions($options, $cfg['DefaultTransformations']['TextLink']); + $url = (isset($options[0]) ? $options[0] : '') . ((isset($options[2]) && $options[2]) ? '' : $buffer); + /* Do not allow javascript links */ + if (! Sanitize::checkLink($url, true, true)) { + return htmlspecialchars($url); + } + return '' + . htmlspecialchars(isset($options[1]) ? $options[1] : $buffer) + . ''; + } + + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the transformation name of the specific plugin + * + * @return string + */ + public static function getName() + { + return "TextLink"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Input/Image_JPEG_Upload.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Input/Image_JPEG_Upload.php new file mode 100644 index 0000000..903bfd2 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Input/Image_JPEG_Upload.php @@ -0,0 +1,44 @@ +'; + } + $class = 'transform_IPToBin'; + $html .= ''; + + return $html; + } + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the transformation name of the plugin + * + * @return string + */ + public static function getName() + { + return "IPv4/IPv6 To Binary"; + } + + /** + * Gets the plugin`s MIME type + * + * @return string + */ + public static function getMIMEType() + { + return "Text"; + } + + /** + * Gets the plugin`s MIME subtype + * + * @return string + */ + public static function getMIMESubtype() + { + return "Plain"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Input/Text_Plain_JsonEditor.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Input/Text_Plain_JsonEditor.php new file mode 100644 index 0000000..17d8cf6 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Input/Text_Plain_JsonEditor.php @@ -0,0 +1,85 @@ +getHeader() + ->getScripts(); + $scripts->addFile('vendor/codemirror/lib/codemirror.js'); + $scripts->addFile('vendor/codemirror/mode/javascript/javascript.js'); + $scripts->addFile('vendor/codemirror/addon/runmode/runmode.js'); + $scripts->addFile('transformations/json.js'); + } + } + + /** + * Gets the transformation description of the specific plugin + * + * @return string + */ + public static function getInfo() + { + return __( + 'Formats text as JSON with syntax highlighting.' + ); + } + + /** + * Does the actual work of each specific transformations plugin. + * + * @param string $buffer text to be transformed + * @param array $options transformation options + * @param stdClass|null $meta meta information + * + * @return string + */ + public function applyTransformation($buffer, array $options = [], ?stdClass $meta = null) + { + return '
    ' . "\n"
    +        . htmlspecialchars($buffer) . "\n"
    +        . '
    '; + } + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the plugin`s MIME type + * + * @return string + */ + public static function getMIMEType() + { + return "Text"; + } + + /** + * Gets the plugin`s MIME subtype + * + * @return string + */ + public static function getMIMESubtype() + { + return "Plain"; + } + + /** + * Gets the transformation name of the specific plugin + * + * @return string + */ + public static function getName() + { + return "JSON"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Text_Plain_Sql.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Text_Plain_Sql.php new file mode 100644 index 0000000..2c39e03 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Text_Plain_Sql.php @@ -0,0 +1,60 @@ +getHeader() + ->getScripts(); + $scripts->addFile('vendor/codemirror/lib/codemirror.js'); + $scripts->addFile('vendor/codemirror/mode/sql/sql.js'); + $scripts->addFile('vendor/codemirror/addon/runmode/runmode.js'); + $scripts->addFile('functions.js'); + } + } + + /** + * Gets the plugin`s MIME type + * + * @return string + */ + public static function getMIMEType() + { + return "Text"; + } + + /** + * Gets the plugin`s MIME subtype + * + * @return string + */ + public static function getMIMESubtype() + { + return "Plain"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Text_Plain_Xml.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Text_Plain_Xml.php new file mode 100644 index 0000000..fc06744 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Output/Text_Plain_Xml.php @@ -0,0 +1,101 @@ +getHeader() + ->getScripts(); + $scripts->addFile('vendor/codemirror/lib/codemirror.js'); + $scripts->addFile('vendor/codemirror/mode/xml/xml.js'); + $scripts->addFile('vendor/codemirror/addon/runmode/runmode.js'); + $scripts->addFile('transformations/xml.js'); + } + } + + /** + * Gets the transformation description of the specific plugin + * + * @return string + */ + public static function getInfo() + { + return __( + 'Formats text as XML with syntax highlighting.' + ); + } + + /** + * Does the actual work of each specific transformations plugin. + * + * @param string $buffer text to be transformed + * @param array $options transformation options + * @param stdClass|null $meta meta information + * + * @return string + */ + public function applyTransformation($buffer, array $options = [], ?stdClass $meta = null) + { + return '
    ' . "\n"
    +        . htmlspecialchars($buffer) . "\n"
    +        . '
    '; + } + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the plugin`s MIME type + * + * @return string + */ + public static function getMIMEType() + { + return "Text"; + } + + /** + * Gets the plugin`s MIME subtype + * + * @return string + */ + public static function getMIMESubtype() + { + return "Plain"; + } + + /** + * Gets the transformation name of the specific plugin + * + * @return string + */ + public static function getName() + { + return "XML"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/README b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/README new file mode 100644 index 0000000..7d7a125 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/README @@ -0,0 +1,4 @@ +TRANSFORMATION USAGE (Garvin Hicking, ) +==================== + +See the documentation for complete instructions on how to use transformation plugins. diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/TEMPLATE b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/TEMPLATE new file mode 100644 index 0000000..7d2b2ff --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/TEMPLATE @@ -0,0 +1,45 @@ + diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/TEMPLATE_ABSTRACT b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/TEMPLATE_ABSTRACT new file mode 100644 index 0000000..4087ac9 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/TEMPLATE_ABSTRACT @@ -0,0 +1,73 @@ +mimetype contains the original MimeType of the field (i.e. 'text/plain', 'image/jpeg' etc.) + + return $buffer; + } + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + + /** + * Gets the TransformationName of the specific plugin + * + * @return string + */ + public static function getName() + { + return "[TransformationName]"; + } +} +?> diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Text_Plain_Link.php b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Text_Plain_Link.php new file mode 100644 index 0000000..298919b --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/Transformations/Text_Plain_Link.php @@ -0,0 +1,43 @@ + $value) { + if (isset($options[$key]) && $options[$key] !== '') { + $result[$key] = $options[$key]; + } else { + $result[$key] = $value; + } + } + + return $result; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/TwoFactor/Application.php b/srcs/phpmyadmin/libraries/classes/Plugins/TwoFactor/Application.php new file mode 100644 index 0000000..15e99b9 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/TwoFactor/Application.php @@ -0,0 +1,162 @@ +_google2fa = new Google2FA(); + } else { + $this->_google2fa = new Google2FA(new SvgImageBackEnd()); + } + $this->_google2fa->setWindow(8); + if (! isset($this->_twofactor->config['settings']['secret'])) { + $this->_twofactor->config['settings']['secret'] = ''; + } + } + + /** + * Get any property of this class + * + * @param string $property name of the property + * + * @return mixed|void if property exist, value of the relevant property + */ + public function __get($property) + { + switch ($property) { + case 'google2fa': + return $this->_google2fa; + } + } + + /** + * Checks authentication, returns true on success + * + * @return boolean + * @throws IncompatibleWithGoogleAuthenticatorException + * @throws InvalidCharactersException + * @throws SecretKeyTooShortException + */ + public function check() + { + $this->_provided = false; + if (! isset($_POST['2fa_code'])) { + return false; + } + $this->_provided = true; + return $this->_google2fa->verifyKey( + $this->_twofactor->config['settings']['secret'], + $_POST['2fa_code'] + ); + } + + /** + * Renders user interface to enter two-factor authentication + * + * @return string HTML code + */ + public function render() + { + return $this->template->render('login/twofactor/application'); + } + + /** + * Renders user interface to configure two-factor authentication + * + * @return string HTML code + */ + public function setup() + { + $secret = $this->_twofactor->config['settings']['secret']; + $inlineUrl = $this->_google2fa->getQRCodeInline( + 'phpMyAdmin (' . $this->getAppId(false) . ')', + $this->_twofactor->user, + $secret + ); + return $this->template->render('login/twofactor/application_configure', [ + 'image' => $inlineUrl, + 'secret' => $secret, + 'has_imagick' => extension_loaded('imagick'), + ]); + } + + /** + * Performs backend configuration + * + * @return boolean + * @throws IncompatibleWithGoogleAuthenticatorException + * @throws InvalidCharactersException + * @throws SecretKeyTooShortException + */ + public function configure() + { + if (! isset($_SESSION['2fa_application_key'])) { + $_SESSION['2fa_application_key'] = $this->_google2fa->generateSecretKey(); + } + $this->_twofactor->config['settings']['secret'] = $_SESSION['2fa_application_key']; + + $result = $this->check(); + if ($result) { + unset($_SESSION['2fa_application_key']); + } + return $result; + } + + /** + * Get user visible name + * + * @return string + */ + public static function getName() + { + return __('Authentication Application (2FA)'); + } + + /** + * Get user visible description + * + * @return string + */ + public static function getDescription() + { + return __('Provides authentication using HOTP and TOTP applications such as FreeOTP, Google Authenticator or Authy.'); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/TwoFactor/Invalid.php b/srcs/phpmyadmin/libraries/classes/Plugins/TwoFactor/Invalid.php new file mode 100644 index 0000000..31fffc0 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/TwoFactor/Invalid.php @@ -0,0 +1,68 @@ +template->render('login/twofactor/invalid'); + } + + /** + * Get user visible name + * + * @return string + */ + public static function getName() + { + return 'Invalid two-factor authentication'; + } + + /** + * Get user visible description + * + * @return string + */ + public static function getDescription() + { + return 'Error fallback only!'; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/TwoFactor/Key.php b/srcs/phpmyadmin/libraries/classes/Plugins/TwoFactor/Key.php new file mode 100644 index 0000000..cbb118c --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/TwoFactor/Key.php @@ -0,0 +1,213 @@ +_twofactor->config['settings']['registrations'])) { + $this->_twofactor->config['settings']['registrations'] = []; + } + } + + /** + * Returns array of U2F registration objects + * + * @return array + */ + public function getRegistrations() + { + $result = []; + foreach ($this->_twofactor->config['settings']['registrations'] as $index => $data) { + $reg = new stdClass(); + $reg->keyHandle = $data['keyHandle']; + $reg->publicKey = $data['publicKey']; + $reg->certificate = $data['certificate']; + $reg->counter = $data['counter']; + $reg->index = $index; + $result[] = $reg; + } + return $result; + } + + /** + * Checks authentication, returns true on success + * + * @return boolean + */ + public function check() + { + $this->_provided = false; + if (! isset($_POST['u2f_authentication_response']) || ! isset($_SESSION['authenticationRequest'])) { + return false; + } + $this->_provided = true; + try { + $response = json_decode($_POST['u2f_authentication_response']); + if ($response === null) { + return false; + } + $authentication = U2FServer::authenticate( + $_SESSION['authenticationRequest'], + $this->getRegistrations(), + $response + ); + $this->_twofactor->config['settings']['registrations'][$authentication->index]['counter'] = $authentication->counter; + $this->_twofactor->save(); + return true; + } catch (U2FException $e) { + $this->_message = $e->getMessage(); + return false; + } + } + + /** + * Loads needed javascripts into the page + * + * @return void + */ + public function loadScripts() + { + $response = Response::getInstance(); + $scripts = $response->getHeader()->getScripts(); + $scripts->addFile('vendor/u2f-api-polyfill.js'); + $scripts->addFile('u2f.js'); + } + + /** + * Renders user interface to enter two-factor authentication + * + * @return string HTML code + */ + public function render() + { + $request = U2FServer::makeAuthentication( + $this->getRegistrations(), + $this->getAppId(true) + ); + $_SESSION['authenticationRequest'] = $request; + $this->loadScripts(); + return $this->template->render('login/twofactor/key', [ + 'request' => json_encode($request), + 'is_https' => $GLOBALS['PMA_Config']->isHttps(), + ]); + } + + /** + * Renders user interface to configure two-factor authentication + * + * @return string HTML code + * @throws U2FException + * @throws Throwable + * @throws Twig_Error_Loader + * @throws Twig_Error_Runtime + * @throws Twig_Error_Syntax + */ + public function setup() + { + $registrationData = U2FServer::makeRegistration( + $this->getAppId(true), + $this->getRegistrations() + ); + $_SESSION['registrationRequest'] = $registrationData['request']; + + $this->loadScripts(); + return $this->template->render('login/twofactor/key_configure', [ + 'request' => json_encode($registrationData['request']), + 'signatures' => json_encode($registrationData['signatures']), + 'is_https' => $GLOBALS['PMA_Config']->isHttps(), + ]); + } + + /** + * Performs backend configuration + * + * @return boolean + */ + public function configure() + { + $this->_provided = false; + if (! isset($_POST['u2f_registration_response']) || ! isset($_SESSION['registrationRequest'])) { + return false; + } + $this->_provided = true; + try { + $response = json_decode($_POST['u2f_registration_response']); + if ($response === null) { + return false; + } + $registration = U2FServer::register( + $_SESSION['registrationRequest'], + $response + ); + $this->_twofactor->config['settings']['registrations'][] = [ + 'keyHandle' => $registration->getKeyHandle(), + 'publicKey' => $registration->getPublicKey(), + 'certificate' => $registration->getCertificate(), + 'counter' => $registration->getCounter(), + ]; + return true; + } catch (U2FException $e) { + $this->_message = $e->getMessage(); + return false; + } + } + + /** + * Get user visible name + * + * @return string + */ + public static function getName() + { + return __('Hardware Security Key (FIDO U2F)'); + } + + /** + * Get user visible description + * + * @return string + */ + public static function getDescription() + { + return __('Provides authentication using hardware security tokens supporting FIDO U2F.'); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/TwoFactor/Simple.php b/srcs/phpmyadmin/libraries/classes/Plugins/TwoFactor/Simple.php new file mode 100644 index 0000000..721d602 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/TwoFactor/Simple.php @@ -0,0 +1,68 @@ +template->render('login/twofactor/simple'); + } + + /** + * Get user visible name + * + * @return string + */ + public static function getName() + { + return __('Simple two-factor authentication'); + } + + /** + * Get user visible description + * + * @return string + */ + public static function getDescription() + { + return __('For testing purposes only!'); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/TwoFactorPlugin.php b/srcs/phpmyadmin/libraries/classes/Plugins/TwoFactorPlugin.php new file mode 100644 index 0000000..23534a1 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/TwoFactorPlugin.php @@ -0,0 +1,183 @@ +_twofactor = $twofactor; + $this->_provided = false; + $this->_message = ''; + $this->template = new Template(); + } + + /** + * Returns authentication error message + * + * @return string + */ + public function getError() + { + if ($this->_provided) { + if (! empty($this->_message)) { + return Message::rawError( + sprintf(__('Two-factor authentication failed: %s'), $this->_message) + )->getDisplay(); + } + return Message::rawError( + __('Two-factor authentication failed.') + )->getDisplay(); + } + return ''; + } + + /** + * Checks authentication, returns true on success + * + * @return boolean + */ + public function check() + { + return true; + } + + /** + * Renders user interface to enter two-factor authentication + * + * @return string HTML code + */ + public function render() + { + return ''; + } + + /** + * Renders user interface to configure two-factor authentication + * + * @return string HTML code + */ + public function setup() + { + return ''; + } + + /** + * Performs backend configuration + * + * @return boolean + */ + public function configure() + { + return true; + } + + /** + * Get user visible name + * + * @return string + */ + public static function getName() + { + return __('No Two-Factor Authentication'); + } + + /** + * Get user visible description + * + * @return string + */ + public static function getDescription() + { + return __('Login using password only.'); + } + + /** + * Return an applicaiton ID + * + * Either hostname or hostname with scheme. + * + * @param boolean $return_url Whether to generate URL + * + * @return string + */ + public function getAppId($return_url) + { + /** @var Config $PMA_Config */ + global $PMA_Config; + + $url = $PMA_Config->get('PmaAbsoluteUri'); + $parsed = []; + if (! empty($url)) { + $parsed = parse_url($url); + } + if (empty($parsed['scheme'])) { + $parsed['scheme'] = $PMA_Config->isHttps() ? 'https' : 'http'; + } + if (empty($parsed['host'])) { + $parsed['host'] = Core::getenv('HTTP_HOST'); + } + if ($return_url) { + return $parsed['scheme'] . '://' . $parsed['host'] . (! empty($parsed['port']) ? ':' . $parsed['port'] : ''); + } else { + return $parsed['host']; + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Plugins/UploadInterface.php b/srcs/phpmyadmin/libraries/classes/Plugins/UploadInterface.php new file mode 100644 index 0000000..3839aab --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Plugins/UploadInterface.php @@ -0,0 +1,35 @@ +upload plugins + * + * @package PhpMyAdmin + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Plugins; + +/** + * Provides a common interface that will have to implemented by all of the + * import->upload plugins. + * + * @package PhpMyAdmin + */ +interface UploadInterface +{ + /** + * Gets the specific upload ID Key + * + * @return string ID Key + */ + public static function getIdKey(); + + /** + * Returns upload status. + * + * @param string $id upload id + * + * @return array|null + */ + public static function getUploadStatus($id); +} diff --git a/srcs/phpmyadmin/libraries/classes/Properties/Options/Groups/OptionsPropertyMainGroup.php b/srcs/phpmyadmin/libraries/classes/Properties/Options/Groups/OptionsPropertyMainGroup.php new file mode 100644 index 0000000..99d3546 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Properties/Options/Groups/OptionsPropertyMainGroup.php @@ -0,0 +1,35 @@ +_subgroupHeader; + } + + /** + * Sets the subgroup header + * + * @param PropertyItem $subgroupHeader subgroup header + * + * @return void + */ + public function setSubgroupHeader($subgroupHeader) + { + $this->_subgroupHeader = $subgroupHeader; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Properties/Options/Items/BoolPropertyItem.php b/srcs/phpmyadmin/libraries/classes/Properties/Options/Items/BoolPropertyItem.php new file mode 100644 index 0000000..7441243 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Properties/Options/Items/BoolPropertyItem.php @@ -0,0 +1,35 @@ +getProperties() == null + && in_array($property, $this->getProperties(), true) + ) { + return; + } + $this->_properties[] = $property; + } + + /** + * Removes a property from the group of properties + * + * @param OptionsPropertyItem $property the property instance to be removed + * from the group + * + * @return void + */ + public function removeProperty($property) + { + $this->_properties = array_diff( + $this->getProperties(), + [$property] + ); + } + + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the instance of the class + * + * @return OptionsPropertyGroup + */ + public function getGroup() + { + return $this; + } + + /** + * Gets the group of properties + * + * @return array + */ + public function getProperties() + { + return $this->_properties; + } + + /** + * Gets the number of properties + * + * @return int + */ + public function getNrOfProperties() + { + if ($this->_properties === null) { + return 0; + } + return count($this->_properties); + } + + /** + * Countable interface implementation. + * + * @return int + */ + public function count() + { + return $this->getNrOfProperties(); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Properties/Options/OptionsPropertyItem.php b/srcs/phpmyadmin/libraries/classes/Properties/Options/OptionsPropertyItem.php new file mode 100644 index 0000000..829f6d9 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Properties/Options/OptionsPropertyItem.php @@ -0,0 +1,136 @@ +_name = $name; + } + if ($text) { + $this->_text = $text; + } + } + + /* ~~~~~~~~~~~~~~~~~~~~ Getters and Setters ~~~~~~~~~~~~~~~~~~~~ */ + + /** + * Gets the name + * + * @return string + */ + public function getName() + { + return $this->_name; + } + + /** + * Sets the name + * + * @param string $name name + * + * @return void + */ + public function setName($name) + { + $this->_name = $name; + } + + /** + * Gets the text + * + * @return string + */ + public function getText() + { + return $this->_text; + } + + /** + * Sets the text + * + * @param string $text text + * + * @return void + */ + public function setText($text) + { + $this->_text = $text; + } + + /** + * Gets the force parameter + * + * @return string + */ + public function getForce() + { + return $this->_force; + } + + /** + * Sets the force parameter + * + * @param string $force force parameter + * + * @return void + */ + public function setForce($force) + { + $this->_force = $force; + } + + /** + * Returns the property type ( either "options", or "plugin" ). + * + * @return string + */ + public function getPropertyType() + { + return "options"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Properties/Options/OptionsPropertyOneItem.php b/srcs/phpmyadmin/libraries/classes/Properties/Options/OptionsPropertyOneItem.php new file mode 100644 index 0000000..2ffca61 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Properties/Options/OptionsPropertyOneItem.php @@ -0,0 +1,161 @@ +_force_one; + } + + /** + * Sets the force parameter + * + * @param bool $force force parameter + * + * @return void + */ + public function setForce($force) + { + $this->_force_one = $force; + } + + /** + * Gets the values + * + * @return array + */ + public function getValues() + { + return $this->_values; + } + + /** + * Sets the values + * + * @param array $values values + * + * @return void + */ + public function setValues(array $values) + { + $this->_values = $values; + } + + /** + * Gets MySQL documentation pointer + * + * @return string + */ + public function getDoc() + { + return $this->_doc; + } + + /** + * Sets the doc + * + * @param string $doc MySQL documentation pointer + * + * @return void + */ + public function setDoc($doc) + { + $this->_doc = $doc; + } + + /** + * Gets the length + * + * @return int + */ + public function getLen() + { + return $this->_len; + } + + /** + * Sets the length + * + * @param int $len length + * + * @return void + */ + public function setLen($len) + { + $this->_len = $len; + } + + /** + * Gets the size + * + * @return int + */ + public function getSize() + { + return $this->_size; + } + + /** + * Sets the size + * + * @param int $size size + * + * @return void + */ + public function setSize($size) + { + $this->_size = $size; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Properties/Plugins/ExportPluginProperties.php b/srcs/phpmyadmin/libraries/classes/Properties/Plugins/ExportPluginProperties.php new file mode 100644 index 0000000..4ae4e03 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Properties/Plugins/ExportPluginProperties.php @@ -0,0 +1,64 @@ +_forceFile; + } + + /** + * Sets the force file parameter + * + * @param bool $forceFile the force file parameter + * + * @return void + */ + public function setForceFile($forceFile) + { + $this->_forceFile = $forceFile; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Properties/Plugins/ImportPluginProperties.php b/srcs/phpmyadmin/libraries/classes/Properties/Plugins/ImportPluginProperties.php new file mode 100644 index 0000000..b270951 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Properties/Plugins/ImportPluginProperties.php @@ -0,0 +1,33 @@ +_text; + } + + /** + * Sets the text + * + * @param string $text text + * + * @return void + */ + public function setText($text) + { + $this->_text = $text; + } + + /** + * Gets the extension + * + * @return string + */ + public function getExtension() + { + return $this->_extension; + } + + /** + * Sets the extension + * + * @param string $extension extension + * + * @return void + */ + public function setExtension($extension) + { + $this->_extension = $extension; + } + + /** + * Gets the options + * + * @return OptionsPropertyRootGroup + */ + public function getOptions() + { + return $this->_options; + } + + /** + * Sets the options + * + * @param OptionsPropertyRootGroup $options options + * + * @return void + */ + public function setOptions($options) + { + $this->_options = $options; + } + + /** + * Gets the options text + * + * @return string + */ + public function getOptionsText() + { + return $this->_optionsText; + } + + /** + * Sets the options text + * + * @param string $optionsText optionsText + * + * @return void + */ + public function setOptionsText($optionsText) + { + $this->_optionsText = $optionsText; + } + + /** + * Gets the MIME type + * + * @return string + */ + public function getMimeType() + { + return $this->_mimeType; + } + + /** + * Sets the MIME type + * + * @param string $mimeType MIME type + * + * @return void + */ + public function setMimeType($mimeType) + { + $this->_mimeType = $mimeType; + } + + /** + * Returns the property type ( either "options", or "plugin" ). + * + * @return string + */ + public function getPropertyType() + { + return "plugin"; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Properties/Plugins/SchemaPluginProperties.php b/srcs/phpmyadmin/libraries/classes/Properties/Plugins/SchemaPluginProperties.php new file mode 100644 index 0000000..28d187e --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Properties/Plugins/SchemaPluginProperties.php @@ -0,0 +1,46 @@ +relation = new Relation($GLOBALS['dbi']); + $this->_tableType = $type; + $server_id = $GLOBALS['server']; + if (! isset($_SESSION['tmpval'][$this->_tableType . 'Tables'][$server_id]) + ) { + $_SESSION['tmpval'][$this->_tableType . 'Tables'][$server_id] + = $this->_getPmaTable() ? $this->getFromDb() : []; + } + $this->_tables + =& $_SESSION['tmpval'][$this->_tableType . 'Tables'][$server_id]; + } + + /** + * Returns class instance. + * + * @param string $type the table type + * + * @return RecentFavoriteTable + */ + public static function getInstance($type) + { + if (! array_key_exists($type, self::$_instances)) { + self::$_instances[$type] = new RecentFavoriteTable($type); + } + return self::$_instances[$type]; + } + + /** + * Returns the recent/favorite tables array + * + * @return array + */ + public function getTables() + { + return $this->_tables; + } + + /** + * Returns recently used tables or favorite from phpMyAdmin database. + * + * @return array + */ + public function getFromDb() + { + // Read from phpMyAdmin database, if recent tables is not in session + $sql_query + = " SELECT `tables` FROM " . $this->_getPmaTable() . + " WHERE `username` = '" . $GLOBALS['dbi']->escapeString($GLOBALS['cfg']['Server']['user']) . "'"; + + $return = []; + $result = $this->relation->queryAsControlUser($sql_query, false); + if ($result) { + $row = $GLOBALS['dbi']->fetchArray($result); + if (isset($row[0])) { + $return = json_decode($row[0], true); + } + } + return $return; + } + + /** + * Save recent/favorite tables into phpMyAdmin database. + * + * @return true|Message + */ + public function saveToDb() + { + $username = $GLOBALS['cfg']['Server']['user']; + $sql_query + = " REPLACE INTO " . $this->_getPmaTable() . " (`username`, `tables`)" . + " VALUES ('" . $GLOBALS['dbi']->escapeString($username) . "', '" + . $GLOBALS['dbi']->escapeString( + json_encode($this->_tables) + ) . "')"; + + $success = $GLOBALS['dbi']->tryQuery($sql_query, DatabaseInterface::CONNECT_CONTROL); + + if (! $success) { + $error_msg = ''; + switch ($this->_tableType) { + case 'recent': + $error_msg = __('Could not save recent table!'); + break; + + case 'favorite': + $error_msg = __('Could not save favorite table!'); + break; + } + $message = Message::error($error_msg); + $message->addMessage( + Message::rawError( + $GLOBALS['dbi']->getError(DatabaseInterface::CONNECT_CONTROL) + ), + '

    ' + ); + return $message; + } + return true; + } + + /** + * Trim recent.favorite table according to the + * NumRecentTables/NumFavoriteTables configuration. + * + * @return boolean True if trimming occurred + */ + public function trim() + { + $max = max( + $GLOBALS['cfg']['Num' . ucfirst($this->_tableType) . 'Tables'], + 0 + ); + $trimming_occurred = count($this->_tables) > $max; + while (count($this->_tables) > $max) { + array_pop($this->_tables); + } + return $trimming_occurred; + } + + /** + * Return HTML ul. + * + * @return string + */ + public function getHtmlList() + { + $html = ''; + if (count($this->_tables)) { + if ($this->_tableType == 'recent') { + foreach ($this->_tables as $table) { + $html .= '
  • '; + } + } else { + foreach ($this->_tables as $table) { + $html .= ''; + } + } + } else { + $html .= ''; + } + return $html; + } + + /** + * Return HTML. + * + * @return string + */ + public function getHtml() + { + $html = '
    '; + if ($this->_tableType == 'recent') { + $html .= '
      '; + } else { + $html .= '
        '; + } + $html .= $this->getHtmlList(); + $html .= '
    '; + return $html; + } + + /** + * Add recently used or favorite tables. + * + * @param string $db database name where the table is located + * @param string $table table name + * + * @return true|Message True if success, Message if not + */ + public function add($db, $table) + { + // If table does not exist, do not add._getPmaTable() + if (! $GLOBALS['dbi']->getColumns($db, $table)) { + return true; + } + + $table_arr = []; + $table_arr['db'] = $db; + $table_arr['table'] = $table; + + // add only if this is new table + if (! isset($this->_tables[0]) || $this->_tables[0] != $table_arr) { + array_unshift($this->_tables, $table_arr); + $this->_tables = array_merge(array_unique($this->_tables, SORT_REGULAR)); + $this->trim(); + if ($this->_getPmaTable()) { + return $this->saveToDb(); + } + } + return true; + } + + /** + * Removes recent/favorite tables that don't exist. + * + * @param string $db database + * @param string $table table + * + * @return boolean|Message True if invalid and removed, False if not invalid, + * Message if error while removing + */ + public function removeIfInvalid($db, $table) + { + foreach ($this->_tables as $tbl) { + if ($tbl['db'] == $db && $tbl['table'] == $table) { + // TODO Figure out a better way to find the existence of a table + if (! $GLOBALS['dbi']->getColumns($tbl['db'], $tbl['table'])) { + return $this->remove($tbl['db'], $tbl['table']); + } + } + } + return false; + } + + /** + * Remove favorite tables. + * + * @param string $db database name where the table is located + * @param string $table table name + * + * @return true|Message True if success, Message if not + */ + public function remove($db, $table) + { + foreach ($this->_tables as $key => $value) { + if ($value['db'] == $db && $value['table'] == $table) { + unset($this->_tables[$key]); + } + } + if ($this->_getPmaTable()) { + return $this->saveToDb(); + } + return true; + } + + /** + * Generate Html for sync Favorite tables anchor. (from localStorage to pmadb) + * + * @return string + */ + public function getHtmlSyncFavoriteTables() + { + $retval = ''; + $server_id = $GLOBALS['server']; + if ($server_id == 0) { + return ''; + } + $cfgRelation = $this->relation->getRelationsParam(); + // Not to show this once list is synchronized. + if ($cfgRelation['favoritework'] && ! isset($_SESSION['tmpval']['favorites_synced'][$server_id])) { + $params = [ + 'ajax_request' => true, + 'favorite_table' => true, + 'sync_favorite_tables' => true, + ]; + $url = 'db_structure.php' . Url::getCommon($params); + $retval = ''; + } + return $retval; + } + + /** + * Generate Html to update recent tables. + * + * @return string html + */ + public static function getHtmlUpdateRecentTables() + { + $params = [ + 'ajax_request' => true, + 'recent_table' => true, + ]; + $url = 'index.php' . Url::getCommon($params); + $retval = ''; + return $retval; + } + + /** + * Return the name of the configuration storage table + * + * @return string|null pma table name + */ + private function _getPmaTable(): ?string + { + $cfgRelation = $this->relation->getRelationsParam(); + if (! $cfgRelation['recentwork']) { + return null; + } + + if (! empty($cfgRelation['db']) + && ! empty($cfgRelation[$this->_tableType]) + ) { + return Util::backquote($cfgRelation['db']) . "." + . Util::backquote($cfgRelation[$this->_tableType]); + } + return null; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Relation.php b/srcs/phpmyadmin/libraries/classes/Relation.php new file mode 100644 index 0000000..2e05aff --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Relation.php @@ -0,0 +1,2280 @@ +dbi = $dbi; + $this->template = $template ?? new Template(); + } + + /** + * Executes a query as controluser if possible, otherwise as normal user + * + * @param string $sql the query to execute + * @param boolean $show_error whether to display SQL error messages or not + * @param int $options query options + * + * @return resource|boolean the result set, or false if no result set + * + * @access public + * + */ + public function queryAsControlUser($sql, $show_error = true, $options = 0) + { + // Avoid caching of the number of rows affected; for example, this function + // is called for tracking purposes but we want to display the correct number + // of rows affected by the original query, not by the query generated for + // tracking. + $cache_affected_rows = false; + + if ($show_error) { + $result = $this->dbi->query( + $sql, + DatabaseInterface::CONNECT_CONTROL, + $options, + $cache_affected_rows + ); + } else { + $result = @$this->dbi->tryQuery( + $sql, + DatabaseInterface::CONNECT_CONTROL, + $options, + $cache_affected_rows + ); + } // end if... else... + + if ($result) { + return $result; + } + + return false; + } + + /** + * Returns current relation parameters + * + * @return array + */ + public function getRelationsParam() + { + if (empty($_SESSION['relation'][$GLOBALS['server']]) + || empty($_SESSION['relation'][$GLOBALS['server']]['PMA_VERSION']) + || $_SESSION['relation'][$GLOBALS['server']]['PMA_VERSION'] != PMA_VERSION + ) { + $_SESSION['relation'][$GLOBALS['server']] = $this->checkRelationsParam(); + } + + // just for BC but needs to be before getRelationsParamDiagnostic() + // which uses it + $GLOBALS['cfgRelation'] = $_SESSION['relation'][$GLOBALS['server']]; + + return $_SESSION['relation'][$GLOBALS['server']]; + } + + /** + * prints out diagnostic info for pma relation feature + * + * @param array $cfgRelation Relation configuration + * + * @return string + */ + public function getRelationsParamDiagnostic(array $cfgRelation) + { + $retval = '
    '; + + $messages = []; + $messages['error'] = '' + . __('not OK') + . ''; + + $messages['ok'] = '' + . _pgettext('Correctly working', 'OK') + . ''; + + $messages['enabled'] = '' . __('Enabled') . ''; + $messages['disabled'] = '' . __('Disabled') . ''; + + if (strlen((string) $cfgRelation['db']) == 0) { + $retval .= __('Configuration of pmadb…') . ' ' + . $messages['error'] + . Util::showDocu('setup', 'linked-tables') + . '
    ' . "\n" + . __('General relation features') + . ' ' . __('Disabled') + . '' . "\n"; + if ($GLOBALS['cfg']['ZeroConf']) { + if (strlen($GLOBALS['db']) == 0) { + $retval .= $this->getHtmlFixPmaTables(true, true); + } else { + $retval .= $this->getHtmlFixPmaTables(true); + } + } + } else { + $retval .= '' . "\n"; + + if (! $cfgRelation['allworks'] + && $GLOBALS['cfg']['ZeroConf'] + // Avoid showing a "Create missing tables" link if it's a + // problem of missing definition + && $this->arePmadbTablesDefined() + ) { + $retval .= $this->getHtmlFixPmaTables(false); + $retval .= '
    '; + } + + $retval .= $this->getDiagMessageForParameter( + 'pmadb', + $cfgRelation['db'], + $messages, + 'pmadb' + ); + $retval .= $this->getDiagMessageForParameter( + 'relation', + isset($cfgRelation['relation']), + $messages, + 'relation' + ); + $retval .= $this->getDiagMessageForFeature( + __('General relation features'), + 'relwork', + $messages + ); + $retval .= $this->getDiagMessageForParameter( + 'table_info', + isset($cfgRelation['table_info']), + $messages, + 'table_info' + ); + $retval .= $this->getDiagMessageForFeature( + __('Display Features'), + 'displaywork', + $messages + ); + $retval .= $this->getDiagMessageForParameter( + 'table_coords', + isset($cfgRelation['table_coords']), + $messages, + 'table_coords' + ); + $retval .= $this->getDiagMessageForParameter( + 'pdf_pages', + isset($cfgRelation['pdf_pages']), + $messages, + 'pdf_pages' + ); + $retval .= $this->getDiagMessageForFeature( + __('Designer and creation of PDFs'), + 'pdfwork', + $messages + ); + $retval .= $this->getDiagMessageForParameter( + 'column_info', + isset($cfgRelation['column_info']), + $messages, + 'column_info' + ); + $retval .= $this->getDiagMessageForFeature( + __('Displaying Column Comments'), + 'commwork', + $messages, + false + ); + $retval .= $this->getDiagMessageForFeature( + __('Browser transformation'), + 'mimework', + $messages + ); + if ($cfgRelation['commwork'] && ! $cfgRelation['mimework']) { + $retval .= ''; + } + $retval .= $this->getDiagMessageForParameter( + 'bookmarktable', + isset($cfgRelation['bookmark']), + $messages, + 'bookmark' + ); + $retval .= $this->getDiagMessageForFeature( + __('Bookmarked SQL query'), + 'bookmarkwork', + $messages + ); + $retval .= $this->getDiagMessageForParameter( + 'history', + isset($cfgRelation['history']), + $messages, + 'history' + ); + $retval .= $this->getDiagMessageForFeature( + __('SQL history'), + 'historywork', + $messages + ); + $retval .= $this->getDiagMessageForParameter( + 'recent', + isset($cfgRelation['recent']), + $messages, + 'recent' + ); + $retval .= $this->getDiagMessageForFeature( + __('Persistent recently used tables'), + 'recentwork', + $messages + ); + $retval .= $this->getDiagMessageForParameter( + 'favorite', + isset($cfgRelation['favorite']), + $messages, + 'favorite' + ); + $retval .= $this->getDiagMessageForFeature( + __('Persistent favorite tables'), + 'favoritework', + $messages + ); + $retval .= $this->getDiagMessageForParameter( + 'table_uiprefs', + isset($cfgRelation['table_uiprefs']), + $messages, + 'table_uiprefs' + ); + $retval .= $this->getDiagMessageForFeature( + __('Persistent tables\' UI preferences'), + 'uiprefswork', + $messages + ); + $retval .= $this->getDiagMessageForParameter( + 'tracking', + isset($cfgRelation['tracking']), + $messages, + 'tracking' + ); + $retval .= $this->getDiagMessageForFeature( + __('Tracking'), + 'trackingwork', + $messages + ); + $retval .= $this->getDiagMessageForParameter( + 'userconfig', + isset($cfgRelation['userconfig']), + $messages, + 'userconfig' + ); + $retval .= $this->getDiagMessageForFeature( + __('User preferences'), + 'userconfigwork', + $messages + ); + $retval .= $this->getDiagMessageForParameter( + 'users', + isset($cfgRelation['users']), + $messages, + 'users' + ); + $retval .= $this->getDiagMessageForParameter( + 'usergroups', + isset($cfgRelation['usergroups']), + $messages, + 'usergroups' + ); + $retval .= $this->getDiagMessageForFeature( + __('Configurable menus'), + 'menuswork', + $messages + ); + $retval .= $this->getDiagMessageForParameter( + 'navigationhiding', + isset($cfgRelation['navigationhiding']), + $messages, + 'navigationhiding' + ); + $retval .= $this->getDiagMessageForFeature( + __('Hide/show navigation items'), + 'navwork', + $messages + ); + $retval .= $this->getDiagMessageForParameter( + 'savedsearches', + isset($cfgRelation['savedsearches']), + $messages, + 'savedsearches' + ); + $retval .= $this->getDiagMessageForFeature( + __('Saving Query-By-Example searches'), + 'savedsearcheswork', + $messages + ); + $retval .= $this->getDiagMessageForParameter( + 'central_columns', + isset($cfgRelation['central_columns']), + $messages, + 'central_columns' + ); + $retval .= $this->getDiagMessageForFeature( + __('Managing Central list of columns'), + 'centralcolumnswork', + $messages + ); + $retval .= $this->getDiagMessageForParameter( + 'designer_settings', + isset($cfgRelation['designer_settings']), + $messages, + 'designer_settings' + ); + $retval .= $this->getDiagMessageForFeature( + __('Remembering Designer Settings'), + 'designersettingswork', + $messages + ); + $retval .= $this->getDiagMessageForParameter( + 'export_templates', + isset($cfgRelation['export_templates']), + $messages, + 'export_templates' + ); + $retval .= $this->getDiagMessageForFeature( + __('Saving export templates'), + 'exporttemplateswork', + $messages + ); + $retval .= '
    '; + $retval .= __( + 'Please see the documentation on how to' + . ' update your column_info table.' + ); + $retval .= Util::showDocu( + 'config', + 'cfg_Servers_column_info' + ); + $retval .= '
    ' . "\n"; + + if (! $cfgRelation['allworks']) { + $retval .= '

    ' . __('Quick steps to set up advanced features:') + . '

    '; + + $items = []; + $items[] = sprintf( + __( + 'Create the needed tables with the ' + . '%screate_tables.sql.' + ), + htmlspecialchars(SQL_DIR) + ) . ' ' . Util::showDocu('setup', 'linked-tables'); + $items[] = __('Create a pma user and give access to these tables.') . ' ' + . Util::showDocu('config', 'cfg_Servers_controluser'); + $items[] = __( + 'Enable advanced features in configuration file ' + . '(config.inc.php), for example by ' + . 'starting from config.sample.inc.php.' + ) . ' ' . Util::showDocu('setup', 'quick-install'); + $items[] = __( + 'Re-login to phpMyAdmin to load the updated configuration file.' + ); + + $retval .= $this->template->render('list/unordered', ['items' => $items]); + } + } + + return $retval; + } + + /** + * prints out one diagnostic message for a feature + * + * @param string $feature_name feature name in a message string + * @param string $relation_parameter the $GLOBALS['cfgRelation'] parameter to check + * @param array $messages utility messages + * @param boolean $skip_line whether to skip a line after the message + * + * @return string + */ + public function getDiagMessageForFeature( + $feature_name, + $relation_parameter, + array $messages, + $skip_line = true + ) { + $retval = ' ' . $feature_name . ': '; + if (isset($GLOBALS['cfgRelation'][$relation_parameter]) + && $GLOBALS['cfgRelation'][$relation_parameter] + ) { + $retval .= $messages['enabled']; + } else { + $retval .= $messages['disabled']; + } + $retval .= ''; + if ($skip_line) { + $retval .= ' '; + } + return $retval; + } + + /** + * prints out one diagnostic message for a configuration parameter + * + * @param string $parameter config parameter name to display + * @param boolean $relationParameterSet whether this parameter is set + * @param array $messages utility messages + * @param string $docAnchor anchor in documentation + * + * @return string + */ + public function getDiagMessageForParameter( + $parameter, + $relationParameterSet, + array $messages, + $docAnchor + ) { + $retval = ''; + $retval .= '$cfg[\'Servers\'][$i][\'' . $parameter . '\'] ... '; + $retval .= ''; + if ($relationParameterSet) { + $retval .= $messages['ok']; + } else { + $retval .= sprintf( + $messages['error'], + Util::getDocuLink('config', 'cfg_Servers_' . $docAnchor) + ); + } + $retval .= '' . "\n"; + return $retval; + } + + /** + * Defines the relation parameters for the current user + * just a copy of the functions used for relations ;-) + * but added some stuff to check what will work + * + * @access protected + * @return array the relation parameters for the current user + */ + public function checkRelationsParam() + { + $cfgRelation = []; + $cfgRelation['PMA_VERSION'] = PMA_VERSION; + + $workToTable = [ + 'relwork' => 'relation', + 'displaywork' => [ + 'relation', + 'table_info', + ], + 'bookmarkwork' => 'bookmarktable', + 'pdfwork' => [ + 'table_coords', + 'pdf_pages', + ], + 'commwork' => 'column_info', + 'mimework' => 'column_info', + 'historywork' => 'history', + 'recentwork' => 'recent', + 'favoritework' => 'favorite', + 'uiprefswork' => 'table_uiprefs', + 'trackingwork' => 'tracking', + 'userconfigwork' => 'userconfig', + 'menuswork' => [ + 'users', + 'usergroups', + ], + 'navwork' => 'navigationhiding', + 'savedsearcheswork' => 'savedsearches', + 'centralcolumnswork' => 'central_columns', + 'designersettingswork' => 'designer_settings', + 'exporttemplateswork' => 'export_templates', + ]; + + foreach ($workToTable as $work => $table) { + $cfgRelation[$work] = false; + } + $cfgRelation['allworks'] = false; + $cfgRelation['user'] = null; + $cfgRelation['db'] = null; + + if ($GLOBALS['server'] == 0 + || empty($GLOBALS['cfg']['Server']['pmadb']) + || ! $this->dbi->selectDb( + $GLOBALS['cfg']['Server']['pmadb'], + DatabaseInterface::CONNECT_CONTROL + ) + ) { + // No server selected -> no bookmark table + // we return the array with the falses in it, + // to avoid some 'Uninitialized string offset' errors later + $GLOBALS['cfg']['Server']['pmadb'] = false; + return $cfgRelation; + } + + $cfgRelation['user'] = $GLOBALS['cfg']['Server']['user']; + $cfgRelation['db'] = $GLOBALS['cfg']['Server']['pmadb']; + + // Now I just check if all tables that i need are present so I can for + // example enable relations but not pdf... + // I was thinking of checking if they have all required columns but I + // fear it might be too slow + + $tab_query = 'SHOW TABLES FROM ' + . Util::backquote( + $GLOBALS['cfg']['Server']['pmadb'] + ); + $tab_rs = $this->queryAsControlUser( + $tab_query, + false, + DatabaseInterface::QUERY_STORE + ); + + if (! $tab_rs) { + // query failed ... ? + //$GLOBALS['cfg']['Server']['pmadb'] = false; + return $cfgRelation; + } + + while ($curr_table = @$this->dbi->fetchRow($tab_rs)) { + if ($curr_table[0] == $GLOBALS['cfg']['Server']['bookmarktable']) { + $cfgRelation['bookmark'] = $curr_table[0]; + } elseif ($curr_table[0] == $GLOBALS['cfg']['Server']['relation']) { + $cfgRelation['relation'] = $curr_table[0]; + } elseif ($curr_table[0] == $GLOBALS['cfg']['Server']['table_info']) { + $cfgRelation['table_info'] = $curr_table[0]; + } elseif ($curr_table[0] == $GLOBALS['cfg']['Server']['table_coords']) { + $cfgRelation['table_coords'] = $curr_table[0]; + } elseif ($curr_table[0] == $GLOBALS['cfg']['Server']['column_info']) { + $cfgRelation['column_info'] = $curr_table[0]; + } elseif ($curr_table[0] == $GLOBALS['cfg']['Server']['pdf_pages']) { + $cfgRelation['pdf_pages'] = $curr_table[0]; + } elseif ($curr_table[0] == $GLOBALS['cfg']['Server']['history']) { + $cfgRelation['history'] = $curr_table[0]; + } elseif ($curr_table[0] == $GLOBALS['cfg']['Server']['recent']) { + $cfgRelation['recent'] = $curr_table[0]; + } elseif ($curr_table[0] == $GLOBALS['cfg']['Server']['favorite']) { + $cfgRelation['favorite'] = $curr_table[0]; + } elseif ($curr_table[0] == $GLOBALS['cfg']['Server']['table_uiprefs']) { + $cfgRelation['table_uiprefs'] = $curr_table[0]; + } elseif ($curr_table[0] == $GLOBALS['cfg']['Server']['tracking']) { + $cfgRelation['tracking'] = $curr_table[0]; + } elseif ($curr_table[0] == $GLOBALS['cfg']['Server']['userconfig']) { + $cfgRelation['userconfig'] = $curr_table[0]; + } elseif ($curr_table[0] == $GLOBALS['cfg']['Server']['users']) { + $cfgRelation['users'] = $curr_table[0]; + } elseif ($curr_table[0] == $GLOBALS['cfg']['Server']['usergroups']) { + $cfgRelation['usergroups'] = $curr_table[0]; + } elseif ($curr_table[0] == $GLOBALS['cfg']['Server']['navigationhiding']) { + $cfgRelation['navigationhiding'] = $curr_table[0]; + } elseif ($curr_table[0] == $GLOBALS['cfg']['Server']['savedsearches']) { + $cfgRelation['savedsearches'] = $curr_table[0]; + } elseif ($curr_table[0] == $GLOBALS['cfg']['Server']['central_columns']) { + $cfgRelation['central_columns'] = $curr_table[0]; + } elseif ($curr_table[0] == $GLOBALS['cfg']['Server']['designer_settings']) { + $cfgRelation['designer_settings'] = $curr_table[0]; + } elseif ($curr_table[0] == $GLOBALS['cfg']['Server']['export_templates']) { + $cfgRelation['export_templates'] = $curr_table[0]; + } + } // end while + $this->dbi->freeResult($tab_rs); + + if (isset($cfgRelation['relation'])) { + if ($this->canAccessStorageTable($cfgRelation['relation'])) { + $cfgRelation['relwork'] = true; + } + } + + if (isset($cfgRelation['relation']) && isset($cfgRelation['table_info'])) { + if ($this->canAccessStorageTable($cfgRelation['table_info'])) { + $cfgRelation['displaywork'] = true; + } + } + + if (isset($cfgRelation['table_coords']) && isset($cfgRelation['pdf_pages'])) { + if ($this->canAccessStorageTable($cfgRelation['table_coords'])) { + if ($this->canAccessStorageTable($cfgRelation['pdf_pages'])) { + $cfgRelation['pdfwork'] = true; + } + } + } + + if (isset($cfgRelation['column_info'])) { + if ($this->canAccessStorageTable($cfgRelation['column_info'])) { + $cfgRelation['commwork'] = true; + // phpMyAdmin 4.3+ + // Check for input transformations upgrade. + $cfgRelation['mimework'] = $this->tryUpgradeTransformations(); + } + } + + if (isset($cfgRelation['history'])) { + if ($this->canAccessStorageTable($cfgRelation['history'])) { + $cfgRelation['historywork'] = true; + } + } + + if (isset($cfgRelation['recent'])) { + if ($this->canAccessStorageTable($cfgRelation['recent'])) { + $cfgRelation['recentwork'] = true; + } + } + + if (isset($cfgRelation['favorite'])) { + if ($this->canAccessStorageTable($cfgRelation['favorite'])) { + $cfgRelation['favoritework'] = true; + } + } + + if (isset($cfgRelation['table_uiprefs'])) { + if ($this->canAccessStorageTable($cfgRelation['table_uiprefs'])) { + $cfgRelation['uiprefswork'] = true; + } + } + + if (isset($cfgRelation['tracking'])) { + if ($this->canAccessStorageTable($cfgRelation['tracking'])) { + $cfgRelation['trackingwork'] = true; + } + } + + if (isset($cfgRelation['userconfig'])) { + if ($this->canAccessStorageTable($cfgRelation['userconfig'])) { + $cfgRelation['userconfigwork'] = true; + } + } + + if (isset($cfgRelation['bookmark'])) { + if ($this->canAccessStorageTable($cfgRelation['bookmark'])) { + $cfgRelation['bookmarkwork'] = true; + } + } + + if (isset($cfgRelation['users']) && isset($cfgRelation['usergroups'])) { + if ($this->canAccessStorageTable($cfgRelation['users'])) { + if ($this->canAccessStorageTable($cfgRelation['usergroups'])) { + $cfgRelation['menuswork'] = true; + } + } + } + + if (isset($cfgRelation['navigationhiding'])) { + if ($this->canAccessStorageTable($cfgRelation['navigationhiding'])) { + $cfgRelation['navwork'] = true; + } + } + + if (isset($cfgRelation['savedsearches'])) { + if ($this->canAccessStorageTable($cfgRelation['savedsearches'])) { + $cfgRelation['savedsearcheswork'] = true; + } + } + + if (isset($cfgRelation['central_columns'])) { + if ($this->canAccessStorageTable($cfgRelation['central_columns'])) { + $cfgRelation['centralcolumnswork'] = true; + } + } + + if (isset($cfgRelation['designer_settings'])) { + if ($this->canAccessStorageTable($cfgRelation['designer_settings'])) { + $cfgRelation['designersettingswork'] = true; + } + } + + if (isset($cfgRelation['export_templates'])) { + if ($this->canAccessStorageTable($cfgRelation['export_templates'])) { + $cfgRelation['exporttemplateswork'] = true; + } + } + + $allWorks = true; + foreach ($workToTable as $work => $table) { + if (! $cfgRelation[$work]) { + if (is_string($table)) { + if (isset($GLOBALS['cfg']['Server'][$table]) + && $GLOBALS['cfg']['Server'][$table] !== false + ) { + $allWorks = false; + break; + } + } elseif (is_array($table)) { + $oneNull = false; + foreach ($table as $t) { + if (isset($GLOBALS['cfg']['Server'][$t]) + && $GLOBALS['cfg']['Server'][$t] === false + ) { + $oneNull = true; + break; + } + } + if (! $oneNull) { + $allWorks = false; + break; + } + } + } + } + $cfgRelation['allworks'] = $allWorks; + + return $cfgRelation; + } + + /** + * Check if the table is accessible + * + * @param string $tableDbName The table or table.db + * @return boolean The table is accessible + */ + public function canAccessStorageTable($tableDbName) + { + $result = $this->queryAsControlUser( + 'SELECT NULL FROM ' . $tableDbName . ' LIMIT 0', + false, + DatabaseInterface::QUERY_STORE + ); + return $result !== false; + } + + /** + * Check whether column_info table input transformation + * upgrade is required and try to upgrade silently + * + * @return bool false if upgrade failed + * + * @access public + */ + public function tryUpgradeTransformations() + { + // From 4.3, new input oriented transformation feature was introduced. + // Check whether column_info table has input transformation columns + $new_cols = [ + "input_transformation", + "input_transformation_options", + ]; + $query = 'SHOW COLUMNS FROM ' + . Util::backquote($GLOBALS['cfg']['Server']['pmadb']) + . '.' . Util::backquote( + $GLOBALS['cfg']['Server']['column_info'] + ) + . ' WHERE Field IN (\'' . implode('\', \'', $new_cols) . '\')'; + $result = $this->queryAsControlUser( + $query, + false, + DatabaseInterface::QUERY_STORE + ); + if ($result) { + $rows = $this->dbi->numRows($result); + $this->dbi->freeResult($result); + // input transformations are present + // no need to upgrade + if ($rows === 2) { + return true; + // try silent upgrade without disturbing the user + } + + // read upgrade query file + $query = @file_get_contents(SQL_DIR . 'upgrade_column_info_4_3_0+.sql'); + // replace database name from query to with set in config.inc.php + // replace pma__column_info table name from query + // to with set in config.inc.php + $query = str_replace( + [ + '`phpmyadmin`', + '`pma__column_info`', + ], + [ + Util::backquote($GLOBALS['cfg']['Server']['pmadb']), + Util::backquote($GLOBALS['cfg']['Server']['column_info']), + ], + $query + ); + $this->dbi->tryMultiQuery($query, DatabaseInterface::CONNECT_CONTROL); + // skips result sets of query as we are not interested in it + do { + $hasResult = ( + $this->dbi->moreResults(DatabaseInterface::CONNECT_CONTROL) + && $this->dbi->nextResult(DatabaseInterface::CONNECT_CONTROL) + ); + } while ($hasResult); + $error = $this->dbi->getError(DatabaseInterface::CONNECT_CONTROL); + // return true if no error exists otherwise false + return empty($error); + } + // some failure, either in upgrading or something else + // make some noise, time to wake up user. + return false; + } + + /** + * Gets all Relations to foreign tables for a given table or + * optionally a given column in a table + * + * @param string $db the name of the db to check for + * @param string $table the name of the table to check for + * @param string $column the name of the column to check for + * @param string $source the source for foreign key information + * + * @return array db,table,column + * + * @access public + */ + public function getForeigners($db, $table, $column = '', $source = 'both') + { + $cfgRelation = $this->getRelationsParam(); + $foreign = []; + + if ($cfgRelation['relwork'] && ($source == 'both' || $source == 'internal')) { + $rel_query = ' + SELECT `master_field`, + `foreign_db`, + `foreign_table`, + `foreign_field` + FROM ' . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['relation']) . ' + WHERE `master_db` = \'' . $this->dbi->escapeString($db) . '\' + AND `master_table` = \'' . $this->dbi->escapeString($table) + . '\' '; + if (strlen($column) > 0) { + $rel_query .= ' AND `master_field` = ' + . '\'' . $this->dbi->escapeString($column) . '\''; + } + $foreign = $this->dbi->fetchResult( + $rel_query, + 'master_field', + null, + DatabaseInterface::CONNECT_CONTROL + ); + } + + if (($source == 'both' || $source == 'foreign') && strlen($table) > 0) { + $tableObj = new Table($table, $db); + $show_create_table = $tableObj->showCreate(); + if ($show_create_table) { + $parser = new Parser($show_create_table); + /** + * @var CreateStatement $stmt + */ + $stmt = $parser->statements[0]; + $foreign['foreign_keys_data'] = TableUtils::getForeignKeys( + $stmt + ); + } + } + + /** + * Emulating relations for some information_schema tables + */ + $isInformationSchema = mb_strtolower($db) == 'information_schema'; + $isMysql = mb_strtolower($db) == 'mysql'; + if (($isInformationSchema || $isMysql) + && ($source == 'internal' || $source == 'both') + ) { + if ($isInformationSchema) { + $internalRelations = InternalRelations::getInformationSchema(); + } else { + $internalRelations = InternalRelations::getMySql(); + } + if (isset($internalRelations[$table])) { + foreach ($internalRelations[$table] as $field => $relations) { + if ((strlen($column) === 0 || $column == $field) + && (! isset($foreign[$field]) + || strlen($foreign[$field]) === 0) + ) { + $foreign[$field] = $relations; + } + } + } + } + + return $foreign; + } + + /** + * Gets the display field of a table + * + * @param string $db the name of the db to check for + * @param string $table the name of the table to check for + * + * @return string|false field name or false + * + * @access public + */ + public function getDisplayField($db, $table) + { + $cfgRelation = $this->getRelationsParam(); + + /** + * Try to fetch the display field from DB. + */ + if ($cfgRelation['displaywork']) { + $disp_query = ' + SELECT `display_field` + FROM ' . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['table_info']) . ' + WHERE `db_name` = \'' . $this->dbi->escapeString((string) $db) . '\' + AND `table_name` = \'' . $this->dbi->escapeString((string) $table) + . '\''; + + $row = $this->dbi->fetchSingleRow( + $disp_query, + 'ASSOC', + DatabaseInterface::CONNECT_CONTROL + ); + if (isset($row['display_field'])) { + return $row['display_field']; + } + } + + /** + * Emulating the display field for some information_schema tables. + */ + if ($db == 'information_schema') { + switch ($table) { + case 'CHARACTER_SETS': + return 'DESCRIPTION'; + case 'TABLES': + return 'TABLE_COMMENT'; + } + } + + /** + * Pick first char field + */ + $columns = $this->dbi->getColumnsFull($db, $table); + if ($columns) { + foreach ($columns as $column) { + if ($this->dbi->types->getTypeClass($column['DATA_TYPE']) == 'CHAR') { + return $column['COLUMN_NAME']; + } + } + } + return false; + } + + /** + * Gets the comments for all columns of a table or the db itself + * + * @param string $db the name of the db to check for + * @param string $table the name of the table to check for + * + * @return array [column_name] = comment + * + * @access public + */ + public function getComments($db, $table = '') + { + $comments = []; + + if ($table != '') { + // MySQL native column comments + $columns = $this->dbi->getColumns($db, $table, null, true); + if ($columns) { + foreach ($columns as $column) { + if (! empty($column['Comment'])) { + $comments[$column['Field']] = $column['Comment']; + } + } + } + } else { + $comments[] = $this->getDbComment($db); + } + + return $comments; + } + + /** + * Gets the comment for a db + * + * @param string $db the name of the db to check for + * + * @return string comment + * + * @access public + */ + public function getDbComment($db) + { + $cfgRelation = $this->getRelationsParam(); + $comment = ''; + + if ($cfgRelation['commwork']) { + // pmadb internal db comment + $com_qry = " + SELECT `comment` + FROM " . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['column_info']) + . " + WHERE db_name = '" . $this->dbi->escapeString($db) . "' + AND table_name = '' + AND column_name = '(db_comment)'"; + $com_rs = $this->queryAsControlUser( + $com_qry, + false, + DatabaseInterface::QUERY_STORE + ); + + if ($com_rs && $this->dbi->numRows($com_rs) > 0) { + $row = $this->dbi->fetchAssoc($com_rs); + $comment = $row['comment']; + } + $this->dbi->freeResult($com_rs); + } + + return $comment; + } + + /** + * Gets the comment for a db + * + * @access public + * + * @return array comments + */ + public function getDbComments() + { + $cfgRelation = $this->getRelationsParam(); + $comments = []; + + if ($cfgRelation['commwork']) { + // pmadb internal db comment + $com_qry = " + SELECT `db_name`, `comment` + FROM " . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['column_info']) + . " + WHERE `column_name` = '(db_comment)'"; + $com_rs = $this->queryAsControlUser( + $com_qry, + false, + DatabaseInterface::QUERY_STORE + ); + + if ($com_rs && $this->dbi->numRows($com_rs) > 0) { + while ($row = $this->dbi->fetchAssoc($com_rs)) { + $comments[$row['db_name']] = $row['comment']; + } + } + $this->dbi->freeResult($com_rs); + } + + return $comments; + } + + /** + * Set a database comment to a certain value. + * + * @param string $db the name of the db + * @param string $comment the value of the column + * + * @return boolean true, if comment-query was made. + * + * @access public + */ + public function setDbComment($db, $comment = '') + { + $cfgRelation = $this->getRelationsParam(); + + if (! $cfgRelation['commwork']) { + return false; + } + + if (strlen($comment) > 0) { + $upd_query = 'INSERT INTO ' + . Util::backquote($cfgRelation['db']) . '.' + . Util::backquote($cfgRelation['column_info']) + . ' (`db_name`, `table_name`, `column_name`, `comment`)' + . ' VALUES (\'' + . $this->dbi->escapeString($db) + . "', '', '(db_comment)', '" + . $this->dbi->escapeString($comment) + . "') " + . ' ON DUPLICATE KEY UPDATE ' + . "`comment` = '" . $this->dbi->escapeString($comment) . "'"; + } else { + $upd_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) . '.' + . Util::backquote($cfgRelation['column_info']) + . ' WHERE `db_name` = \'' . $this->dbi->escapeString($db) + . '\' + AND `table_name` = \'\' + AND `column_name` = \'(db_comment)\''; + } + + return $this->queryAsControlUser($upd_query); + } + + /** + * Set a SQL history entry + * + * @param string $db the name of the db + * @param string $table the name of the table + * @param string $username the username + * @param string $sqlquery the sql query + * + * @return void + * + * @access public + */ + public function setHistory($db, $table, $username, $sqlquery) + { + $maxCharactersInDisplayedSQL = $GLOBALS['cfg']['MaxCharactersInDisplayedSQL']; + // Prevent to run this automatically on Footer class destroying in testsuite + if (defined('TESTSUITE') + || mb_strlen($sqlquery) > $maxCharactersInDisplayedSQL + ) { + return; + } + + $cfgRelation = $this->getRelationsParam(); + + if (! isset($_SESSION['sql_history'])) { + $_SESSION['sql_history'] = []; + } + + $_SESSION['sql_history'][] = [ + 'db' => $db, + 'table' => $table, + 'sqlquery' => $sqlquery, + ]; + + if (count($_SESSION['sql_history']) > $GLOBALS['cfg']['QueryHistoryMax']) { + // history should not exceed a maximum count + array_shift($_SESSION['sql_history']); + } + + if (! $cfgRelation['historywork'] || ! $GLOBALS['cfg']['QueryHistoryDB']) { + return; + } + + $this->queryAsControlUser( + 'INSERT INTO ' + . Util::backquote($cfgRelation['db']) . '.' + . Util::backquote($cfgRelation['history']) . ' + (`username`, + `db`, + `table`, + `timevalue`, + `sqlquery`) + VALUES + (\'' . $this->dbi->escapeString($username) . '\', + \'' . $this->dbi->escapeString($db) . '\', + \'' . $this->dbi->escapeString($table) . '\', + NOW(), + \'' . $this->dbi->escapeString($sqlquery) . '\')' + ); + + $this->purgeHistory($username); + } + + /** + * Gets a SQL history entry + * + * @param string $username the username + * + * @return array|bool list of history items + * + * @access public + */ + public function getHistory($username) + { + $cfgRelation = $this->getRelationsParam(); + + if (! $cfgRelation['historywork']) { + return false; + } + + /** + * if db-based history is disabled but there exists a session-based + * history, use it + */ + if (! $GLOBALS['cfg']['QueryHistoryDB']) { + if (isset($_SESSION['sql_history'])) { + return array_reverse($_SESSION['sql_history']); + } + return false; + } + + $hist_query = ' + SELECT `db`, + `table`, + `sqlquery`, + `timevalue` + FROM ' . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['history']) . ' + WHERE `username` = \'' . $this->dbi->escapeString($username) . '\' + ORDER BY `id` DESC'; + + return $this->dbi->fetchResult( + $hist_query, + null, + null, + DatabaseInterface::CONNECT_CONTROL + ); + } + + /** + * purges SQL history + * + * deletes entries that exceeds $cfg['QueryHistoryMax'], oldest first, for the + * given user + * + * @param string $username the username + * + * @return void + * + * @access public + */ + public function purgeHistory($username) + { + $cfgRelation = $this->getRelationsParam(); + if (! $GLOBALS['cfg']['QueryHistoryDB'] || ! $cfgRelation['historywork']) { + return; + } + + if (! $cfgRelation['historywork']) { + return; + } + + $search_query = ' + SELECT `timevalue` + FROM ' . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['history']) . ' + WHERE `username` = \'' . $this->dbi->escapeString($username) . '\' + ORDER BY `timevalue` DESC + LIMIT ' . $GLOBALS['cfg']['QueryHistoryMax'] . ', 1'; + + if ($max_time = $this->dbi->fetchValue( + $search_query, + 0, + 0, + DatabaseInterface::CONNECT_CONTROL + )) { + $this->queryAsControlUser( + 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) . '.' + . Util::backquote($cfgRelation['history']) . ' + WHERE `username` = \'' . $this->dbi->escapeString($username) + . '\' + AND `timevalue` <= \'' . $max_time . '\'' + ); + } + } + + /** + * Prepares the dropdown for one mode + * + * @param array $foreign the keys and values for foreigns + * @param string $data the current data of the dropdown + * @param string $mode the needed mode + * + * @return array the '; + } elseif ($mode == 'id-content') { + $reloptions[] = $reloption . '>' + . $key . ' - ' . $value . ''; + } elseif ($mode == 'id-only') { + $reloptions[] = $reloption . '>' + . $key . ''; + } + } // end foreach + + return $reloptions; + } + + /** + * Outputs dropdown with values of foreign fields + * + * @param array $disp_row array of the displayed row + * @param string $foreign_field the foreign field + * @param string $foreign_display the foreign field to display + * @param string $data the current data of the dropdown (field in row) + * @param int $max maximum number of items in the dropdown + * + * @return string the '; + $top_count = count($top); + if ($max == -1 || $top_count < $max) { + $ret .= implode('', $top); + if ($foreign_display && $top_count > 0) { + // this empty option is to visually mark the beginning of the + // second series of values (bottom) + $ret .= ''; + } + } + if ($foreign_display) { + $ret .= implode('', $bottom); + } + + return $ret; + } + + /** + * Gets foreign keys in preparation for a drop-down selector + * + * @param array|boolean $foreigners array of the foreign keys + * @param string $field the foreign field name + * @param bool $override_total whether to override the total + * @param string $foreign_filter a possible filter + * @param string $foreign_limit a possible LIMIT clause + * @param bool $get_total optional, whether to get total num of rows + * in $foreignData['the_total;] + * (has an effect of performance) + * + * @return array data about the foreign keys + * + * @access public + */ + public function getForeignData( + $foreigners, + $field, + $override_total, + $foreign_filter, + $foreign_limit, + $get_total = false + ) { + // we always show the foreign field in the drop-down; if a display + // field is defined, we show it besides the foreign field + $foreign_link = false; + do { + if (! $foreigners) { + break; + } + $foreigner = $this->searchColumnInForeigners($foreigners, $field); + if ($foreigner != false) { + $foreign_db = $foreigner['foreign_db']; + $foreign_table = $foreigner['foreign_table']; + $foreign_field = $foreigner['foreign_field']; + } else { + break; + } + + // Count number of rows in the foreign table. Currently we do + // not use a drop-down if more than ForeignKeyMaxLimit rows in the + // foreign table, + // for speed reasons and because we need a better interface for this. + // + // We could also do the SELECT anyway, with a LIMIT, and ensure that + // the current value of the field is one of the choices. + + // Check if table has more rows than specified by + // $GLOBALS['cfg']['ForeignKeyMaxLimit'] + $moreThanLimit = $this->dbi->getTable($foreign_db, $foreign_table) + ->checkIfMinRecordsExist($GLOBALS['cfg']['ForeignKeyMaxLimit']); + + if ($override_total === true + || ! $moreThanLimit + ) { + // foreign_display can be false if no display field defined: + $foreign_display = $this->getDisplayField($foreign_db, $foreign_table); + + $f_query_main = 'SELECT ' . Util::backquote($foreign_field) + . ( + $foreign_display === false + ? '' + : ', ' . Util::backquote($foreign_display) + ); + $f_query_from = ' FROM ' . Util::backquote($foreign_db) + . '.' . Util::backquote($foreign_table); + $f_query_filter = empty($foreign_filter) ? '' : ' WHERE ' + . Util::backquote($foreign_field) + . ' LIKE "%' . $this->dbi->escapeString($foreign_filter) . '%"' + . ( + $foreign_display === false + ? '' + : ' OR ' . Util::backquote($foreign_display) + . ' LIKE "%' . $this->dbi->escapeString($foreign_filter) + . '%"' + ); + $f_query_order = $foreign_display === false ? '' : ' ORDER BY ' + . Util::backquote($foreign_table) . '.' + . Util::backquote($foreign_display); + + $f_query_limit = ! empty($foreign_limit) ? $foreign_limit : ''; + + if (! empty($foreign_filter)) { + $the_total = $this->dbi->fetchValue( + 'SELECT COUNT(*)' . $f_query_from . $f_query_filter + ); + if ($the_total === false) { + $the_total = 0; + } + } + + $disp = $this->dbi->tryQuery( + $f_query_main . $f_query_from . $f_query_filter + . $f_query_order . $f_query_limit + ); + if ($disp && $this->dbi->numRows($disp) > 0) { + // If a resultset has been created, pre-cache it in the $disp_row + // array. This helps us from not needing to use mysql_data_seek by + // accessing a pre-cached PHP array. Usually those resultsets are + // not that big, so a performance hit should not be expected. + $disp_row = []; + while ($single_disp_row = @$this->dbi->fetchAssoc($disp)) { + $disp_row[] = $single_disp_row; + } + @$this->dbi->freeResult($disp); + } else { + // Either no data in the foreign table or + // user does not have select permission to foreign table/field + // Show an input field with a 'Browse foreign values' link + $disp_row = null; + $foreign_link = true; + } + } else { + $disp_row = null; + $foreign_link = true; + } + } while (false); + + if ($get_total) { + $the_total = $this->dbi->getTable($foreign_db, $foreign_table) + ->countRecords(true); + } + + $foreignData = []; + $foreignData['foreign_link'] = $foreign_link; + $foreignData['the_total'] = isset($the_total) ? $the_total : null; + $foreignData['foreign_display'] = ( + isset($foreign_display) ? $foreign_display : null + ); + $foreignData['disp_row'] = isset($disp_row) ? $disp_row : null; + $foreignData['foreign_field'] = isset($foreign_field) ? $foreign_field : null; + + return $foreignData; + } + + /** + * Rename a field in relation tables + * + * usually called after a column in a table was renamed + * + * @param string $db database name + * @param string $table table name + * @param string $field old field name + * @param string $new_name new field name + * + * @return void + */ + public function renameField($db, $table, $field, $new_name) + { + $cfgRelation = $this->getRelationsParam(); + + if ($cfgRelation['displaywork']) { + $table_query = 'UPDATE ' + . Util::backquote($cfgRelation['db']) . '.' + . Util::backquote($cfgRelation['table_info']) + . ' SET display_field = \'' . $this->dbi->escapeString( + $new_name + ) . '\'' + . ' WHERE db_name = \'' . $this->dbi->escapeString($db) + . '\'' + . ' AND table_name = \'' . $this->dbi->escapeString($table) + . '\'' + . ' AND display_field = \'' . $this->dbi->escapeString($field) + . '\''; + $this->queryAsControlUser($table_query); + } + + if ($cfgRelation['relwork']) { + $table_query = 'UPDATE ' + . Util::backquote($cfgRelation['db']) . '.' + . Util::backquote($cfgRelation['relation']) + . ' SET master_field = \'' . $this->dbi->escapeString( + $new_name + ) . '\'' + . ' WHERE master_db = \'' . $this->dbi->escapeString($db) + . '\'' + . ' AND master_table = \'' . $this->dbi->escapeString($table) + . '\'' + . ' AND master_field = \'' . $this->dbi->escapeString($field) + . '\''; + $this->queryAsControlUser($table_query); + + $table_query = 'UPDATE ' + . Util::backquote($cfgRelation['db']) . '.' + . Util::backquote($cfgRelation['relation']) + . ' SET foreign_field = \'' . $this->dbi->escapeString( + $new_name + ) . '\'' + . ' WHERE foreign_db = \'' . $this->dbi->escapeString($db) + . '\'' + . ' AND foreign_table = \'' . $this->dbi->escapeString($table) + . '\'' + . ' AND foreign_field = \'' . $this->dbi->escapeString($field) + . '\''; + $this->queryAsControlUser($table_query); + } + } + + + /** + * Performs SQL query used for renaming table. + * + * @param string $table Relation table to use + * @param string $source_db Source database name + * @param string $target_db Target database name + * @param string $source_table Source table name + * @param string $target_table Target table name + * @param string $db_field Name of database field + * @param string $table_field Name of table field + * + * @return void + */ + public function renameSingleTable( + $table, + $source_db, + $target_db, + $source_table, + $target_table, + $db_field, + $table_field + ) { + $query = 'UPDATE ' + . Util::backquote($GLOBALS['cfgRelation']['db']) . '.' + . Util::backquote($GLOBALS['cfgRelation'][$table]) + . ' SET ' + . $db_field . ' = \'' . $this->dbi->escapeString($target_db) + . '\', ' + . $table_field . ' = \'' . $this->dbi->escapeString($target_table) + . '\'' + . ' WHERE ' + . $db_field . ' = \'' . $this->dbi->escapeString($source_db) . '\'' + . ' AND ' + . $table_field . ' = \'' . $this->dbi->escapeString($source_table) + . '\''; + $this->queryAsControlUser($query); + } + + + /** + * Rename a table in relation tables + * + * usually called after table has been moved + * + * @param string $source_db Source database name + * @param string $target_db Target database name + * @param string $source_table Source table name + * @param string $target_table Target table name + * + * @return void + */ + public function renameTable($source_db, $target_db, $source_table, $target_table) + { + // Move old entries from PMA-DBs to new table + if ($GLOBALS['cfgRelation']['commwork']) { + $this->renameSingleTable( + 'column_info', + $source_db, + $target_db, + $source_table, + $target_table, + 'db_name', + 'table_name' + ); + } + + // updating bookmarks is not possible since only a single table is + // moved, and not the whole DB. + + if ($GLOBALS['cfgRelation']['displaywork']) { + $this->renameSingleTable( + 'table_info', + $source_db, + $target_db, + $source_table, + $target_table, + 'db_name', + 'table_name' + ); + } + + if ($GLOBALS['cfgRelation']['relwork']) { + $this->renameSingleTable( + 'relation', + $source_db, + $target_db, + $source_table, + $target_table, + 'foreign_db', + 'foreign_table' + ); + + $this->renameSingleTable( + 'relation', + $source_db, + $target_db, + $source_table, + $target_table, + 'master_db', + 'master_table' + ); + } + + if ($GLOBALS['cfgRelation']['pdfwork']) { + if ($source_db == $target_db) { + // rename within the database can be handled + $this->renameSingleTable( + 'table_coords', + $source_db, + $target_db, + $source_table, + $target_table, + 'db_name', + 'table_name' + ); + } else { + // if the table is moved out of the database we can no loger keep the + // record for table coordinate + $remove_query = "DELETE FROM " + . Util::backquote($GLOBALS['cfgRelation']['db']) . "." + . Util::backquote($GLOBALS['cfgRelation']['table_coords']) + . " WHERE db_name = '" . $this->dbi->escapeString($source_db) . "'" + . " AND table_name = '" . $this->dbi->escapeString($source_table) + . "'"; + $this->queryAsControlUser($remove_query); + } + } + + if ($GLOBALS['cfgRelation']['uiprefswork']) { + $this->renameSingleTable( + 'table_uiprefs', + $source_db, + $target_db, + $source_table, + $target_table, + 'db_name', + 'table_name' + ); + } + + if ($GLOBALS['cfgRelation']['navwork']) { + // update hidden items inside table + $this->renameSingleTable( + 'navigationhiding', + $source_db, + $target_db, + $source_table, + $target_table, + 'db_name', + 'table_name' + ); + + // update data for hidden table + $query = "UPDATE " + . Util::backquote($GLOBALS['cfgRelation']['db']) . "." + . Util::backquote( + $GLOBALS['cfgRelation']['navigationhiding'] + ) + . " SET db_name = '" . $this->dbi->escapeString($target_db) + . "'," + . " item_name = '" . $this->dbi->escapeString($target_table) + . "'" + . " WHERE db_name = '" . $this->dbi->escapeString($source_db) + . "'" + . " AND item_name = '" . $this->dbi->escapeString($source_table) + . "'" + . " AND item_type = 'table'"; + $this->queryAsControlUser($query); + } + } + + /** + * Create a PDF page + * + * @param string|null $newpage name of the new PDF page + * @param array $cfgRelation Relation configuration + * @param string $db database name + * + * @return int + */ + public function createPage(?string $newpage, array $cfgRelation, $db) + { + if (! isset($newpage) || $newpage == '') { + $newpage = __('no description'); + } + $ins_query = 'INSERT INTO ' + . Util::backquote($GLOBALS['cfgRelation']['db']) . '.' + . Util::backquote($cfgRelation['pdf_pages']) + . ' (db_name, page_descr)' + . ' VALUES (\'' + . $this->dbi->escapeString($db) . '\', \'' + . $this->dbi->escapeString($newpage) . '\')'; + $this->queryAsControlUser($ins_query, false); + + return $this->dbi->insertId(DatabaseInterface::CONNECT_CONTROL); + } + + /** + * Get child table references for a table column. + * This works only if 'DisableIS' is false. An empty array is returned otherwise. + * + * @param string $db name of master table db. + * @param string $table name of master table. + * @param string $column name of master table column. + * + * @return array + */ + public function getChildReferences($db, $table, $column = '') + { + $child_references = []; + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $rel_query = "SELECT `column_name`, `table_name`," + . " `table_schema`, `referenced_column_name`" + . " FROM `information_schema`.`key_column_usage`" + . " WHERE `referenced_table_name` = '" + . $this->dbi->escapeString($table) . "'" + . " AND `referenced_table_schema` = '" + . $this->dbi->escapeString($db) . "'"; + if ($column) { + $rel_query .= " AND `referenced_column_name` = '" + . $this->dbi->escapeString($column) . "'"; + } + + $child_references = $this->dbi->fetchResult( + $rel_query, + [ + 'referenced_column_name', + null, + ] + ); + } + return $child_references; + } + + /** + * Check child table references and foreign key for a table column. + * + * @param string $db name of master table db. + * @param string $table name of master table. + * @param string $column name of master table column. + * @param array|null $foreigners_full foreiners array for the whole table. + * @param array|null $child_references_full child references for the whole table. + * + * @return array telling about references if foreign key. + */ + public function checkChildForeignReferences( + $db, + $table, + $column, + $foreigners_full = null, + $child_references_full = null + ) { + $column_status = []; + $column_status['isEditable'] = false; + $column_status['isReferenced'] = false; + $column_status['isForeignKey'] = false; + $column_status['references'] = []; + + $foreigners = []; + if ($foreigners_full !== null) { + if (isset($foreigners_full[$column])) { + $foreigners[$column] = $foreigners_full[$column]; + } + if (isset($foreigners_full['foreign_keys_data'])) { + $foreigners['foreign_keys_data'] = $foreigners_full['foreign_keys_data']; + } + } else { + $foreigners = $this->getForeigners($db, $table, $column, 'foreign'); + } + $foreigner = $this->searchColumnInForeigners($foreigners, $column); + + $child_references = []; + if ($child_references_full !== null) { + if (isset($child_references_full[$column])) { + $child_references = $child_references_full[$column]; + } + } else { + $child_references = $this->getChildReferences($db, $table, $column); + } + + if (count($child_references) > 0 + || $foreigner + ) { + if (count($child_references) > 0) { + $column_status['isReferenced'] = true; + foreach ($child_references as $columns) { + $column_status['references'][] = Util::backquote($columns['table_schema']) + . '.' . Util::backquote($columns['table_name']); + } + } + + if ($foreigner) { + $column_status['isForeignKey'] = true; + } + } else { + $column_status['isEditable'] = true; + } + + return $column_status; + } + + /** + * Search a table column in foreign data. + * + * @param array $foreigners Table Foreign data + * @param string $column Column name + * + * @return bool|array + */ + public function searchColumnInForeigners(array $foreigners, $column) + { + if (isset($foreigners[$column])) { + return $foreigners[$column]; + } + + $foreigner = []; + foreach ($foreigners['foreign_keys_data'] as $one_key) { + $column_index = array_search($column, $one_key['index_list']); + if ($column_index !== false) { + $foreigner['foreign_field'] + = $one_key['ref_index_list'][$column_index]; + $foreigner['foreign_db'] = isset($one_key['ref_db_name']) + ? $one_key['ref_db_name'] + : $GLOBALS['db']; + $foreigner['foreign_table'] = $one_key['ref_table_name']; + $foreigner['constraint'] = $one_key['constraint']; + $foreigner['on_update'] = isset($one_key['on_update']) + ? $one_key['on_update'] + : 'RESTRICT'; + $foreigner['on_delete'] = isset($one_key['on_delete']) + ? $one_key['on_delete'] + : 'RESTRICT'; + + return $foreigner; + } + } + + return false; + } + + /** + * Returns default PMA table names and their create queries. + * + * @return array table name, create query + */ + public function getDefaultPmaTableNames() + { + $pma_tables = []; + $create_tables_file = file_get_contents( + SQL_DIR . 'create_tables.sql' + ); + + $queries = explode(';', $create_tables_file); + + foreach ($queries as $query) { + if (preg_match( + '/CREATE TABLE IF NOT EXISTS `(.*)` \(/', + $query, + $table + ) + ) { + $pma_tables[$table[1]] = $query . ';'; + } + } + + return $pma_tables; + } + + /** + * Create a table named phpmyadmin to be used as configuration storage + * + * @return bool + */ + public function createPmaDatabase() + { + $this->dbi->tryQuery("CREATE DATABASE IF NOT EXISTS `phpmyadmin`"); + if ($error = $this->dbi->getError()) { + if ($GLOBALS['errno'] == 1044) { + $GLOBALS['message'] = __( + 'You do not have necessary privileges to create a database named' + . ' \'phpmyadmin\'. You may go to \'Operations\' tab of any' + . ' database to set up the phpMyAdmin configuration storage there.' + ); + } else { + $GLOBALS['message'] = $error; + } + return false; + } + return true; + } + + /** + * Creates PMA tables in the given db, updates if already exists. + * + * @param string $db database + * @param boolean $create whether to create tables if they don't exist. + * + * @return void + */ + public function fixPmaTables($db, $create = true) + { + $tablesToFeatures = [ + 'pma__bookmark' => 'bookmarktable', + 'pma__relation' => 'relation', + 'pma__table_info' => 'table_info', + 'pma__table_coords' => 'table_coords', + 'pma__pdf_pages' => 'pdf_pages', + 'pma__column_info' => 'column_info', + 'pma__history' => 'history', + 'pma__recent' => 'recent', + 'pma__favorite' => 'favorite', + 'pma__table_uiprefs' => 'table_uiprefs', + 'pma__tracking' => 'tracking', + 'pma__userconfig' => 'userconfig', + 'pma__users' => 'users', + 'pma__usergroups' => 'usergroups', + 'pma__navigationhiding' => 'navigationhiding', + 'pma__savedsearches' => 'savedsearches', + 'pma__central_columns' => 'central_columns', + 'pma__designer_settings' => 'designer_settings', + 'pma__export_templates' => 'export_templates', + ]; + + $existingTables = $this->dbi->getTables($db, DatabaseInterface::CONNECT_CONTROL); + + $createQueries = null; + $foundOne = false; + foreach ($tablesToFeatures as $table => $feature) { + if (! in_array($table, $existingTables)) { + if ($create) { + if ($createQueries == null) { // first create + $createQueries = $this->getDefaultPmaTableNames(); + $this->dbi->selectDb($db); + } + $this->dbi->tryQuery($createQueries[$table]); + if ($error = $this->dbi->getError()) { + $GLOBALS['message'] = $error; + return; + } + $foundOne = true; + $GLOBALS['cfg']['Server'][$feature] = $table; + } + } else { + $foundOne = true; + $GLOBALS['cfg']['Server'][$feature] = $table; + } + } + + if (! $foundOne) { + return; + } + $GLOBALS['cfg']['Server']['pmadb'] = $db; + $_SESSION['relation'][$GLOBALS['server']] = $this->checkRelationsParam(); + + $cfgRelation = $this->getRelationsParam(); + if ($cfgRelation['recentwork'] || $cfgRelation['favoritework']) { + // Since configuration storage is updated, we need to + // re-initialize the favorite and recent tables stored in the + // session from the current configuration storage. + if ($cfgRelation['favoritework']) { + $fav_tables = RecentFavoriteTable::getInstance('favorite'); + $_SESSION['tmpval']['favoriteTables'][$GLOBALS['server']] + = $fav_tables->getFromDb(); + } + + if ($cfgRelation['recentwork']) { + $recent_tables = RecentFavoriteTable::getInstance('recent'); + $_SESSION['tmpval']['recentTables'][$GLOBALS['server']] + = $recent_tables->getFromDb(); + } + + // Reload navi panel to update the recent/favorite lists. + $GLOBALS['reload'] = true; + } + } + + /** + * Get Html for PMA tables fixing anchor. + * + * @param boolean $allTables whether to create all tables + * @param boolean $createDb whether to create the pmadb also + * + * @return string Html + */ + public function getHtmlFixPmaTables($allTables, $createDb = false) + { + $retval = ''; + + $url_query = Url::getCommon(['db' => $GLOBALS['db']], ''); + if ($allTables) { + if ($createDb) { + $url_query .= '&goto=db_operations.php&create_pmadb=1'; + $message = Message::notice( + __( + '%sCreate%s a database named \'phpmyadmin\' and setup ' + . 'the phpMyAdmin configuration storage there.' + ) + ); + } else { + $url_query .= '&goto=db_operations.php&fixall_pmadb=1'; + $message = Message::notice( + __( + '%sCreate%s the phpMyAdmin configuration storage in the ' + . 'current database.' + ) + ); + } + } else { + $url_query .= '&goto=db_operations.php&fix_pmadb=1'; + $message = Message::notice( + __('%sCreate%s missing phpMyAdmin configuration storage tables.') + ); + } + $message->addParamHtml(''); + $message->addParamHtml(''); + + $retval .= $message->getDisplay(); + + return $retval; + } + + /** + * Gets the relations info and status, depending on the condition + * + * @param boolean $condition whether to look for foreigners or not + * @param string $db database name + * @param string $table table name + * + * @return array ($res_rel, $have_rel) + */ + public function getRelationsAndStatus($condition, $db, $table) + { + if ($condition) { + // Find which tables are related with the current one and write it in + // an array + $res_rel = $this->getForeigners($db, $table); + + if (count($res_rel) > 0) { + $have_rel = true; + } else { + $have_rel = false; + } + } else { + $have_rel = false; + $res_rel = []; + } // end if + return [ + $res_rel, + $have_rel, + ]; + } + + /** + * Verifies if all the pmadb tables are defined + * + * @return boolean + */ + public function arePmadbTablesDefined() + { + return ! (empty($GLOBALS['cfg']['Server']['bookmarktable']) + || empty($GLOBALS['cfg']['Server']['relation']) + || empty($GLOBALS['cfg']['Server']['table_info']) + || empty($GLOBALS['cfg']['Server']['table_coords']) + || empty($GLOBALS['cfg']['Server']['column_info']) + || empty($GLOBALS['cfg']['Server']['pdf_pages']) + || empty($GLOBALS['cfg']['Server']['history']) + || empty($GLOBALS['cfg']['Server']['recent']) + || empty($GLOBALS['cfg']['Server']['favorite']) + || empty($GLOBALS['cfg']['Server']['table_uiprefs']) + || empty($GLOBALS['cfg']['Server']['tracking']) + || empty($GLOBALS['cfg']['Server']['userconfig']) + || empty($GLOBALS['cfg']['Server']['users']) + || empty($GLOBALS['cfg']['Server']['usergroups']) + || empty($GLOBALS['cfg']['Server']['navigationhiding']) + || empty($GLOBALS['cfg']['Server']['savedsearches']) + || empty($GLOBALS['cfg']['Server']['central_columns']) + || empty($GLOBALS['cfg']['Server']['designer_settings']) + || empty($GLOBALS['cfg']['Server']['export_templates'])); + } + + /** + * Get tables for foreign key constraint + * + * @param string $foreignDb Database name + * @param string $tblStorageEngine Table storage engine + * + * @return array Table names + */ + public function getTables($foreignDb, $tblStorageEngine) + { + $tables = []; + $tablesRows = $this->dbi->query( + 'SHOW TABLE STATUS FROM ' . Util::backquote($foreignDb), + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + while ($row = $this->dbi->fetchRow($tablesRows)) { + if (isset($row[1]) && mb_strtoupper($row[1]) == $tblStorageEngine) { + $tables[] = $row[0]; + } + } + if ($GLOBALS['cfg']['NaturalOrder']) { + usort($tables, 'strnatcasecmp'); + } + return $tables; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/RelationCleanup.php b/srcs/phpmyadmin/libraries/classes/RelationCleanup.php new file mode 100644 index 0000000..ce98ee4 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/RelationCleanup.php @@ -0,0 +1,392 @@ +dbi = $dbi; + $this->relation = $relation; + } + + /** + * Cleanup column related relation stuff + * + * @param string $db database name + * @param string $table table name + * @param string $column column name + * + * @return void + */ + public function column($db, $table, $column) + { + $cfgRelation = $this->relation->getRelationsParam(); + + if ($cfgRelation['commwork']) { + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['column_info']) + . ' WHERE db_name = \'' . $this->dbi->escapeString($db) . '\'' + . ' AND table_name = \'' . $this->dbi->escapeString($table) + . '\'' + . ' AND column_name = \'' . $this->dbi->escapeString($column) + . '\''; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['displaywork']) { + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['table_info']) + . ' WHERE db_name = \'' . $this->dbi->escapeString($db) . '\'' + . ' AND table_name = \'' . $this->dbi->escapeString($table) + . '\'' + . ' AND display_field = \'' . $this->dbi->escapeString($column) + . '\''; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['relwork']) { + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['relation']) + . ' WHERE master_db = \'' . $this->dbi->escapeString($db) + . '\'' + . ' AND master_table = \'' . $this->dbi->escapeString($table) + . '\'' + . ' AND master_field = \'' . $this->dbi->escapeString($column) + . '\''; + $this->relation->queryAsControlUser($remove_query); + + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['relation']) + . ' WHERE foreign_db = \'' . $this->dbi->escapeString($db) + . '\'' + . ' AND foreign_table = \'' . $this->dbi->escapeString($table) + . '\'' + . ' AND foreign_field = \'' . $this->dbi->escapeString($column) + . '\''; + $this->relation->queryAsControlUser($remove_query); + } + } + + /** + * Cleanup table related relation stuff + * + * @param string $db database name + * @param string $table table name + * + * @return void + */ + public function table($db, $table) + { + $cfgRelation = $this->relation->getRelationsParam(); + + if ($cfgRelation['commwork']) { + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['column_info']) + . ' WHERE db_name = \'' . $this->dbi->escapeString($db) . '\'' + . ' AND table_name = \'' . $this->dbi->escapeString($table) + . '\''; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['displaywork']) { + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['table_info']) + . ' WHERE db_name = \'' . $this->dbi->escapeString($db) . '\'' + . ' AND table_name = \'' . $this->dbi->escapeString($table) + . '\''; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['pdfwork']) { + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['table_coords']) + . ' WHERE db_name = \'' . $this->dbi->escapeString($db) . '\'' + . ' AND table_name = \'' . $this->dbi->escapeString($table) + . '\''; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['relwork']) { + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['relation']) + . ' WHERE master_db = \'' . $this->dbi->escapeString($db) + . '\'' + . ' AND master_table = \'' . $this->dbi->escapeString($table) + . '\''; + $this->relation->queryAsControlUser($remove_query); + + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['relation']) + . ' WHERE foreign_db = \'' . $this->dbi->escapeString($db) + . '\'' + . ' AND foreign_table = \'' . $this->dbi->escapeString($table) + . '\''; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['uiprefswork']) { + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['table_uiprefs']) + . ' WHERE db_name = \'' . $this->dbi->escapeString($db) . '\'' + . ' AND table_name = \'' . $this->dbi->escapeString($table) + . '\''; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['navwork']) { + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['navigationhiding']) + . ' WHERE db_name = \'' . $this->dbi->escapeString($db) . '\'' + . ' AND (table_name = \'' . $this->dbi->escapeString($table) + . '\'' + . ' OR (item_name = \'' . $this->dbi->escapeString($table) + . '\'' + . ' AND item_type = \'table\'))'; + $this->relation->queryAsControlUser($remove_query); + } + } + + /** + * Cleanup database related relation stuff + * + * @param string $db database name + * + * @return void + */ + public function database($db) + { + $cfgRelation = $this->relation->getRelationsParam(); + + if ($cfgRelation['commwork']) { + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['column_info']) + . ' WHERE db_name = \'' . $this->dbi->escapeString($db) . '\''; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['bookmarkwork']) { + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['bookmark']) + . ' WHERE dbase = \'' . $this->dbi->escapeString($db) . '\''; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['displaywork']) { + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['table_info']) + . ' WHERE db_name = \'' . $this->dbi->escapeString($db) . '\''; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['pdfwork']) { + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['pdf_pages']) + . ' WHERE db_name = \'' . $this->dbi->escapeString($db) . '\''; + $this->relation->queryAsControlUser($remove_query); + + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['table_coords']) + . ' WHERE db_name = \'' . $this->dbi->escapeString($db) . '\''; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['relwork']) { + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['relation']) + . ' WHERE master_db = \'' + . $this->dbi->escapeString($db) . '\''; + $this->relation->queryAsControlUser($remove_query); + + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['relation']) + . ' WHERE foreign_db = \'' . $this->dbi->escapeString($db) + . '\''; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['uiprefswork']) { + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['table_uiprefs']) + . ' WHERE db_name = \'' . $this->dbi->escapeString($db) . '\''; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['navwork']) { + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['navigationhiding']) + . ' WHERE db_name = \'' . $this->dbi->escapeString($db) . '\''; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['savedsearcheswork']) { + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['savedsearches']) + . ' WHERE db_name = \'' . $this->dbi->escapeString($db) . '\''; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['centralcolumnswork']) { + $remove_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['central_columns']) + . ' WHERE db_name = \'' . $this->dbi->escapeString($db) . '\''; + $this->relation->queryAsControlUser($remove_query); + } + } + + /** + * Cleanup user related relation stuff + * + * @param string $username username + * + * @return void + */ + public function user($username) + { + $cfgRelation = $this->relation->getRelationsParam(); + + if ($cfgRelation['bookmarkwork']) { + $remove_query = "DELETE FROM " + . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['bookmark']) + . " WHERE `user` = '" . $this->dbi->escapeString($username) + . "'"; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['historywork']) { + $remove_query = "DELETE FROM " + . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['history']) + . " WHERE `username` = '" . $this->dbi->escapeString($username) + . "'"; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['recentwork']) { + $remove_query = "DELETE FROM " + . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['recent']) + . " WHERE `username` = '" . $this->dbi->escapeString($username) + . "'"; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['favoritework']) { + $remove_query = "DELETE FROM " + . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['favorite']) + . " WHERE `username` = '" . $this->dbi->escapeString($username) + . "'"; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['uiprefswork']) { + $remove_query = "DELETE FROM " + . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['table_uiprefs']) + . " WHERE `username` = '" . $this->dbi->escapeString($username) + . "'"; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['userconfigwork']) { + $remove_query = "DELETE FROM " + . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['userconfig']) + . " WHERE `username` = '" . $this->dbi->escapeString($username) + . "'"; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['menuswork']) { + $remove_query = "DELETE FROM " + . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['users']) + . " WHERE `username` = '" . $this->dbi->escapeString($username) + . "'"; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['navwork']) { + $remove_query = "DELETE FROM " + . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['navigationhiding']) + . " WHERE `username` = '" . $this->dbi->escapeString($username) + . "'"; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['savedsearcheswork']) { + $remove_query = "DELETE FROM " + . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['savedsearches']) + . " WHERE `username` = '" . $this->dbi->escapeString($username) + . "'"; + $this->relation->queryAsControlUser($remove_query); + } + + if ($cfgRelation['designersettingswork']) { + $remove_query = "DELETE FROM " + . Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['designer_settings']) + . " WHERE `username` = '" . $this->dbi->escapeString($username) + . "'"; + $this->relation->queryAsControlUser($remove_query); + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Replication.php b/srcs/phpmyadmin/libraries/classes/Replication.php new file mode 100644 index 0000000..4fa68f2 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Replication.php @@ -0,0 +1,190 @@ +tryQuery($action . " SLAVE " . $control . ";", $link); + } + + /** + * Changes master for replication slave + * + * @param string $user replication user on master + * @param string $password password for the user + * @param string $host master's hostname or IP + * @param int $port port, where mysql is running + * @param array $pos position of mysql replication, + * array should contain fields File and Position + * @param bool $stop shall we stop slave? + * @param bool $start shall we start slave? + * @param mixed $link mysql link + * + * @return string output of CHANGE MASTER mysql command + */ + public function slaveChangeMaster( + $user, + $password, + $host, + $port, + array $pos, + $stop = true, + $start = true, + $link = null + ) { + if ($stop) { + $this->slaveControl("STOP", null, $link); + } + + $out = $GLOBALS['dbi']->tryQuery( + 'CHANGE MASTER TO ' . + 'MASTER_HOST=\'' . $host . '\',' . + 'MASTER_PORT=' . ($port * 1) . ',' . + 'MASTER_USER=\'' . $user . '\',' . + 'MASTER_PASSWORD=\'' . $password . '\',' . + 'MASTER_LOG_FILE=\'' . $pos["File"] . '\',' . + 'MASTER_LOG_POS=' . $pos["Position"] . ';', + $link + ); + + if ($start) { + $this->slaveControl("START", null, $link); + } + + return $out; + } + + /** + * This function provides connection to remote mysql server + * + * @param string $user mysql username + * @param string $password password for the user + * @param string $host mysql server's hostname or IP + * @param int $port mysql remote port + * @param string $socket path to unix socket + * + * @return mixed mysql link on success + */ + public function connectToMaster( + $user, + $password, + $host = null, + $port = null, + $socket = null + ) { + $server = []; + $server['user'] = $user; + $server['password'] = $password; + $server["host"] = Core::sanitizeMySQLHost($host); + $server["port"] = $port; + $server["socket"] = $socket; + + // 5th parameter set to true means that it's an auxiliary connection + // and we must not go back to login page if it fails + return $GLOBALS['dbi']->connect(DatabaseInterface::CONNECT_AUXILIARY, $server); + } + + /** + * Fetches position and file of current binary log on master + * + * @param mixed $link mysql link + * + * @return array an array containing File and Position in MySQL replication + * on master server, useful for slaveChangeMaster() + */ + public function slaveBinLogMaster($link = null) + { + $data = $GLOBALS['dbi']->fetchResult('SHOW MASTER STATUS', null, null, $link); + $output = []; + + if (! empty($data)) { + $output["File"] = $data[0]["File"]; + $output["Position"] = $data[0]["Position"]; + } + return $output; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/ReplicationGui.php b/srcs/phpmyadmin/libraries/classes/ReplicationGui.php new file mode 100644 index 0000000..a6cbf58 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/ReplicationGui.php @@ -0,0 +1,602 @@ +replication = $replication; + $this->template = $template; + } + + /** + * returns HTML for error message + * + * @return string HTML code + */ + public function getHtmlForErrorMessage() + { + $html = ''; + if (isset($_SESSION['replication']['sr_action_status']) + && isset($_SESSION['replication']['sr_action_info']) + ) { + if ($_SESSION['replication']['sr_action_status'] == 'error') { + $error_message = $_SESSION['replication']['sr_action_info']; + $html .= Message::error($error_message)->getDisplay(); + $_SESSION['replication']['sr_action_status'] = 'unknown'; + } elseif ($_SESSION['replication']['sr_action_status'] == 'success') { + $success_message = $_SESSION['replication']['sr_action_info']; + $html .= Message::success($success_message)->getDisplay(); + $_SESSION['replication']['sr_action_status'] = 'unknown'; + } + } + return $html; + } + + /** + * returns HTML for master replication + * + * @return string HTML code + */ + public function getHtmlForMasterReplication() + { + if (! isset($_POST['repl_clear_scr'])) { + $masterStatusTable = $this->getHtmlForReplicationStatusTable('master', true, false); + $slaves = $GLOBALS['dbi']->fetchResult('SHOW SLAVE HOSTS', null, null); + + $urlParams = $GLOBALS['url_params']; + $urlParams['mr_adduser'] = true; + $urlParams['repl_clear_scr'] = true; + } + + if (isset($_POST['mr_adduser'])) { + $masterAddSlaveUser = $this->getHtmlForReplicationMasterAddSlaveUser(); + } + + return $this->template->render('server/replication/master_replication', [ + 'clear_screen' => isset($_POST['repl_clear_scr']), + 'master_status_table' => $masterStatusTable ?? '', + 'slaves' => $slaves ?? [], + 'url_params' => $urlParams ?? [], + 'master_add_user' => isset($_POST['mr_adduser']), + 'master_add_slave_user' => $masterAddSlaveUser ?? '', + ]); + } + + /** + * returns HTML for master replication configuration + * + * @return string HTML code + */ + public function getHtmlForMasterConfiguration() + { + $databaseMultibox = $this->getHtmlForReplicationDbMultibox(); + + return $this->template->render('server/replication/master_configuration', [ + 'database_multibox' => $databaseMultibox, + ]); + } + + /** + * returns HTML for slave replication configuration + * + * @param bool $serverSlaveStatus Whether it is Master or Slave + * @param array $serverSlaveReplication Slave replication + * + * @return string HTML code + */ + public function getHtmlForSlaveConfiguration( + $serverSlaveStatus, + array $serverSlaveReplication + ) { + $serverSlaveMultiReplication = $GLOBALS['dbi']->fetchResult( + 'SHOW ALL SLAVES STATUS' + ); + if ($serverSlaveStatus) { + $urlParams = $GLOBALS['url_params']; + $urlParams['sr_take_action'] = true; + $urlParams['sr_slave_server_control'] = true; + + if ($serverSlaveReplication[0]['Slave_IO_Running'] == 'No') { + $urlParams['sr_slave_action'] = 'start'; + } else { + $urlParams['sr_slave_action'] = 'stop'; + } + + $urlParams['sr_slave_control_parm'] = 'IO_THREAD'; + $slaveControlIoLink = Url::getCommon($urlParams, ''); + + if ($serverSlaveReplication[0]['Slave_SQL_Running'] == 'No') { + $urlParams['sr_slave_action'] = 'start'; + } else { + $urlParams['sr_slave_action'] = 'stop'; + } + + $urlParams['sr_slave_control_parm'] = 'SQL_THREAD'; + $slaveControlSqlLink = Url::getCommon($urlParams, ''); + + if ($serverSlaveReplication[0]['Slave_IO_Running'] == 'No' + || $serverSlaveReplication[0]['Slave_SQL_Running'] == 'No' + ) { + $urlParams['sr_slave_action'] = 'start'; + } else { + $urlParams['sr_slave_action'] = 'stop'; + } + + $urlParams['sr_slave_control_parm'] = null; + $slaveControlFullLink = Url::getCommon($urlParams, ''); + + $urlParams['sr_slave_action'] = 'reset'; + $slaveControlResetLink = Url::getCommon($urlParams, ''); + + $urlParams = $GLOBALS['url_params']; + $urlParams['sr_take_action'] = true; + $urlParams['sr_slave_skip_error'] = true; + $slaveSkipErrorLink = Url::getCommon($urlParams, ''); + + $urlParams = $GLOBALS['url_params']; + $urlParams['sl_configure'] = true; + $urlParams['repl_clear_scr'] = true; + + $reconfigureMasterLink = Url::getCommon($urlParams, ''); + + $slaveStatusTable = $this->getHtmlForReplicationStatusTable('slave', true, false); + + $slaveIoRunning = $serverSlaveReplication[0]['Slave_IO_Running'] !== 'No'; + $slaveSqlRunning = $serverSlaveReplication[0]['Slave_SQL_Running'] !== 'No'; + } + + return $this->template->render('server/replication/slave_configuration', [ + 'server_slave_multi_replication' => $serverSlaveMultiReplication, + 'url_params' => $GLOBALS['url_params'], + 'master_connection' => $_POST['master_connection'] ?? '', + 'server_slave_status' => $serverSlaveStatus, + 'slave_status_table' => $slaveStatusTable ?? '', + 'slave_sql_running' => $slaveSqlRunning ?? false, + 'slave_io_running' => $slaveIoRunning ?? false, + 'slave_control_full_link' => $slaveControlFullLink ?? '', + 'slave_control_reset_link' => $slaveControlResetLink ?? '', + 'slave_control_sql_link' => $slaveControlSqlLink ?? '', + 'slave_control_io_link' => $slaveControlIoLink ?? '', + 'slave_skip_error_link' => $slaveSkipErrorLink ?? '', + 'reconfigure_master_link' => $reconfigureMasterLink ?? '', + 'has_slave_configure' => isset($_POST['sl_configure']), + ]); + } + + /** + * returns HTML code for selecting databases + * + * @return string HTML code + */ + public function getHtmlForReplicationDbMultibox() + { + $databases = []; + foreach ($GLOBALS['dblist']->databases as $database) { + if (! $GLOBALS['dbi']->isSystemSchema($database)) { + $databases[] = $database; + } + } + + return $this->template->render('server/replication/database_multibox', [ + 'databases' => $databases, + ]); + } + + /** + * returns HTML for changing master + * + * @param string $submitName submit button name + * + * @return string HTML code + */ + public function getHtmlForReplicationChangeMaster($submitName) + { + list( + $usernameLength, + $hostnameLength + ) = $this->getUsernameHostnameLength(); + + return $this->template->render('server/replication/change_master', [ + 'server_id' => time(), + 'username_length' => $usernameLength, + 'hostname_length' => $hostnameLength, + 'submit_name' => $submitName, + ]); + } + + /** + * This function returns html code for table with replication status. + * + * @param string $type either master or slave + * @param boolean $isHidden if true, then default style is set to hidden, + * default value false + * @param boolean $hasTitle if true, then title is displayed, default true + * + * @return string HTML code + */ + public function getHtmlForReplicationStatusTable( + $type, + $isHidden = false, + $hasTitle = true + ): string { + global $master_variables, $slave_variables; + global $master_variables_alerts, $slave_variables_alerts; + global $master_variables_oks, $slave_variables_oks; + global $server_master_replication, $server_slave_replication; + + $replicationVariables = $master_variables; + $variablesAlerts = $master_variables_alerts; + $variablesOks = $master_variables_oks; + $serverReplication = $server_master_replication; + if ($type === 'slave') { + $replicationVariables = $slave_variables; + $variablesAlerts = $slave_variables_alerts; + $variablesOks = $slave_variables_oks; + $serverReplication = $server_slave_replication; + } + + $variables = []; + foreach ($replicationVariables as $variable) { + $serverReplicationVariable = is_array($serverReplication) && isset($serverReplication[0]) ? $serverReplication[0][$variable] : ''; + + $variables[$variable] = [ + 'name' => $variable, + 'status' => '', + 'value' => $serverReplicationVariable, + ]; + + if (isset($variablesAlerts[$variable]) + && $variablesAlerts[$variable] === $serverReplicationVariable + ) { + $variables[$variable]['status'] = 'attention'; + } elseif (isset($variablesOks[$variable]) + && $variablesOks[$variable] === $serverReplicationVariable + ) { + $variables[$variable]['status'] = 'allfine'; + } + + $variablesWrap = [ + 'Replicate_Do_DB', + 'Replicate_Ignore_DB', + 'Replicate_Do_Table', + 'Replicate_Ignore_Table', + 'Replicate_Wild_Do_Table', + 'Replicate_Wild_Ignore_Table', + ]; + if (in_array($variable, $variablesWrap)) { + $variables[$variable]['value'] = str_replace( + ',', + ', ', + $serverReplicationVariable + ); + } + } + + return $this->template->render('server/replication/status_table', [ + 'type' => $type, + 'is_hidden' => $isHidden, + 'has_title' => $hasTitle, + 'variables' => $variables, + ]); + } + + /** + * get the correct username and hostname lengths for this MySQL server + * + * @return array username length, hostname length + */ + public function getUsernameHostnameLength() + { + $fields_info = $GLOBALS['dbi']->getColumns('mysql', 'user'); + $username_length = 16; + $hostname_length = 41; + foreach ($fields_info as $val) { + if ($val['Field'] == 'User') { + strtok($val['Type'], '()'); + $v = strtok('()'); + if (is_int($v)) { + $username_length = $v; + } + } elseif ($val['Field'] == 'Host') { + strtok($val['Type'], '()'); + $v = strtok('()'); + if (is_int($v)) { + $hostname_length = $v; + } + } + } + return [ + $username_length, + $hostname_length, + ]; + } + + /** + * returns html code to add a replication slave user to the master + * + * @return string HTML code + */ + public function getHtmlForReplicationMasterAddSlaveUser() + { + list( + $usernameLength, + $hostnameLength + ) = $this->getUsernameHostnameLength(); + + if (isset($_POST['username']) && strlen($_POST['username']) === 0) { + $GLOBALS['pred_username'] = 'any'; + } + + $username = ''; + if (! empty($_POST['username'])) { + $username = $GLOBALS['new_username'] ?? $_POST['username']; + } + + $currentUser = $GLOBALS['dbi']->fetchValue('SELECT USER();'); + if (! empty($currentUser)) { + $userHost = str_replace( + "'", + '', + mb_substr( + $currentUser, + mb_strrpos($currentUser, '@') + 1 + ) + ); + if ($userHost !== 'localhost' && $userHost !== '127.0.0.1') { + $thisHost = $userHost; + } + } + + // when we start editing a user, $GLOBALS['pred_hostname'] is not defined + if (! isset($GLOBALS['pred_hostname']) && isset($_POST['hostname'])) { + switch (mb_strtolower($_POST['hostname'])) { + case 'localhost': + case '127.0.0.1': + $GLOBALS['pred_hostname'] = 'localhost'; + break; + case '%': + $GLOBALS['pred_hostname'] = 'any'; + break; + default: + $GLOBALS['pred_hostname'] = 'userdefined'; + break; + } + } + + return $this->template->render('server/replication/master_add_slave_user', [ + 'username_length' => $usernameLength, + 'hostname_length' => $hostnameLength, + 'has_username' => isset($_POST['username']), + 'username' => $username, + 'hostname' => $_POST['hostname'] ?? '', + 'predefined_username' => $GLOBALS['pred_username'] ?? '', + 'predefined_hostname' => $GLOBALS['pred_hostname'] ?? '', + 'this_host' => $thisHost ?? null, + ]); + } + + /** + * handle control requests + * + * @return void + */ + public function handleControlRequest() + { + if (isset($_POST['sr_take_action'])) { + $refresh = false; + $result = false; + $messageSuccess = null; + $messageError = null; + + if (isset($_POST['slave_changemaster']) && ! $GLOBALS['cfg']['AllowArbitraryServer']) { + $_SESSION['replication']['sr_action_status'] = 'error'; + $_SESSION['replication']['sr_action_info'] = __('Connection to server is disabled, please enable $cfg[\'AllowArbitraryServer\'] in phpMyAdmin configuration.'); + } elseif (isset($_POST['slave_changemaster'])) { + $result = $this->handleRequestForSlaveChangeMaster(); + } elseif (isset($_POST['sr_slave_server_control'])) { + $result = $this->handleRequestForSlaveServerControl(); + $refresh = true; + + switch ($_POST['sr_slave_action']) { + case 'start': + $messageSuccess = __('Replication started successfully.'); + $messageError = __('Error starting replication.'); + break; + case 'stop': + $messageSuccess = __('Replication stopped successfully.'); + $messageError = __('Error stopping replication.'); + break; + case 'reset': + $messageSuccess = __('Replication resetting successfully.'); + $messageError = __('Error resetting replication.'); + break; + default: + $messageSuccess = __('Success.'); + $messageError = __('Error.'); + break; + } + } elseif (isset($_POST['sr_slave_skip_error'])) { + $result = $this->handleRequestForSlaveSkipError(); + } + + if ($refresh) { + $response = Response::getInstance(); + if ($response->isAjax()) { + $response->setRequestStatus($result); + $response->addJSON( + 'message', + $result + ? Message::success($messageSuccess) + : Message::error($messageError) + ); + } else { + Core::sendHeaderLocation( + './server_replication.php' + . Url::getCommonRaw($GLOBALS['url_params']) + ); + } + } + unset($refresh); + } + } + + /** + * handle control requests for Slave Change Master + * + * @return boolean + */ + public function handleRequestForSlaveChangeMaster() + { + $sr = []; + $_SESSION['replication']['m_username'] = $sr['username'] + = $GLOBALS['dbi']->escapeString($_POST['username']); + $_SESSION['replication']['m_password'] = $sr['pma_pw'] + = $GLOBALS['dbi']->escapeString($_POST['pma_pw']); + $_SESSION['replication']['m_hostname'] = $sr['hostname'] + = $GLOBALS['dbi']->escapeString($_POST['hostname']); + $_SESSION['replication']['m_port'] = $sr['port'] + = $GLOBALS['dbi']->escapeString($_POST['text_port']); + $_SESSION['replication']['m_correct'] = ''; + $_SESSION['replication']['sr_action_status'] = 'error'; + $_SESSION['replication']['sr_action_info'] = __('Unknown error'); + + // Attempt to connect to the new master server + $link_to_master = $this->replication->connectToMaster( + $sr['username'], + $sr['pma_pw'], + $sr['hostname'], + $sr['port'] + ); + + if (! $link_to_master) { + $_SESSION['replication']['sr_action_status'] = 'error'; + $_SESSION['replication']['sr_action_info'] = sprintf( + __('Unable to connect to master %s.'), + htmlspecialchars($sr['hostname']) + ); + } else { + // Read the current master position + $position = $this->replication->slaveBinLogMaster($link_to_master); + + if (empty($position)) { + $_SESSION['replication']['sr_action_status'] = 'error'; + $_SESSION['replication']['sr_action_info'] + = __( + 'Unable to read master log position. ' + . 'Possible privilege problem on master.' + ); + } else { + $_SESSION['replication']['m_correct'] = true; + + if (! $this->replication->slaveChangeMaster( + $sr['username'], + $sr['pma_pw'], + $sr['hostname'], + $sr['port'], + $position, + true, + false + ) + ) { + $_SESSION['replication']['sr_action_status'] = 'error'; + $_SESSION['replication']['sr_action_info'] + = __('Unable to change master!'); + } else { + $_SESSION['replication']['sr_action_status'] = 'success'; + $_SESSION['replication']['sr_action_info'] = sprintf( + __('Master server changed successfully to %s.'), + htmlspecialchars($sr['hostname']) + ); + } + } + } + + return $_SESSION['replication']['sr_action_status'] === 'success'; + } + + /** + * handle control requests for Slave Server Control + * + * @return boolean + */ + public function handleRequestForSlaveServerControl() + { + if (empty($_POST['sr_slave_control_parm'])) { + $_POST['sr_slave_control_parm'] = null; + } + if ($_POST['sr_slave_action'] == 'reset') { + $qStop = $this->replication->slaveControl("STOP", null, DatabaseInterface::CONNECT_USER); + $qReset = $GLOBALS['dbi']->tryQuery("RESET SLAVE;"); + $qStart = $this->replication->slaveControl("START", null, DatabaseInterface::CONNECT_USER); + + $result = ($qStop !== false && $qStop !== -1 && + $qReset !== false && $qReset !== -1 && + $qStart !== false && $qStart !== -1); + } else { + $qControl = $this->replication->slaveControl( + $_POST['sr_slave_action'], + $_POST['sr_slave_control_parm'], + DatabaseInterface::CONNECT_USER + ); + + $result = ($qControl !== false && $qControl !== -1); + } + + return $result; + } + + /** + * handle control requests for Slave Skip Error + * + * @return boolean + */ + public function handleRequestForSlaveSkipError() + { + $count = 1; + if (isset($_POST['sr_skip_errors_count'])) { + $count = $_POST['sr_skip_errors_count'] * 1; + } + + $qStop = $this->replication->slaveControl("STOP", null, DatabaseInterface::CONNECT_USER); + $qSkip = $GLOBALS['dbi']->tryQuery( + "SET GLOBAL SQL_SLAVE_SKIP_COUNTER = " . $count . ";" + ); + $qStart = $this->replication->slaveControl("START", null, DatabaseInterface::CONNECT_USER); + + $result = ($qStop !== false && $qStop !== -1 && + $qSkip !== false && $qSkip !== -1 && + $qStart !== false && $qStart !== -1); + + return $result; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Response.php b/srcs/phpmyadmin/libraries/classes/Response.php new file mode 100644 index 0000000..be9313b --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Response.php @@ -0,0 +1,614 @@ + + * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + */ + protected static $httpStatusMessages = [ + // Informational + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 103 => 'Early Hints', + // Success + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 208 => 'Already Reported', + 226 => 'IM Used', + // Redirection + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', + // Client Error + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Payload Too Large', + 414 => 'URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Range Not Satisfiable', + 417 => 'Expectation Failed', + 421 => 'Misdirected Request', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 425 => 'Too Early', + 426 => 'Upgrade Required', + 427 => 'Unassigned', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 430 => 'Unassigned', + 431 => 'Request Header Fields Too Large', + 451 => 'Unavailable For Legal Reasons', + // Server Error + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 508 => 'Loop Detected', + 509 => 'Unassigned', + 510 => 'Not Extended', + 511 => 'Network Authentication Required', + ]; + + /** + * Creates a new class instance + */ + private function __construct() + { + if (! defined('TESTSUITE')) { + $buffer = OutputBuffering::getInstance(); + $buffer->start(); + register_shutdown_function([$this, 'response']); + } + $this->_header = new Header(); + $this->_HTML = ''; + $this->_JSON = []; + $this->_footer = new Footer(); + + $this->_isSuccess = true; + $this->_isDisabled = false; + $this->setAjax(! empty($_REQUEST['ajax_request'])); + $this->_CWD = getcwd(); + } + + /** + * Set the ajax flag to indicate whether + * we are servicing an ajax request + * + * @param bool $isAjax Whether we are servicing an ajax request + * + * @return void + */ + public function setAjax(bool $isAjax): void + { + $this->_isAjax = $isAjax; + $this->_header->setAjax($this->_isAjax); + $this->_footer->setAjax($this->_isAjax); + } + + /** + * Returns the singleton Response object + * + * @return Response object + */ + public static function getInstance() + { + if (empty(self::$_instance)) { + self::$_instance = new Response(); + } + return self::$_instance; + } + + /** + * Set the status of an ajax response, + * whether it is a success or an error + * + * @param bool $state Whether the request was successfully processed + * + * @return void + */ + public function setRequestStatus(bool $state): void + { + $this->_isSuccess = ($state === true); + } + + /** + * Returns true or false depending on whether + * we are servicing an ajax request + * + * @return bool + */ + public function isAjax(): bool + { + return $this->_isAjax; + } + + /** + * Returns the path to the current working directory + * Necessary to work around a PHP bug where the CWD is + * reset after the initial script exits + * + * @return string + */ + public function getCWD() + { + return $this->_CWD; + } + + /** + * Disables the rendering of the header + * and the footer in responses + * + * @return void + */ + public function disable() + { + $this->_header->disable(); + $this->_footer->disable(); + $this->_isDisabled = true; + } + + /** + * Returns a PhpMyAdmin\Header object + * + * @return Header + */ + public function getHeader() + { + return $this->_header; + } + + /** + * Returns a PhpMyAdmin\Footer object + * + * @return Footer + */ + public function getFooter() + { + return $this->_footer; + } + + /** + * Add HTML code to the response + * + * @param string $content A string to be appended to + * the current output buffer + * + * @return void + */ + public function addHTML($content) + { + if (is_array($content)) { + foreach ($content as $msg) { + $this->addHTML($msg); + } + } elseif ($content instanceof Message) { + $this->_HTML .= $content->getDisplay(); + } else { + $this->_HTML .= $content; + } + } + + /** + * Add JSON code to the response + * + * @param mixed $json Either a key (string) or an + * array or key-value pairs + * @param mixed $value Null, if passing an array in $json otherwise + * it's a string value to the key + * + * @return void + */ + public function addJSON($json, $value = null) + { + if (is_array($json)) { + foreach ($json as $key => $value) { + $this->addJSON($key, $value); + } + } else { + if ($value instanceof Message) { + $this->_JSON[$json] = $value->getDisplay(); + } else { + $this->_JSON[$json] = $value; + } + } + } + + /** + * Renders the HTML response text + * + * @return string + */ + private function _getDisplay() + { + // The header may contain nothing at all, + // if its content was already rendered + // and, in this case, the header will be + // in the content part of the request + $retval = $this->_header->getDisplay(); + $retval .= $this->_HTML; + $retval .= $this->_footer->getDisplay(); + return $retval; + } + + /** + * Sends an HTML response to the browser + * + * @return void + */ + private function _htmlResponse() + { + echo $this->_getDisplay(); + } + + /** + * Sends a JSON response to the browser + * + * @return void + */ + private function _ajaxResponse() + { + /* Avoid wrapping in case we're disabled */ + if ($this->_isDisabled) { + echo $this->_getDisplay(); + return; + } + + if (! isset($this->_JSON['message'])) { + $this->_JSON['message'] = $this->_getDisplay(); + } elseif ($this->_JSON['message'] instanceof Message) { + $this->_JSON['message'] = $this->_JSON['message']->getDisplay(); + } + + if ($this->_isSuccess) { + $this->_JSON['success'] = true; + } else { + $this->_JSON['success'] = false; + $this->_JSON['error'] = $this->_JSON['message']; + unset($this->_JSON['message']); + } + + if ($this->_isSuccess) { + $this->addJSON('title', '' . $this->getHeader()->getPageTitle() . ''); + + if (isset($GLOBALS['dbi'])) { + $menuHash = $this->getHeader()->getMenu()->getHash(); + $this->addJSON('menuHash', $menuHash); + $hashes = []; + if (isset($_REQUEST['menuHashes'])) { + $hashes = explode('-', $_REQUEST['menuHashes']); + } + if (! in_array($menuHash, $hashes)) { + $this->addJSON( + 'menu', + $this->getHeader() + ->getMenu() + ->getDisplay() + ); + } + } + + $this->addJSON('scripts', $this->getHeader()->getScripts()->getFiles()); + $this->addJSON('selflink', $this->getFooter()->getSelfUrl()); + $this->addJSON('displayMessage', $this->getHeader()->getMessage()); + + $debug = $this->_footer->getDebugMessage(); + if (empty($_REQUEST['no_debug']) + && strlen($debug) > 0 + ) { + $this->addJSON('debug', $debug); + } + + $errors = $this->_footer->getErrorMessages(); + if (strlen($errors) > 0) { + $this->addJSON('errors', $errors); + } + $promptPhpErrors = $GLOBALS['error_handler']->hasErrorsForPrompt(); + $this->addJSON('promptPhpErrors', $promptPhpErrors); + + if (empty($GLOBALS['error_message'])) { + // set current db, table and sql query in the querywindow + // (this is for the bottom console) + $query = ''; + $maxChars = $GLOBALS['cfg']['MaxCharactersInDisplayedSQL']; + if (isset($GLOBALS['sql_query']) + && mb_strlen($GLOBALS['sql_query']) < $maxChars + ) { + $query = $GLOBALS['sql_query']; + } + $this->addJSON( + 'reloadQuerywindow', + [ + 'db' => Core::ifSetOr($GLOBALS['db'], ''), + 'table' => Core::ifSetOr($GLOBALS['table'], ''), + 'sql_query' => $query, + ] + ); + if (! empty($GLOBALS['focus_querywindow'])) { + $this->addJSON('_focusQuerywindow', $query); + } + if (! empty($GLOBALS['reload'])) { + $this->addJSON('reloadNavigation', 1); + } + $this->addJSON('params', $this->getHeader()->getJsParams()); + } + } + + // Set the Content-Type header to JSON so that jQuery parses the + // response correctly. + Core::headerJSON(); + + $result = json_encode($this->_JSON); + if ($result === false) { + switch (json_last_error()) { + case JSON_ERROR_NONE: + $error = 'No errors'; + break; + case JSON_ERROR_DEPTH: + $error = 'Maximum stack depth exceeded'; + break; + case JSON_ERROR_STATE_MISMATCH: + $error = 'Underflow or the modes mismatch'; + break; + case JSON_ERROR_CTRL_CHAR: + $error = 'Unexpected control character found'; + break; + case JSON_ERROR_SYNTAX: + $error = 'Syntax error, malformed JSON'; + break; + case JSON_ERROR_UTF8: + $error = 'Malformed UTF-8 characters, possibly incorrectly encoded'; + break; + case JSON_ERROR_RECURSION: + $error = 'One or more recursive references in the value to be encoded'; + break; + case JSON_ERROR_INF_OR_NAN: + $error = 'One or more NAN or INF values in the value to be encoded'; + break; + case JSON_ERROR_UNSUPPORTED_TYPE: + $error = 'A value of a type that cannot be encoded was given'; + break; + default: + $error = 'Unknown error'; + break; + } + echo json_encode([ + 'success' => false, + 'error' => 'JSON encoding failed: ' . $error, + ]); + } else { + echo $result; + } + } + + /** + * Sends an HTML response to the browser + * + * @return void + */ + public function response() + { + chdir($this->getCWD()); + $buffer = OutputBuffering::getInstance(); + if (empty($this->_HTML)) { + $this->_HTML = $buffer->getContents(); + } + if ($this->isAjax()) { + $this->_ajaxResponse(); + } else { + $this->_htmlResponse(); + } + $buffer->flush(); + exit; + } + + /** + * Wrapper around PHP's header() function. + * + * @param string $text header string + * + * @return void + */ + public function header($text) + { + header($text); + } + + /** + * Wrapper around PHP's headers_sent() function. + * + * @return bool + */ + public function headersSent() + { + return headers_sent(); + } + + /** + * Wrapper around PHP's http_response_code() function. + * + * @param int $response_code will set the response code. + * + * @return void + */ + public function httpResponseCode($response_code) + { + http_response_code($response_code); + } + + /** + * Sets http response code. + * + * @param int $responseCode will set the response code. + * + * @return void + */ + public function setHttpResponseCode(int $responseCode): void + { + $this->httpResponseCode($responseCode); + $header = 'status: ' . $responseCode . ' '; + if (isset(static::$httpStatusMessages[$responseCode])) { + $header .= static::$httpStatusMessages[$responseCode]; + } else { + $header .= 'Web server is down'; + } + if (PHP_SAPI !== 'cgi-fcgi') { + $this->header($header); + } + } + + /** + * Generate header for 303 + * + * @param string $location will set location to redirect. + * + * @return void + */ + public function generateHeader303($location) + { + $this->setHttpResponseCode(303); + $this->header('Location: ' . $location); + if (! defined('TESTSUITE')) { + exit; + } + } + + /** + * Configures response for the login page + * + * @return bool Whether caller should exit + */ + public function loginPage() + { + /* Handle AJAX redirection */ + if ($this->isAjax()) { + $this->setRequestStatus(false); + // redirect_flag redirects to the login page + $this->addJSON('redirect_flag', '1'); + return true; + } + + $this->getFooter()->setMinimal(); + $header = $this->getHeader(); + $header->setBodyId('loginform'); + $header->setTitle('phpMyAdmin'); + $header->disableMenuAndConsole(); + $header->disableWarnings(); + return false; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Rte/Events.php b/srcs/phpmyadmin/libraries/classes/Rte/Events.php new file mode 100644 index 0000000..bb0d52b --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Rte/Events.php @@ -0,0 +1,680 @@ +dbi = $dbi; + $this->export = new Export($this->dbi); + $this->footer = new Footer($this->dbi); + $this->general = new General($this->dbi); + $this->rteList = new RteList($this->dbi); + $this->words = new Words(); + } + + /** + * Sets required globals + * + * @return void + */ + public function setGlobals() + { + global $event_status, $event_type, $event_interval; + + $event_status = [ + 'query' => [ + 'ENABLE', + 'DISABLE', + 'DISABLE ON SLAVE', + ], + 'display' => [ + 'ENABLED', + 'DISABLED', + 'SLAVESIDE_DISABLED', + ], + ]; + $event_type = [ + 'RECURRING', + 'ONE TIME', + ]; + $event_interval = [ + 'YEAR', + 'QUARTER', + 'MONTH', + 'DAY', + 'HOUR', + 'MINUTE', + 'WEEK', + 'SECOND', + 'YEAR_MONTH', + 'DAY_HOUR', + 'DAY_MINUTE', + 'DAY_SECOND', + 'HOUR_MINUTE', + 'HOUR_SECOND', + 'MINUTE_SECOND', + ]; + } + + /** + * Main function for the events functionality + * + * @return void + */ + public function main() + { + global $db; + + $this->setGlobals(); + /** + * Process all requests + */ + $this->handleEditor(); + $this->export->events(); + /** + * Display a list of available events + */ + $items = $this->dbi->getEvents($db); + echo $this->rteList->get('event', $items); + /** + * Display a link for adding a new event, if + * the user has the privileges and a link to + * toggle the state of the event scheduler. + */ + echo $this->footer->events(); + } + + /** + * Handles editor requests for adding or editing an item + * + * @return void + */ + public function handleEditor() + { + global $errors, $db; + + if (! empty($_POST['editor_process_add']) + || ! empty($_POST['editor_process_edit']) + ) { + $sql_query = ''; + + $item_query = $this->getQueryFromRequest(); + + if (! count($errors)) { // set by PhpMyAdmin\Rte\Routines::getQueryFromRequest() + // Execute the created query + if (! empty($_POST['editor_process_edit'])) { + // Backup the old trigger, in case something goes wrong + $create_item = $this->dbi->getDefinition( + $db, + 'EVENT', + $_POST['item_original_name'] + ); + $drop_item = "DROP EVENT " + . Util::backquote($_POST['item_original_name']) + . ";\n"; + $result = $this->dbi->tryQuery($drop_item); + if (! $result) { + $errors[] = sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($drop_item) + ) + . '
    ' + . __('MySQL said: ') . $this->dbi->getError(); + } else { + $result = $this->dbi->tryQuery($item_query); + if (! $result) { + $errors[] = sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($item_query) + ) + . '
    ' + . __('MySQL said: ') . $this->dbi->getError(); + // We dropped the old item, but were unable to create + // the new one. Try to restore the backup query + $result = $this->dbi->tryQuery($create_item); + $errors = $this->general->checkResult( + $result, + __( + 'Sorry, we failed to restore the dropped event.' + ), + $create_item, + $errors + ); + } else { + $message = Message::success( + __('Event %1$s has been modified.') + ); + $message->addParam( + Util::backquote($_POST['item_name']) + ); + $sql_query = $drop_item . $item_query; + } + } + } else { + // 'Add a new item' mode + $result = $this->dbi->tryQuery($item_query); + if (! $result) { + $errors[] = sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($item_query) + ) + . '

    ' + . __('MySQL said: ') . $this->dbi->getError(); + } else { + $message = Message::success( + __('Event %1$s has been created.') + ); + $message->addParam( + Util::backquote($_POST['item_name']) + ); + $sql_query = $item_query; + } + } + } + + if (count($errors)) { + $message = Message::error( + '' + . __( + 'One or more errors have occurred while processing your request:' + ) + . '' + ); + $message->addHtml('
      '); + foreach ($errors as $string) { + $message->addHtml('
    • ' . $string . '
    • '); + } + $message->addHtml('
    '); + } + + $output = Util::getMessage($message, $sql_query); + $response = Response::getInstance(); + if ($response->isAjax()) { + if ($message->isSuccess()) { + $events = $this->dbi->getEvents($db, $_POST['item_name']); + $event = $events[0]; + $response->addJSON( + 'name', + htmlspecialchars( + mb_strtoupper($_POST['item_name']) + ) + ); + if (! empty($event)) { + $response->addJSON('new_row', $this->rteList->getEventRow($event)); + } + $response->addJSON('insert', ! empty($event)); + $response->addJSON('message', $output); + } else { + $response->setRequestStatus(false); + $response->addJSON('message', $message); + } + exit; + } + } + /** + * Display a form used to add/edit a trigger, if necessary + */ + if (count($errors) + || (empty($_POST['editor_process_add']) + && empty($_POST['editor_process_edit']) + && (! empty($_REQUEST['add_item']) + || ! empty($_REQUEST['edit_item']) + || ! empty($_POST['item_changetype']))) + ) { // FIXME: this must be simpler than that + $operation = ''; + if (! empty($_POST['item_changetype'])) { + $operation = 'change'; + } + // Get the data for the form (if any) + if (! empty($_REQUEST['add_item'])) { + $title = $this->words->get('add'); + $item = $this->getDataFromRequest(); + $mode = 'add'; + } elseif (! empty($_REQUEST['edit_item'])) { + $title = __("Edit event"); + if (! empty($_REQUEST['item_name']) + && empty($_POST['editor_process_edit']) + && empty($_POST['item_changetype']) + ) { + $item = $this->getDataFromName($_REQUEST['item_name']); + if ($item !== false) { + $item['item_original_name'] = $item['item_name']; + } + } else { + $item = $this->getDataFromRequest(); + } + $mode = 'edit'; + } + $this->general->sendEditor('EVN', $mode, $item, $title, $db, $operation); + } + } + + /** + * This function will generate the values that are required to for the editor + * + * @return array Data necessary to create the editor. + */ + public function getDataFromRequest() + { + $retval = []; + $indices = [ + 'item_name', + 'item_original_name', + 'item_status', + 'item_execute_at', + 'item_interval_value', + 'item_interval_field', + 'item_starts', + 'item_ends', + 'item_definition', + 'item_preserve', + 'item_comment', + 'item_definer', + ]; + foreach ($indices as $index) { + $retval[$index] = isset($_POST[$index]) ? $_POST[$index] : ''; + } + $retval['item_type'] = 'ONE TIME'; + $retval['item_type_toggle'] = 'RECURRING'; + if (isset($_POST['item_type']) && $_POST['item_type'] == 'RECURRING') { + $retval['item_type'] = 'RECURRING'; + $retval['item_type_toggle'] = 'ONE TIME'; + } + return $retval; + } + + /** + * This function will generate the values that are required to complete + * the "Edit event" form given the name of a event. + * + * @param string $name The name of the event. + * + * @return array|bool Data necessary to create the editor. + */ + public function getDataFromName($name) + { + global $db; + + $retval = []; + $columns = "`EVENT_NAME`, `STATUS`, `EVENT_TYPE`, `EXECUTE_AT`, " + . "`INTERVAL_VALUE`, `INTERVAL_FIELD`, `STARTS`, `ENDS`, " + . "`EVENT_DEFINITION`, `ON_COMPLETION`, `DEFINER`, `EVENT_COMMENT`"; + $where = "EVENT_SCHEMA " . Util::getCollateForIS() . "=" + . "'" . $this->dbi->escapeString($db) . "' " + . "AND EVENT_NAME='" . $this->dbi->escapeString($name) . "'"; + $query = "SELECT $columns FROM `INFORMATION_SCHEMA`.`EVENTS` WHERE $where;"; + $item = $this->dbi->fetchSingleRow($query); + if (! $item) { + return false; + } + $retval['item_name'] = $item['EVENT_NAME']; + $retval['item_status'] = $item['STATUS']; + $retval['item_type'] = $item['EVENT_TYPE']; + if ($retval['item_type'] == 'RECURRING') { + $retval['item_type_toggle'] = 'ONE TIME'; + } else { + $retval['item_type_toggle'] = 'RECURRING'; + } + $retval['item_execute_at'] = $item['EXECUTE_AT']; + $retval['item_interval_value'] = $item['INTERVAL_VALUE']; + $retval['item_interval_field'] = $item['INTERVAL_FIELD']; + $retval['item_starts'] = $item['STARTS']; + $retval['item_ends'] = $item['ENDS']; + $retval['item_preserve'] = ''; + if ($item['ON_COMPLETION'] == 'PRESERVE') { + $retval['item_preserve'] = " checked='checked'"; + } + $retval['item_definition'] = $item['EVENT_DEFINITION']; + $retval['item_definer'] = $item['DEFINER']; + $retval['item_comment'] = $item['EVENT_COMMENT']; + + return $retval; + } + + /** + * Displays a form used to add/edit an event + * + * @param string $mode If the editor will be used to edit an event + * or add a new one: 'edit' or 'add'. + * @param string $operation If the editor was previously invoked with + * JS turned off, this will hold the name of + * the current operation + * @param array $item Data for the event returned by + * getDataFromRequest() or getDataFromName() + * + * @return string HTML code for the editor. + */ + public function getEditorForm($mode, $operation, array $item) + { + global $db, $table, $event_status, $event_type, $event_interval; + + $modeToUpper = mb_strtoupper($mode); + + $response = Response::getInstance(); + + // Escape special characters + $need_escape = [ + 'item_original_name', + 'item_name', + 'item_type', + 'item_execute_at', + 'item_interval_value', + 'item_starts', + 'item_ends', + 'item_definition', + 'item_definer', + 'item_comment', + ]; + foreach ($need_escape as $index) { + $item[$index] = htmlentities((string) $item[$index], ENT_QUOTES); + } + $original_data = ''; + if ($mode == 'edit') { + $original_data = "\n"; + } + // Handle some logic first + if ($operation == 'change') { + if ($item['item_type'] == 'RECURRING') { + $item['item_type'] = 'ONE TIME'; + $item['item_type_toggle'] = 'RECURRING'; + } else { + $item['item_type'] = 'RECURRING'; + $item['item_type_toggle'] = 'ONE TIME'; + } + } + if ($item['item_type'] == 'ONE TIME') { + $isrecurring_class = ' hide'; + $isonetime_class = ''; + } else { + $isrecurring_class = ''; + $isonetime_class = ' hide'; + } + // Create the output + $retval = ""; + $retval .= "\n\n"; + $retval .= "
    \n"; + $retval .= "\n"; + $retval .= $original_data; + $retval .= Url::getHiddenInputs($db, $table) . "\n"; + $retval .= "
    \n"; + $retval .= "" . __('Details') . "\n"; + $retval .= "\n"; + $retval .= "\n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= "\n"; + + $retval .= "\n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= "\n"; + $retval .= "\n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= "\n"; + $retval .= "\n"; + $retval .= " \n"; + $retval .= " "; + } + return $output; + } + + /** + * Creates the HTML code that shows the routine execution dialog. + * + * @param array $routine Data for the routine returned by + * getDataFromName() + * + * @return string HTML code for the routine execution dialog. + */ + public function getExecuteForm(array $routine) + { + global $db, $cfg; + + $response = Response::getInstance(); + + // Escape special characters + $routine['item_name'] = htmlentities($routine['item_name'], ENT_QUOTES); + for ($i = 0; $i < $routine['item_num_params']; $i++) { + $routine['item_param_name'][$i] = htmlentities( + $routine['item_param_name'][$i], + ENT_QUOTES + ); + } + + // Create the output + $retval = ""; + $retval .= "\n\n"; + $retval .= "isAjax()) { + $retval .= "{$routine['item_name']}\n"; + $retval .= "
    " . __('Event name') . "\n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= "
    " . __('Event type') . "\n"; + if ($response->isAjax()) { + $retval .= " \n"; + } else { + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " $value) { + $selected = ""; + if (! empty($item['item_interval_field']) + && $item['item_interval_field'] == $value + ) { + $selected = " selected='selected'"; + } + $retval .= "$value"; + } + $retval .= " \n"; + $retval .= "
    " . _pgettext('Start of recurring event', 'Start'); + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= "
    " . __('On completion preserve') . "\n"; + $retval .= " \n"; + $retval .= " isAjax()) { + $retval .= "\n"; + $retval .= "\n"; + } + $retval .= "\n\n"; + $retval .= "\n\n"; + + return $retval; + } + + /** + * Composes the query necessary to create an event from an HTTP request. + * + * @return string The CREATE EVENT query. + */ + public function getQueryFromRequest() + { + global $errors, $event_status, $event_type, $event_interval; + + $query = 'CREATE '; + if (! empty($_POST['item_definer'])) { + if (mb_strpos($_POST['item_definer'], '@') !== false + ) { + $arr = explode('@', $_POST['item_definer']); + $query .= 'DEFINER=' . Util::backquote($arr[0]); + $query .= '@' . Util::backquote($arr[1]) . ' '; + } else { + $errors[] = __('The definer must be in the "username@hostname" format!'); + } + } + $query .= 'EVENT '; + if (! empty($_POST['item_name'])) { + $query .= Util::backquote($_POST['item_name']) . ' '; + } else { + $errors[] = __('You must provide an event name!'); + } + $query .= 'ON SCHEDULE '; + if (! empty($_POST['item_type']) + && in_array($_POST['item_type'], $event_type) + ) { + if ($_POST['item_type'] == 'RECURRING') { + if (! empty($_POST['item_interval_value']) + && ! empty($_POST['item_interval_field']) + && in_array($_POST['item_interval_field'], $event_interval) + ) { + $query .= 'EVERY ' . intval($_POST['item_interval_value']) . ' '; + $query .= $_POST['item_interval_field'] . ' '; + } else { + $errors[] + = __('You must provide a valid interval value for the event.'); + } + if (! empty($_POST['item_starts'])) { + $query .= "STARTS '" + . $this->dbi->escapeString($_POST['item_starts']) + . "' "; + } + if (! empty($_POST['item_ends'])) { + $query .= "ENDS '" + . $this->dbi->escapeString($_POST['item_ends']) + . "' "; + } + } else { + if (! empty($_POST['item_execute_at'])) { + $query .= "AT '" + . $this->dbi->escapeString($_POST['item_execute_at']) + . "' "; + } else { + $errors[] + = __('You must provide a valid execution time for the event.'); + } + } + } else { + $errors[] = __('You must provide a valid type for the event.'); + } + $query .= 'ON COMPLETION '; + if (empty($_POST['item_preserve'])) { + $query .= 'NOT '; + } + $query .= 'PRESERVE '; + if (! empty($_POST['item_status'])) { + foreach ($event_status['display'] as $key => $value) { + if ($value == $_POST['item_status']) { + $query .= $event_status['query'][$key] . ' '; + break; + } + } + } + if (! empty($_POST['item_comment'])) { + $query .= "COMMENT '" . $this->dbi->escapeString( + $_POST['item_comment'] + ) . "' "; + } + $query .= 'DO '; + if (! empty($_POST['item_definition'])) { + $query .= $_POST['item_definition']; + } else { + $errors[] = __('You must provide an event definition.'); + } + + return $query; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Rte/Export.php b/srcs/phpmyadmin/libraries/classes/Rte/Export.php new file mode 100644 index 0000000..2ae19e6 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Rte/Export.php @@ -0,0 +1,168 @@ +dbi = $dbi; + $this->words = new Words(); + } + + /** + * This function is called from one of the other functions in this file + * and it completes the handling of the export functionality. + * + * @param string $export_data The SQL query to create the requested item + * + * @return void + */ + private function handle($export_data) + { + global $db; + + $response = Response::getInstance(); + + $item_name = htmlspecialchars(Util::backquote($_GET['item_name'])); + if ($export_data !== false) { + $export_data = htmlspecialchars(trim($export_data)); + $title = sprintf($this->words->get('export'), $item_name); + if ($response->isAjax()) { + $response->addJSON('message', $export_data); + $response->addJSON('title', $title); + exit; + } else { + $export_data = ''; + echo "
    \n" + , "$title\n" + , $export_data + , "
    \n"; + } + } else { + $_db = htmlspecialchars(Util::backquote($db)); + $message = __('Error in processing request:') . ' ' + . sprintf($this->words->get('no_view'), $item_name, $_db); + $message = Message::error($message); + + if ($response->isAjax()) { + $response->setRequestStatus(false); + $response->addJSON('message', $message); + exit; + } else { + $message->display(); + } + } + } + + /** + * If necessary, prepares event information and passes + * it to handle() for the actual export. + * + * @return void + */ + public function events() + { + global $db; + + if (! empty($_GET['export_item']) && ! empty($_GET['item_name'])) { + $item_name = $_GET['item_name']; + $export_data = $this->dbi->getDefinition($db, 'EVENT', $item_name); + if (! $export_data) { + $export_data = false; + } + $this->handle($export_data); + } + } + + /** + * If necessary, prepares routine information and passes + * it to handle() for the actual export. + * + * @return void + */ + public function routines() + { + global $db; + + if (! empty($_GET['export_item']) + && ! empty($_GET['item_name']) + && ! empty($_GET['item_type']) + ) { + if ($_GET['item_type'] == 'FUNCTION' || $_GET['item_type'] == 'PROCEDURE') { + $rtn_definition + = $this->dbi->getDefinition( + $db, + $_GET['item_type'], + $_GET['item_name'] + ); + if ($rtn_definition === null) { + $export_data = false; + } else { + $export_data = "DELIMITER $$\n" + . $rtn_definition + . "$$\nDELIMITER ;\n"; + } + + $this->handle($export_data); + } + } + } + + /** + * If necessary, prepares trigger information and passes + * it to handle() for the actual export. + * + * @return void + */ + public function triggers() + { + global $db, $table; + + if (! empty($_GET['export_item']) && ! empty($_GET['item_name'])) { + $item_name = $_GET['item_name']; + $triggers = $this->dbi->getTriggers($db, $table, ''); + $export_data = false; + foreach ($triggers as $trigger) { + if ($trigger['name'] === $item_name) { + $export_data = $trigger['create']; + break; + } + } + $this->handle($export_data); + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Rte/Footer.php b/srcs/phpmyadmin/libraries/classes/Rte/Footer.php new file mode 100644 index 0000000..5181b00 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Rte/Footer.php @@ -0,0 +1,160 @@ +dbi = $dbi; + $this->words = new Words(); + } + + /** + * Creates a fieldset for adding a new item, if the user has the privileges. + * + * @param string $docu String used to create a link to the MySQL docs + * @param string $priv Privilege to check for adding a new item + * @param string $name MySQL name of the item + * + * @return string An HTML snippet with the link to add a new item + */ + private function getLinks($docu, $priv, $name) + { + global $db, $table, $url_query; + + $icon = mb_strtolower($name) . '_add'; + $retval = ""; + $retval .= "\n"; + $retval .= "
    \n"; + $retval .= "" . _pgettext('Create new procedure', 'New') . "\n"; + $retval .= "
    \n"; + if (Util::currentUserHasPrivilege($priv, $db, $table)) { + $retval .= ' words->get('add') . "\n"; + } else { + $icon = 'bd_' . $icon; + $retval .= Util::getIcon($icon); + $retval .= $this->words->get('add') . "\n"; + } + $retval .= " " . Util::showMySQLDocu($docu) . "\n"; + $retval .= "
    \n"; + $retval .= "
    \n"; + $retval .= "\n\n"; + + return $retval; + } + + /** + * Creates a fieldset for adding a new routine, if the user has the privileges. + * + * @return string HTML code with containing the footer fieldset + */ + public function routines() + { + return $this->getLinks('CREATE_PROCEDURE', 'CREATE ROUTINE', 'ROUTINE'); + } + + /** + * Creates a fieldset for adding a new trigger, if the user has the privileges. + * + * @return string HTML code with containing the footer fieldset + */ + public function triggers() + { + return $this->getLinks('CREATE_TRIGGER', 'TRIGGER', 'TRIGGER'); + } + + /** + * Creates a fieldset for adding a new event, if the user has the privileges. + * + * @return string HTML code with containing the footer fieldset + */ + public function events() + { + global $db, $url_query; + + /** + * For events, we show the usual 'Add event' form and also + * a form for toggling the state of the event scheduler + */ + // Init options for the event scheduler toggle functionality + $es_state = $this->dbi->fetchValue( + "SHOW GLOBAL VARIABLES LIKE 'event_scheduler'", + 0, + 1 + ); + $es_state = mb_strtolower($es_state); + $options = [ + 0 => [ + 'label' => __('OFF'), + 'value' => "SET GLOBAL event_scheduler=\"OFF\"", + 'selected' => $es_state != 'on', + ], + 1 => [ + 'label' => __('ON'), + 'value' => "SET GLOBAL event_scheduler=\"ON\"", + 'selected' => $es_state == 'on', + ], + ]; + // Generate output + $retval = "\n"; + $retval .= "
    \n"; + // show the usual footer + $retval .= $this->getLinks('CREATE_EVENT', 'EVENT', 'EVENT'); + $retval .= "
    \n"; + $retval .= " \n"; + $retval .= " " . __('Event scheduler status') . "\n"; + $retval .= " \n"; + $retval .= "
    \n"; + // show the toggle button + $retval .= Util::toggleButton( + "sql.php$url_query&goto=db_events.php" . urlencode("?db=$db"), + 'sql_query', + $options, + 'Functions.slidingMessage(data.sql_query);' + ); + $retval .= "
    \n"; + $retval .= "
    \n"; + $retval .= "
    \n"; + $retval .= "
    "; + $retval .= "\n"; + + return $retval; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Rte/General.php b/srcs/phpmyadmin/libraries/classes/Rte/General.php new file mode 100644 index 0000000..37962b5 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Rte/General.php @@ -0,0 +1,118 @@ +dbi = $dbi; + } + + /** + * Check result + * + * @param resource|bool $result Query result + * @param string $error Error to add + * @param string $createStatement Query + * @param array $errors Errors + * + * @return array + */ + public function checkResult($result, $error, $createStatement, array $errors) + { + if ($result) { + return $errors; + } + + // OMG, this is really bad! We dropped the query, + // failed to create a new one + // and now even the backup query does not execute! + // This should not happen, but we better handle + // this just in case. + $errors[] = $error . '
    ' + . __('The backed up query was:') + . "\"" . htmlspecialchars($createStatement) . "\"" . '
    ' + . __('MySQL said: ') . $this->dbi->getError(); + + return $errors; + } + + /** + * Send TRI or EVN editor via ajax or by echoing. + * + * @param string $type TRI or EVN + * @param string $mode Editor mode 'add' or 'edit' + * @param array $item Data necessary to create the editor + * @param string $title Title of the editor + * @param string $db Database + * @param string $operation Operation 'change' or '' + * + * @return void + */ + public function sendEditor($type, $mode, array $item, $title, $db, $operation = null) + { + $events = new Events($this->dbi); + $triggers = new Triggers($this->dbi); + $words = new Words(); + $response = Response::getInstance(); + if ($item !== false) { + // Show form + if ($type == 'TRI') { + $editor = $triggers->getEditorForm($mode, $item); + } else { // EVN + $editor = $events->getEditorForm($mode, $operation, $item); + } + if ($response->isAjax()) { + $response->addJSON('message', $editor); + $response->addJSON('title', $title); + } else { + echo "\n\n

    $title

    \n\n$editor"; + unset($_POST); + } + exit; + } else { + $message = __('Error in processing request:') . ' '; + $message .= sprintf( + $words->get('not_found'), + htmlspecialchars(Util::backquote($_REQUEST['item_name'])), + htmlspecialchars(Util::backquote($db)) + ); + $message = Message::error($message); + if ($response->isAjax()) { + $response->setRequestStatus(false); + $response->addJSON('message', $message); + exit; + } else { + $message->display(); + } + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Rte/Routines.php b/srcs/phpmyadmin/libraries/classes/Rte/Routines.php new file mode 100644 index 0000000..24b0dd5 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Rte/Routines.php @@ -0,0 +1,1743 @@ +dbi = $dbi; + $this->export = new Export($this->dbi); + $this->footer = new Footer($this->dbi); + $this->general = new General($this->dbi); + $this->rteList = new RteList($this->dbi); + $this->words = new Words(); + } + + /** + * Sets required globals + * + * @return void + */ + public function setGlobals() + { + global $param_directions, $param_opts_num, $param_sqldataaccess; + + $param_directions = [ + 'IN', + 'OUT', + 'INOUT', + ]; + $param_opts_num = [ + 'UNSIGNED', + 'ZEROFILL', + 'UNSIGNED ZEROFILL', + ]; + $param_sqldataaccess = [ + 'NO SQL', + 'CONTAINS SQL', + 'READS SQL DATA', + 'MODIFIES SQL DATA', + ]; + } + + /** + * Main function for the routines functionality + * + * @param string $type 'FUNCTION' for functions, + * 'PROCEDURE' for procedures, + * null for both + * + * @return void + */ + public function main($type) + { + global $db; + + $this->setGlobals(); + /** + * Process all requests + */ + $this->handleEditor(); + $this->handleExecute(); + $this->export->routines(); + /** + * Display a list of available routines + */ + if (! Core::isValid($type, ['FUNCTION', 'PROCEDURE'])) { + $type = null; + } + $items = $this->dbi->getRoutines($db, $type); + echo $this->rteList->get('routine', $items); + /** + * Display the form for adding a new routine, if the user has the privileges. + */ + echo $this->footer->routines(); + /** + * Display a warning for users with PHP's old "mysql" extension. + */ + if (! DatabaseInterface::checkDbExtension('mysqli')) { + trigger_error( + __( + 'You are using PHP\'s deprecated \'mysql\' extension, ' + . 'which is not capable of handling multi queries. ' + . '[strong]The execution of some stored routines may fail![/strong] ' + . 'Please use the improved \'mysqli\' extension to ' + . 'avoid any problems.' + ), + E_USER_WARNING + ); + } + } + + /** + * Handles editor requests for adding or editing an item + * + * @return void + */ + public function handleEditor() + { + global $db, $errors; + + $errors = $this->handleRequestCreateOrEdit($errors, $db); + $response = Response::getInstance(); + + /** + * Display a form used to add/edit a routine, if necessary + */ + // FIXME: this must be simpler than that + if (count($errors) + || ( empty($_POST['editor_process_add']) + && empty($_POST['editor_process_edit']) + && (! empty($_REQUEST['add_item']) || ! empty($_REQUEST['edit_item']) + || ! empty($_POST['routine_addparameter']) + || ! empty($_POST['routine_removeparameter']) + || ! empty($_POST['routine_changetype']))) + ) { + // Handle requests to add/remove parameters and changing routine type + // This is necessary when JS is disabled + $operation = ''; + if (! empty($_POST['routine_addparameter'])) { + $operation = 'add'; + } elseif (! empty($_POST['routine_removeparameter'])) { + $operation = 'remove'; + } elseif (! empty($_POST['routine_changetype'])) { + $operation = 'change'; + } + // Get the data for the form (if any) + if (! empty($_REQUEST['add_item'])) { + $title = $this->words->get('add'); + $routine = $this->getDataFromRequest(); + $mode = 'add'; + } elseif (! empty($_REQUEST['edit_item'])) { + $title = __("Edit routine"); + if (! $operation && ! empty($_GET['item_name']) + && empty($_POST['editor_process_edit']) + ) { + $routine = $this->getDataFromName( + $_GET['item_name'], + $_GET['item_type'] + ); + if ($routine !== false) { + $routine['item_original_name'] = $routine['item_name']; + $routine['item_original_type'] = $routine['item_type']; + } + } else { + $routine = $this->getDataFromRequest(); + } + $mode = 'edit'; + } + if ($routine !== false) { + // Show form + $editor = $this->getEditorForm($mode, $operation, $routine); + if ($response->isAjax()) { + $response->addJSON('message', $editor); + $response->addJSON('title', $title); + $response->addJSON('paramTemplate', $this->getParameterRow()); + $response->addJSON('type', $routine['item_type']); + } else { + echo "\n\n

    $title

    \n\n$editor"; + } + exit; + } else { + $message = __('Error in processing request:') . ' '; + $message .= sprintf( + $this->words->get('no_edit'), + htmlspecialchars( + Util::backquote($_REQUEST['item_name']) + ), + htmlspecialchars(Util::backquote($db)) + ); + + $message = Message::error($message); + if ($response->isAjax()) { + $response->setRequestStatus(false); + $response->addJSON('message', $message); + exit; + } else { + $message->display(); + } + } + } + } + + /** + * Handle request to create or edit a routine + * + * @param array $errors Errors + * @param string $db DB name + * + * @return array + */ + public function handleRequestCreateOrEdit(array $errors, $db) + { + if (empty($_POST['editor_process_add']) + && empty($_POST['editor_process_edit']) + ) { + return $errors; + } + + $sql_query = ''; + $routine_query = $this->getQueryFromRequest(); + if (! count($errors)) { + // Execute the created query + if (! empty($_POST['editor_process_edit'])) { + $isProcOrFunc = in_array( + $_POST['item_original_type'], + [ + 'PROCEDURE', + 'FUNCTION', + ] + ); + + if (! $isProcOrFunc) { + $errors[] = sprintf( + __('Invalid routine type: "%s"'), + htmlspecialchars($_POST['item_original_type']) + ); + } else { + // Backup the old routine, in case something goes wrong + $create_routine = $this->dbi->getDefinition( + $db, + $_POST['item_original_type'], + $_POST['item_original_name'] + ); + + $privilegesBackup = $this->backupPrivileges(); + + $drop_routine = "DROP {$_POST['item_original_type']} " + . Util::backquote($_POST['item_original_name']) + . ";\n"; + $result = $this->dbi->tryQuery($drop_routine); + if (! $result) { + $errors[] = sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($drop_routine) + ) + . '
    ' + . __('MySQL said: ') . $this->dbi->getError(); + } else { + list($newErrors, $message) = $this->create( + $routine_query, + $create_routine, + $privilegesBackup + ); + if (empty($newErrors)) { + $sql_query = $drop_routine . $routine_query; + } else { + $errors = array_merge($errors, $newErrors); + } + unset($newErrors); + if (null === $message) { + unset($message); + } + } + } + } else { + // 'Add a new routine' mode + $result = $this->dbi->tryQuery($routine_query); + if (! $result) { + $errors[] = sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($routine_query) + ) + . '

    ' + . __('MySQL said: ') . $this->dbi->getError(); + } else { + $message = Message::success( + __('Routine %1$s has been created.') + ); + $message->addParam( + Util::backquote($_POST['item_name']) + ); + $sql_query = $routine_query; + } + } + } + + if (count($errors)) { + $message = Message::error( + __( + 'One or more errors have occurred while' + . ' processing your request:' + ) + ); + $message->addHtml('
      '); + foreach ($errors as $string) { + $message->addHtml('
    • ' . $string . '
    • '); + } + $message->addHtml('
    '); + } + + $output = Util::getMessage($message, $sql_query); + $response = Response::getInstance(); + if (! $response->isAjax()) { + return $errors; + } + + if (! $message->isSuccess()) { + $response->setRequestStatus(false); + $response->addJSON('message', $output); + exit; + } + + $routines = $this->dbi->getRoutines( + $db, + $_POST['item_type'], + $_POST['item_name'] + ); + $routine = $routines[0]; + $response->addJSON( + 'name', + htmlspecialchars( + mb_strtoupper($_POST['item_name']) + ) + ); + $response->addJSON('new_row', $this->rteList->getRoutineRow($routine)); + $response->addJSON('insert', ! empty($routine)); + $response->addJSON('message', $output); + exit; + } + + /** + * Backup the privileges + * + * @return array + */ + public function backupPrivileges() + { + if (! $GLOBALS['proc_priv'] || ! $GLOBALS['is_reload_priv']) { + return []; + } + + // Backup the Old Privileges before dropping + // if $_POST['item_adjust_privileges'] set + if (! isset($_POST['item_adjust_privileges']) + || empty($_POST['item_adjust_privileges']) + ) { + return []; + } + + $privilegesBackupQuery = 'SELECT * FROM ' . Util::backquote( + 'mysql' + ) + . '.' . Util::backquote('procs_priv') + . ' where Routine_name = "' . $_POST['item_original_name'] + . '" AND Routine_type = "' . $_POST['item_original_type'] + . '";'; + + $privilegesBackup = $this->dbi->fetchResult( + $privilegesBackupQuery, + 0 + ); + + return $privilegesBackup; + } + + /** + * Create the routine + * + * @param string $routine_query Query to create routine + * @param string $create_routine Query to restore routine + * @param array $privilegesBackup Privileges backup + * + * @return array + */ + public function create( + $routine_query, + $create_routine, + array $privilegesBackup + ) { + $result = $this->dbi->tryQuery($routine_query); + if (! $result) { + $errors = []; + $errors[] = sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($routine_query) + ) + . '
    ' + . __('MySQL said: ') . $this->dbi->getError(); + // We dropped the old routine, + // but were unable to create the new one + // Try to restore the backup query + $result = $this->dbi->tryQuery($create_routine); + $errors = $this->general->checkResult( + $result, + __( + 'Sorry, we failed to restore' + . ' the dropped routine.' + ), + $create_routine, + $errors + ); + + return [ + $errors, + null, + ]; + } + + // Default value + $resultAdjust = false; + + if ($GLOBALS['proc_priv'] + && $GLOBALS['is_reload_priv'] + ) { + // Insert all the previous privileges + // but with the new name and the new type + foreach ($privilegesBackup as $priv) { + $adjustProcPrivilege = 'INSERT INTO ' + . Util::backquote('mysql') . '.' + . Util::backquote('procs_priv') + . ' VALUES("' . $priv[0] . '", "' + . $priv[1] . '", "' . $priv[2] . '", "' + . $_POST['item_name'] . '", "' + . $_POST['item_type'] . '", "' + . $priv[5] . '", "' + . $priv[6] . '", "' + . $priv[7] . '");'; + $resultAdjust = $this->dbi->query( + $adjustProcPrivilege + ); + } + } + + $message = $this->flushPrivileges($resultAdjust); + + return [ + [], + $message, + ]; + } + + /** + * Flush privileges and get message + * + * @param bool $flushPrivileges Flush privileges + * + * @return Message + */ + public function flushPrivileges($flushPrivileges) + { + if ($flushPrivileges) { + // Flush the Privileges + $flushPrivQuery = 'FLUSH PRIVILEGES;'; + $this->dbi->query($flushPrivQuery); + + $message = Message::success( + __( + 'Routine %1$s has been modified. Privileges have been adjusted.' + ) + ); + } else { + $message = Message::success( + __('Routine %1$s has been modified.') + ); + } + $message->addParam( + Util::backquote($_POST['item_name']) + ); + + return $message; + } + + /** + * This function will generate the values that are required to + * complete the editor form. It is especially necessary to handle + * the 'Add another parameter', 'Remove last parameter' and + * 'Change routine type' functionalities when JS is disabled. + * + * @return array Data necessary to create the routine editor. + */ + public function getDataFromRequest() + { + global $param_directions, $param_sqldataaccess; + + $retval = []; + $indices = [ + 'item_name', + 'item_original_name', + 'item_returnlength', + 'item_returnopts_num', + 'item_returnopts_text', + 'item_definition', + 'item_comment', + 'item_definer', + ]; + foreach ($indices as $index) { + $retval[$index] = isset($_POST[$index]) ? $_POST[$index] : ''; + } + + $retval['item_type'] = 'PROCEDURE'; + $retval['item_type_toggle'] = 'FUNCTION'; + if (isset($_REQUEST['item_type']) && $_REQUEST['item_type'] == 'FUNCTION') { + $retval['item_type'] = 'FUNCTION'; + $retval['item_type_toggle'] = 'PROCEDURE'; + } + $retval['item_original_type'] = 'PROCEDURE'; + if (isset($_POST['item_original_type']) + && $_POST['item_original_type'] == 'FUNCTION' + ) { + $retval['item_original_type'] = 'FUNCTION'; + } + $retval['item_num_params'] = 0; + $retval['item_param_dir'] = []; + $retval['item_param_name'] = []; + $retval['item_param_type'] = []; + $retval['item_param_length'] = []; + $retval['item_param_opts_num'] = []; + $retval['item_param_opts_text'] = []; + if (isset($_POST['item_param_name']) + && isset($_POST['item_param_type']) + && isset($_POST['item_param_length']) + && isset($_POST['item_param_opts_num']) + && isset($_POST['item_param_opts_text']) + && is_array($_POST['item_param_name']) + && is_array($_POST['item_param_type']) + && is_array($_POST['item_param_length']) + && is_array($_POST['item_param_opts_num']) + && is_array($_POST['item_param_opts_text']) + ) { + if ($_POST['item_type'] == 'PROCEDURE') { + $retval['item_param_dir'] = $_POST['item_param_dir']; + foreach ($retval['item_param_dir'] as $key => $value) { + if (! in_array($value, $param_directions, true)) { + $retval['item_param_dir'][$key] = ''; + } + } + } + $retval['item_param_name'] = $_POST['item_param_name']; + $retval['item_param_type'] = $_POST['item_param_type']; + foreach ($retval['item_param_type'] as $key => $value) { + if (! in_array($value, Util::getSupportedDatatypes(), true)) { + $retval['item_param_type'][$key] = ''; + } + } + $retval['item_param_length'] = $_POST['item_param_length']; + $retval['item_param_opts_num'] = $_POST['item_param_opts_num']; + $retval['item_param_opts_text'] = $_POST['item_param_opts_text']; + $retval['item_num_params'] = max( + count($retval['item_param_name']), + count($retval['item_param_type']), + count($retval['item_param_length']), + count($retval['item_param_opts_num']), + count($retval['item_param_opts_text']) + ); + } + $retval['item_returntype'] = ''; + if (isset($_POST['item_returntype']) + && in_array($_POST['item_returntype'], Util::getSupportedDatatypes()) + ) { + $retval['item_returntype'] = $_POST['item_returntype']; + } + + $retval['item_isdeterministic'] = ''; + if (isset($_POST['item_isdeterministic']) + && mb_strtolower($_POST['item_isdeterministic']) == 'on' + ) { + $retval['item_isdeterministic'] = " checked='checked'"; + } + $retval['item_securitytype_definer'] = ''; + $retval['item_securitytype_invoker'] = ''; + if (isset($_POST['item_securitytype'])) { + if ($_POST['item_securitytype'] === 'DEFINER') { + $retval['item_securitytype_definer'] = " selected='selected'"; + } elseif ($_POST['item_securitytype'] === 'INVOKER') { + $retval['item_securitytype_invoker'] = " selected='selected'"; + } + } + $retval['item_sqldataaccess'] = ''; + if (isset($_POST['item_sqldataaccess']) + && in_array($_POST['item_sqldataaccess'], $param_sqldataaccess, true) + ) { + $retval['item_sqldataaccess'] = $_POST['item_sqldataaccess']; + } + + return $retval; + } + + /** + * This function will generate the values that are required to complete + * the "Edit routine" form given the name of a routine. + * + * @param string $name The name of the routine. + * @param string $type Type of routine (ROUTINE|PROCEDURE) + * @param bool $all Whether to return all data or just the info about parameters. + * + * @return array|bool Data necessary to create the routine editor. + */ + public function getDataFromName($name, $type, $all = true) + { + global $db; + + $retval = []; + + // Build and execute the query + $fields = "SPECIFIC_NAME, ROUTINE_TYPE, DTD_IDENTIFIER, " + . "ROUTINE_DEFINITION, IS_DETERMINISTIC, SQL_DATA_ACCESS, " + . "ROUTINE_COMMENT, SECURITY_TYPE"; + $where = "ROUTINE_SCHEMA " . Util::getCollateForIS() . "=" + . "'" . $this->dbi->escapeString($db) . "' " + . "AND SPECIFIC_NAME='" . $this->dbi->escapeString($name) . "'" + . "AND ROUTINE_TYPE='" . $this->dbi->escapeString($type) . "'"; + $query = "SELECT $fields FROM INFORMATION_SCHEMA.ROUTINES WHERE $where;"; + + $routine = $this->dbi->fetchSingleRow($query, 'ASSOC'); + + if (! $routine) { + return false; + } + + // Get required data + $retval['item_name'] = $routine['SPECIFIC_NAME']; + $retval['item_type'] = $routine['ROUTINE_TYPE']; + + $definition + = $this->dbi->getDefinition( + $db, + $routine['ROUTINE_TYPE'], + $routine['SPECIFIC_NAME'] + ); + + if ($definition === null) { + return false; + } + + $parser = new Parser($definition); + + /** + * @var CreateStatement $stmt + */ + $stmt = $parser->statements[0]; + + $params = Routine::getParameters($stmt); + $retval['item_num_params'] = $params['num']; + $retval['item_param_dir'] = $params['dir']; + $retval['item_param_name'] = $params['name']; + $retval['item_param_type'] = $params['type']; + $retval['item_param_length'] = $params['length']; + $retval['item_param_length_arr'] = $params['length_arr']; + $retval['item_param_opts_num'] = $params['opts']; + $retval['item_param_opts_text'] = $params['opts']; + + // Get extra data + if (! $all) { + return $retval; + } + + if ($retval['item_type'] == 'FUNCTION') { + $retval['item_type_toggle'] = 'PROCEDURE'; + } else { + $retval['item_type_toggle'] = 'FUNCTION'; + } + $retval['item_returntype'] = ''; + $retval['item_returnlength'] = ''; + $retval['item_returnopts_num'] = ''; + $retval['item_returnopts_text'] = ''; + + if (! empty($routine['DTD_IDENTIFIER'])) { + $options = []; + foreach ($stmt->return->options->options as $opt) { + $options[] = is_string($opt) ? $opt : $opt['value']; + } + + $retval['item_returntype'] = $stmt->return->name; + $retval['item_returnlength'] = implode(',', $stmt->return->parameters); + $retval['item_returnopts_num'] = implode(' ', $options); + $retval['item_returnopts_text'] = implode(' ', $options); + } + + $retval['item_definer'] = $stmt->options->has('DEFINER'); + $retval['item_definition'] = $routine['ROUTINE_DEFINITION']; + $retval['item_isdeterministic'] = ''; + if ($routine['IS_DETERMINISTIC'] == 'YES') { + $retval['item_isdeterministic'] = " checked='checked'"; + } + $retval['item_securitytype_definer'] = ''; + $retval['item_securitytype_invoker'] = ''; + if ($routine['SECURITY_TYPE'] == 'DEFINER') { + $retval['item_securitytype_definer'] = " selected='selected'"; + } elseif ($routine['SECURITY_TYPE'] == 'INVOKER') { + $retval['item_securitytype_invoker'] = " selected='selected'"; + } + $retval['item_sqldataaccess'] = $routine['SQL_DATA_ACCESS']; + $retval['item_comment'] = $routine['ROUTINE_COMMENT']; + + return $retval; + } + + /** + * Creates one row for the parameter table used in the routine editor. + * + * @param array $routine Data for the routine returned by + * getDataFromRequest() or getDataFromName() + * @param mixed $index Either a numeric index of the row being processed + * or NULL to create a template row for AJAX request + * @param string $class Class used to hide the direction column, if the + * row is for a stored function. + * + * @return string HTML code of one row of parameter table for the editor. + */ + public function getParameterRow(array $routine = [], $index = null, $class = '') + { + global $param_directions, $param_opts_num; + + if ($index === null) { + // template row for AJAX request + $i = 0; + $index = '%s'; + $drop_class = ''; + $routine = [ + 'item_param_dir' => [0 => ''], + 'item_param_name' => [0 => ''], + 'item_param_type' => [0 => ''], + 'item_param_length' => [0 => ''], + 'item_param_opts_num' => [0 => ''], + 'item_param_opts_text' => [0 => ''], + ]; + } elseif (! empty($routine)) { + // regular row for routine editor + $drop_class = ' hide'; + $i = $index; + } else { + // No input data. This shouldn't happen, + // but better be safe than sorry. + return ''; + } + + $allCharsets = Charsets::getCharsets($this->dbi, $GLOBALS['cfg']['Server']['DisableIS']); + $charsets = []; + /** @var Charset $charset */ + foreach ($allCharsets as $charset) { + $charsets[] = [ + 'name' => $charset->getName(), + 'description' => $charset->getDescription(), + 'is_selected' => $charset->getName() === $routine['item_param_opts_text'][$i], + ]; + } + + $template = new Template(); + return $template->render('rte/routines/parameter_row', [ + 'class' => $class, + 'index' => $index, + 'param_directions' => $param_directions, + 'param_opts_num' => $param_opts_num, + 'item_param_dir' => $routine['item_param_dir'][$i] ?? '', + 'item_param_name' => $routine['item_param_name'][$i] ?? '', + 'item_param_length' => $routine['item_param_length'][$i] ?? '', + 'item_param_opts_num' => $routine['item_param_opts_num'][$i] ?? '', + 'supported_datatypes' => Util::getSupportedDatatypes( + true, + $routine['item_param_type'][$i] + ), + 'charsets' => $charsets, + 'drop_class' => $drop_class, + ]); + } + + /** + * Displays a form used to add/edit a routine + * + * @param string $mode If the editor will be used to edit a routine + * or add a new one: 'edit' or 'add'. + * @param string $operation If the editor was previously invoked with + * JS turned off, this will hold the name of + * the current operation + * @param array $routine Data for the routine returned by + * getDataFromRequest() or getDataFromName() + * + * @return string HTML code for the editor. + */ + public function getEditorForm($mode, $operation, array $routine) + { + global $db, $errors, $param_sqldataaccess, $param_opts_num; + + $response = Response::getInstance(); + + // Escape special characters + $need_escape = [ + 'item_original_name', + 'item_name', + 'item_returnlength', + 'item_definition', + 'item_definer', + 'item_comment', + ]; + foreach ($need_escape as $key => $index) { + $routine[$index] = htmlentities($routine[$index], ENT_QUOTES, 'UTF-8'); + } + for ($i = 0; $i < $routine['item_num_params']; $i++) { + $routine['item_param_name'][$i] = htmlentities( + $routine['item_param_name'][$i], + ENT_QUOTES + ); + $routine['item_param_length'][$i] = htmlentities( + $routine['item_param_length'][$i], + ENT_QUOTES + ); + } + + // Handle some logic first + if ($operation == 'change') { + if ($routine['item_type'] == 'PROCEDURE') { + $routine['item_type'] = 'FUNCTION'; + $routine['item_type_toggle'] = 'PROCEDURE'; + } else { + $routine['item_type'] = 'PROCEDURE'; + $routine['item_type_toggle'] = 'FUNCTION'; + } + } elseif ($operation == 'add' + || ($routine['item_num_params'] == 0 && $mode == 'add' && ! $errors) + ) { + $routine['item_param_dir'][] = ''; + $routine['item_param_name'][] = ''; + $routine['item_param_type'][] = ''; + $routine['item_param_length'][] = ''; + $routine['item_param_opts_num'][] = ''; + $routine['item_param_opts_text'][] = ''; + $routine['item_num_params']++; + } elseif ($operation == 'remove') { + unset($routine['item_param_dir'][$routine['item_num_params'] - 1]); + unset($routine['item_param_name'][$routine['item_num_params'] - 1]); + unset($routine['item_param_type'][$routine['item_num_params'] - 1]); + unset($routine['item_param_length'][$routine['item_num_params'] - 1]); + unset($routine['item_param_opts_num'][$routine['item_num_params'] - 1]); + unset($routine['item_param_opts_text'][$routine['item_num_params'] - 1]); + $routine['item_num_params']--; + } + $disableRemoveParam = ''; + if (! $routine['item_num_params']) { + $disableRemoveParam = " class='isdisableremoveparam_class' disabled=disabled"; + } + $original_routine = ''; + if ($mode == 'edit') { + $original_routine = "\n" + . "\n"; + } + $isfunction_class = ''; + $isprocedure_class = ''; + $isfunction_select = ''; + $isprocedure_select = ''; + if ($routine['item_type'] == 'PROCEDURE') { + $isfunction_class = ' hide'; + $isprocedure_select = " selected='selected'"; + } else { + $isprocedure_class = ' hide'; + $isfunction_select = " selected='selected'"; + } + + // Create the output + $retval = ""; + $retval .= "\n\n"; + $retval .= "
    \n"; + $retval .= "\n"; + $retval .= $original_routine; + $retval .= Url::getHiddenInputs($db) . "\n"; + $retval .= "
    \n"; + $retval .= "" . __('Details') . "\n"; + $retval .= "\n"; + $retval .= "\n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= "\n"; + $retval .= "\n"; + $retval .= " \n"; + $retval .= " "; + $retval .= ""; + $retval .= ""; + $retval .= " "; + $retval .= " "; + $retval .= ""; + // parameter handling end + $retval .= ""; + $retval .= " "; + $retval .= " "; + $retval .= ""; + $retval .= ""; + $retval .= " "; + $retval .= " "; + $retval .= " "; + $retval .= ""; + $retval .= ""; + $retval .= " "; + $retval .= " "; + $retval .= ""; + $retval .= ""; + $retval .= " "; + $retval .= " "; + $retval .= ""; + $retval .= ""; + $retval .= " "; + $retval .= " "; + $retval .= ""; + if (isset($_REQUEST['edit_item']) + && ! empty($_REQUEST['edit_item']) + ) { + $retval .= ""; + $retval .= " "; + if ($GLOBALS['proc_priv'] + && $GLOBALS['is_reload_priv'] + ) { + $retval .= " "; + } else { + $retval .= " "; + } + $retval .= ""; + } + + $retval .= ""; + $retval .= " "; + $retval .= " "; + $retval .= ""; + $retval .= ""; + $retval .= " "; + $retval .= " "; + $retval .= ""; + $retval .= ""; + $retval .= " "; + $retval .= " "; + $retval .= ""; + $retval .= ""; + $retval .= " "; + $retval .= " "; + $retval .= ""; + $retval .= "
    " . __('Routine name') . "\n"; + $retval .= " \n"; + if ($response->isAjax()) { + $retval .= " \n"; + } else { + $retval .= "\n" + . "
    \n" + . $routine['item_type'] . "\n" + . "
    \n" + . "\n"; + } + $retval .= "
    " . __('Parameters') . "\n"; + // parameter handling start + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " "; + $retval .= " \n"; + $retval .= " \n"; + for ($i = 0; $i < $routine['item_num_params']; $i++) { // each parameter + $retval .= $this->getParameterRow($routine, $i, $isprocedure_class); + } + $retval .= " \n"; + $retval .= "
    " + . __('Direction') . "" . __('Name') . "" . __('Type') . "" . __('Length/Values') . "" . __('Options') . " 
    "; + $retval .= "
     "; + $retval .= " "; + $retval .= " "; + $retval .= "
    " . __('Return type') . "
    " . __('Return length/values') . "---
    " . __('Return options') . "
    "; + $retval .= '' . "\n"; + $retval .= "
    "; + $retval .= "
    "; + $retval .= "
    ---
    "; + $retval .= "
    " . __('Definition') . "
    " . __('Is deterministic') . "
    " . __('Adjust privileges'); + $retval .= Util::showDocu('faq', 'faq6-39'); + $retval .= "
    " . __('Definer') . "
    " . __('Security type') . "
    " . __('SQL data access') . "
    " . __('Comment') . "
    "; + $retval .= "
    "; + if ($response->isAjax()) { + $retval .= ""; + $retval .= ""; + } + $retval .= "
    "; + $retval .= ""; + + return $retval; + } + + /** + * Composes the query necessary to create a routine from an HTTP request. + * + * @return string The CREATE [ROUTINE | PROCEDURE] query. + */ + public function getQueryFromRequest() + { + global $errors, $param_sqldataaccess, $param_directions, $dbi; + + $_POST['item_type'] = isset($_POST['item_type']) + ? $_POST['item_type'] : ''; + + $query = 'CREATE '; + if (! empty($_POST['item_definer'])) { + if (mb_strpos($_POST['item_definer'], '@') !== false) { + $arr = explode('@', $_POST['item_definer']); + + $do_backquote = true; + if (substr($arr[0], 0, 1) === "`" + && substr($arr[0], -1) === "`" + ) { + $do_backquote = false; + } + $query .= 'DEFINER=' . Util::backquote($arr[0], $do_backquote); + + $do_backquote = true; + if (substr($arr[1], 0, 1) === "`" + && substr($arr[1], -1) === "`" + ) { + $do_backquote = false; + } + $query .= '@' . Util::backquote($arr[1], $do_backquote) . ' '; + } else { + $errors[] = __('The definer must be in the "username@hostname" format!'); + } + } + if ($_POST['item_type'] == 'FUNCTION' + || $_POST['item_type'] == 'PROCEDURE' + ) { + $query .= $_POST['item_type'] . ' '; + } else { + $errors[] = sprintf( + __('Invalid routine type: "%s"'), + htmlspecialchars($_POST['item_type']) + ); + } + if (! empty($_POST['item_name'])) { + $query .= Util::backquote($_POST['item_name']); + } else { + $errors[] = __('You must provide a routine name!'); + } + $params = ''; + $warned_about_dir = false; + $warned_about_length = false; + + if (! empty($_POST['item_param_name']) + && ! empty($_POST['item_param_type']) + && ! empty($_POST['item_param_length']) + && is_array($_POST['item_param_name']) + && is_array($_POST['item_param_type']) + && is_array($_POST['item_param_length']) + ) { + $item_param_name = $_POST['item_param_name']; + $item_param_type = $_POST['item_param_type']; + $item_param_length = $_POST['item_param_length']; + + for ($i = 0, $nb = count($item_param_name); $i < $nb; $i++) { + if (! empty($item_param_name[$i]) + && ! empty($item_param_type[$i]) + ) { + if ($_POST['item_type'] == 'PROCEDURE' + && ! empty($_POST['item_param_dir'][$i]) + && in_array($_POST['item_param_dir'][$i], $param_directions) + ) { + $params .= $_POST['item_param_dir'][$i] . " " + . Util::backquote($item_param_name[$i]) + . " " . $item_param_type[$i]; + } elseif ($_POST['item_type'] == 'FUNCTION') { + $params .= Util::backquote($item_param_name[$i]) + . " " . $item_param_type[$i]; + } elseif (! $warned_about_dir) { + $warned_about_dir = true; + $errors[] = sprintf( + __('Invalid direction "%s" given for parameter.'), + htmlspecialchars($_POST['item_param_dir'][$i]) + ); + } + if ($item_param_length[$i] != '' + && ! preg_match( + '@^(DATE|TINYBLOB|TINYTEXT|BLOB|TEXT|' + . 'MEDIUMBLOB|MEDIUMTEXT|LONGBLOB|LONGTEXT|' + . 'SERIAL|BOOLEAN)$@i', + $item_param_type[$i] + ) + ) { + $params .= "(" . $item_param_length[$i] . ")"; + } elseif ($item_param_length[$i] == '' + && preg_match( + '@^(ENUM|SET|VARCHAR|VARBINARY)$@i', + $item_param_type[$i] + ) + ) { + if (! $warned_about_length) { + $warned_about_length = true; + $errors[] = __( + 'You must provide length/values for routine parameters' + . ' of type ENUM, SET, VARCHAR and VARBINARY.' + ); + } + } + if (! empty($_POST['item_param_opts_text'][$i])) { + if ($dbi->types->getTypeClass($item_param_type[$i]) == 'CHAR') { + if (! in_array($item_param_type[$i], ['VARBINARY', 'BINARY'])) { + $params .= ' CHARSET ' + . mb_strtolower( + $_POST['item_param_opts_text'][$i] + ); + } + } + } + if (! empty($_POST['item_param_opts_num'][$i])) { + if ($dbi->types->getTypeClass($item_param_type[$i]) == 'NUMBER') { + $params .= ' ' + . mb_strtoupper( + $_POST['item_param_opts_num'][$i] + ); + } + } + if ($i != (count($item_param_name) - 1)) { + $params .= ", "; + } + } else { + $errors[] = __( + 'You must provide a name and a type for each routine parameter.' + ); + break; + } + } + } + $query .= "(" . $params . ") "; + if ($_POST['item_type'] == 'FUNCTION') { + $item_returntype = isset($_POST['item_returntype']) + ? $_POST['item_returntype'] + : null; + + if (! empty($item_returntype) + && in_array( + $item_returntype, + Util::getSupportedDatatypes() + ) + ) { + $query .= "RETURNS " . $item_returntype; + } else { + $errors[] = __('You must provide a valid return type for the routine.'); + } + if (! empty($_POST['item_returnlength']) + && ! preg_match( + '@^(DATE|DATETIME|TIME|TINYBLOB|TINYTEXT|BLOB|TEXT|' + . 'MEDIUMBLOB|MEDIUMTEXT|LONGBLOB|LONGTEXT|SERIAL|BOOLEAN)$@i', + $item_returntype + ) + ) { + $query .= "(" . $_POST['item_returnlength'] . ")"; + } elseif (empty($_POST['item_returnlength']) + && preg_match( + '@^(ENUM|SET|VARCHAR|VARBINARY)$@i', + $item_returntype + ) + ) { + if (! $warned_about_length) { + $errors[] = __( + 'You must provide length/values for routine parameters' + . ' of type ENUM, SET, VARCHAR and VARBINARY.' + ); + } + } + if (! empty($_POST['item_returnopts_text'])) { + if ($dbi->types->getTypeClass($item_returntype) == 'CHAR') { + $query .= ' CHARSET ' + . mb_strtolower($_POST['item_returnopts_text']); + } + } + if (! empty($_POST['item_returnopts_num'])) { + if ($dbi->types->getTypeClass($item_returntype) == 'NUMBER') { + $query .= ' ' + . mb_strtoupper($_POST['item_returnopts_num']); + } + } + $query .= ' '; + } + if (! empty($_POST['item_comment'])) { + $query .= "COMMENT '" . $this->dbi->escapeString($_POST['item_comment']) + . "' "; + } + if (isset($_POST['item_isdeterministic'])) { + $query .= 'DETERMINISTIC '; + } else { + $query .= 'NOT DETERMINISTIC '; + } + if (! empty($_POST['item_sqldataaccess']) + && in_array($_POST['item_sqldataaccess'], $param_sqldataaccess) + ) { + $query .= $_POST['item_sqldataaccess'] . ' '; + } + if (! empty($_POST['item_securitytype'])) { + if ($_POST['item_securitytype'] == 'DEFINER' + || $_POST['item_securitytype'] == 'INVOKER' + ) { + $query .= 'SQL SECURITY ' . $_POST['item_securitytype'] . ' '; + } + } + if (! empty($_POST['item_definition'])) { + $query .= $_POST['item_definition']; + } else { + $errors[] = __('You must provide a routine definition.'); + } + + return $query; + } + + /** + * Handles requests for executing a routine + * + * @return void + */ + public function handleExecute() + { + global $db; + + $response = Response::getInstance(); + + /** + * Handle all user requests other than the default of listing routines + */ + if (! empty($_POST['execute_routine']) && ! empty($_POST['item_name'])) { + // Build the queries + $routine = $this->getDataFromName( + $_POST['item_name'], + $_POST['item_type'], + false + ); + if ($routine === false) { + $message = __('Error in processing request:') . ' '; + $message .= sprintf( + $this->words->get('not_found'), + htmlspecialchars(Util::backquote($_POST['item_name'])), + htmlspecialchars(Util::backquote($db)) + ); + $message = Message::error($message); + if ($response->isAjax()) { + $response->setRequestStatus(false); + $response->addJSON('message', $message); + exit; + } else { + echo $message->getDisplay(); + unset($_POST); + } + } + + $queries = []; + $end_query = []; + $args = []; + $all_functions = $this->dbi->types->getAllFunctions(); + for ($i = 0; $i < $routine['item_num_params']; $i++) { + if (isset($_POST['params'][$routine['item_param_name'][$i]])) { + $value = $_POST['params'][$routine['item_param_name'][$i]]; + if (is_array($value)) { // is SET type + $value = implode(',', $value); + } + $value = $this->dbi->escapeString($value); + if (! empty($_POST['funcs'][$routine['item_param_name'][$i]]) + && in_array( + $_POST['funcs'][$routine['item_param_name'][$i]], + $all_functions + ) + ) { + $queries[] = "SET @p$i=" + . $_POST['funcs'][$routine['item_param_name'][$i]] + . "('$value');\n"; + } else { + $queries[] = "SET @p$i='$value';\n"; + } + $args[] = "@p$i"; + } else { + $args[] = "@p$i"; + } + if ($routine['item_type'] == 'PROCEDURE') { + if ($routine['item_param_dir'][$i] == 'OUT' + || $routine['item_param_dir'][$i] == 'INOUT' + ) { + $end_query[] = "@p$i AS " + . Util::backquote($routine['item_param_name'][$i]); + } + } + } + if ($routine['item_type'] == 'PROCEDURE') { + $queries[] = "CALL " . Util::backquote($routine['item_name']) + . "(" . implode(', ', $args) . ");\n"; + if (count($end_query)) { + $queries[] = "SELECT " . implode(', ', $end_query) . ";\n"; + } + } else { + $queries[] = "SELECT " . Util::backquote($routine['item_name']) + . "(" . implode(', ', $args) . ") " + . "AS " . Util::backquote($routine['item_name']) + . ";\n"; + } + + // Get all the queries as one SQL statement + $multiple_query = implode("", $queries); + + $outcome = true; + $affected = 0; + + // Execute query + if (! $this->dbi->tryMultiQuery($multiple_query)) { + $outcome = false; + } + + // Generate output + if ($outcome) { + // Pass the SQL queries through the "pretty printer" + $output = Util::formatSql(implode($queries, "\n")); + + // Display results + $output .= "
    "; + $output .= sprintf( + __('Execution results of routine %s'), + Util::backquote(htmlspecialchars($routine['item_name'])) + ); + $output .= ""; + + $nbResultsetToDisplay = 0; + + do { + $result = $this->dbi->storeResult(); + $num_rows = $this->dbi->numRows($result); + + if (($result !== false) && ($num_rows > 0)) { + $output .= ""; + foreach ($this->dbi->getFieldsMeta($result) as $field) { + $output .= ""; + } + $output .= ""; + + while ($row = $this->dbi->fetchAssoc($result)) { + $output .= "" . $this->browseRow($row) . ""; + } + + $output .= "
    "; + $output .= htmlspecialchars($field->name); + $output .= "
    "; + $nbResultsetToDisplay++; + $affected = $num_rows; + } + + if (! $this->dbi->moreResults()) { + break; + } + + $output .= "
    "; + + $this->dbi->freeResult($result); + } while ($outcome = $this->dbi->nextResult()); + } + + if ($outcome) { + $output .= "
    "; + + $message = __('Your SQL query has been executed successfully.'); + if ($routine['item_type'] == 'PROCEDURE') { + $message .= '
    '; + + // TODO : message need to be modified according to the + // output from the routine + $message .= sprintf( + _ngettext( + '%d row affected by the last statement inside the ' + . 'procedure.', + '%d rows affected by the last statement inside the ' + . 'procedure.', + $affected + ), + $affected + ); + } + $message = Message::success($message); + + if ($nbResultsetToDisplay == 0) { + $notice = __( + 'MySQL returned an empty result set (i.e. zero rows).' + ); + $output .= Message::notice($notice)->getDisplay(); + } + } else { + $output = ''; + $message = Message::error( + sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($multiple_query) + ) + . '

    ' + . __('MySQL said: ') . $this->dbi->getError() + ); + } + + // Print/send output + if ($response->isAjax()) { + $response->setRequestStatus($message->isSuccess()); + $response->addJSON('message', $message->getDisplay() . $output); + $response->addJSON('dialog', false); + exit; + } else { + echo $message->getDisplay() , $output; + if ($message->isError()) { + // At least one query has failed, so shouldn't + // execute any more queries, so we quit. + exit; + } + unset($_POST); + // Now deliberately fall through to displaying the routines list + } + return; + } elseif (! empty($_GET['execute_dialog']) && ! empty($_GET['item_name'])) { + /** + * Display the execute form for a routine. + */ + $routine = $this->getDataFromName( + $_GET['item_name'], + $_GET['item_type'], + true + ); + if ($routine !== false) { + $form = $this->getExecuteForm($routine); + if ($response->isAjax()) { + $title = __("Execute routine") . " " . Util::backquote( + htmlentities($_GET['item_name'], ENT_QUOTES) + ); + $response->addJSON('message', $form); + $response->addJSON('title', $title); + $response->addJSON('dialog', true); + } else { + echo "\n\n

    " . __("Execute routine") . "

    \n\n"; + echo $form; + } + exit; + } elseif ($response->isAjax()) { + $message = __('Error in processing request:') . ' '; + $message .= sprintf( + $this->words->get('not_found'), + htmlspecialchars(Util::backquote($_GET['item_name'])), + htmlspecialchars(Util::backquote($db)) + ); + $message = Message::error($message); + + $response->setRequestStatus(false); + $response->addJSON('message', $message); + exit; + } + } + } + + /** + * Browse row array + * + * @param array $row Columns + * + * @return string + */ + private function browseRow(array $row) + { + $output = null; + foreach ($row as $value) { + if ($value === null) { + $value = 'NULL'; + } else { + $value = htmlspecialchars($value); + } + $output .= "
    " . $value . "
    \n"; + $retval .= "\n"; + } else { + $retval .= "" . __('Routine parameters') . "\n"; + $retval .= "
    \n"; + $retval .= __('Routine parameters'); + $retval .= "
    \n"; + } + $retval .= "\n"; + $retval .= "\n"; + $retval .= "\n"; + if ($cfg['ShowFunctionFields']) { + $retval .= "\n"; + } + $retval .= "\n"; + $retval .= "\n"; + // Get a list of data types that are not yet supported. + $no_support_types = Util::unsupportedDatatypes(); + for ($i = 0; $i < $routine['item_num_params']; $i++) { // Each parameter + if ($routine['item_type'] == 'PROCEDURE' + && $routine['item_param_dir'][$i] == 'OUT' + ) { + continue; + } + $retval .= "\n\n"; + $retval .= "\n"; + $retval .= "\n"; + if ($cfg['ShowFunctionFields']) { + $retval .= "\n"; + } + // Append a class to date/time fields so that + // jQuery can attach a datepicker to them + $class = ''; + if ($routine['item_param_type'][$i] == 'DATETIME' + || $routine['item_param_type'][$i] == 'TIMESTAMP' + ) { + $class = 'datetimefield'; + } elseif ($routine['item_param_type'][$i] == 'DATE') { + $class = 'datefield'; + } + $retval .= "\n"; + $retval .= "\n"; + } + $retval .= "\n
    " . __('Name') . "" . __('Type') . "" . __('Function') . "" . __('Value') . "
    {$routine['item_param_name'][$i]}{$routine['item_param_type'][$i]}\n"; + if (false !== stripos($routine['item_param_type'][$i], 'enum') + || false !== stripos($routine['item_param_type'][$i], 'set') + || in_array( + mb_strtolower($routine['item_param_type'][$i]), + $no_support_types + ) + ) { + $retval .= "--\n"; + } else { + $field = [ + 'True_Type' => mb_strtolower( + $routine['item_param_type'][$i] + ), + 'Type' => '', + 'Key' => '', + 'Field' => '', + 'Default' => '', + 'first_timestamp' => false, + ]; + $retval .= ""; + } + $retval .= "\n"; + if (in_array($routine['item_param_type'][$i], ['ENUM', 'SET'])) { + if ($routine['item_param_type'][$i] == 'ENUM') { + $input_type = 'radio'; + } else { + $input_type = 'checkbox'; + } + foreach ($routine['item_param_length_arr'][$i] as $value) { + $value = htmlentities(Util::unQuote($value), ENT_QUOTES); + $retval .= "" + . $value . "
    \n"; + } + } elseif (in_array( + mb_strtolower($routine['item_param_type'][$i]), + $no_support_types + )) { + $retval .= "\n"; + } else { + $retval .= "\n"; + } + $retval .= "
    \n"; + if (! $response->isAjax()) { + $retval .= "
    \n\n"; + $retval .= "
    \n"; + $retval .= " \n"; + $retval .= "
    \n"; + } else { + $retval .= ""; + $retval .= ""; + } + $retval .= "\n\n"; + $retval .= "\n\n"; + + return $retval; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Rte/RteList.php b/srcs/phpmyadmin/libraries/classes/Rte/RteList.php new file mode 100644 index 0000000..0e87c6c --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Rte/RteList.php @@ -0,0 +1,518 @@ +dbi = $dbi; + $this->words = new Words(); + $this->template = new Template(); + } + + /** + * Creates a list of items containing the relevant + * information and some action links. + * + * @param string $type One of ['routine'|'trigger'|'event'] + * @param array $items An array of items + * + * @return string HTML code of the list of items + */ + public function get($type, array $items) + { + global $table; + + /** + * Conditional classes switch the list on or off + */ + $class1 = 'hide'; + $class2 = ''; + if (! $items) { + $class1 = ''; + $class2 = ' hide'; + } + /** + * Generate output + */ + $retval = "\n"; + $retval .= '
    '; + $retval .= Url::getHiddenInputs($GLOBALS['db'], $GLOBALS['table']); + $retval .= "
    \n"; + $retval .= " \n"; + $retval .= " " . $this->words->get('title') . "\n"; + $retval .= " " + . Util::showMySQLDocu($this->words->get('docu')) . "\n"; + $retval .= " \n"; + $retval .= "
    \n"; + $retval .= " " . $this->words->get('nothing') . "\n"; + $retval .= "
    \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + // th cells with a colspan need corresponding td cells, according to W3C + switch ($type) { + case 'routine': + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; // see comment above + for ($i = 0; $i < 7; $i++) { + $retval .= " \n"; + } + break; + case 'trigger': + $retval .= " \n"; + $retval .= " \n"; + if (empty($table)) { + $retval .= " \n"; + } + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; // see comment above + for ($i = 0; $i < (empty($table) ? 7 : 6); $i++) { + $retval .= " \n"; + } + break; + case 'event': + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; // see comment above + for ($i = 0; $i < 6; $i++) { + $retval .= " \n"; + } + break; + default: + break; + } + $retval .= " \n"; + $retval .= " \n"; + $response = Response::getInstance(); + foreach ($items as $item) { + if ($response->isAjax() && empty($_REQUEST['ajax_page_request'])) { + $rowclass = 'ajaxInsert hide'; + } else { + $rowclass = ''; + } + // Get each row from the correct function + switch ($type) { + case 'routine': + $retval .= $this->getRoutineRow($item, $rowclass); + break; + case 'trigger': + $retval .= $this->getTriggerRow($item, $rowclass); + break; + case 'event': + $retval .= $this->getEventRow($item, $rowclass); + break; + default: + break; + } + } + $retval .= "
    " . __('Name') . "" . __('Action') . "" . __('Type') . "" . __('Returns') . "
    " . __('Name') . "" . __('Table') . "" . __('Action') . "" . __('Time') . "" . __('Event') . "
    " . __('Name') . "" . __('Status') . "" . __('Action') . "" . __('Type') . "
    \n"; + + if (count($items)) { + $retval .= '
    '; + $retval .= $this->template->render('select_all', [ + 'pma_theme_image' => $GLOBALS['pmaThemeImage'], + 'text_dir' => $GLOBALS['text_dir'], + 'form_name' => 'rteListForm', + ]); + $retval .= Util::getButtonOrImage( + 'submit_mult', + 'mult_submit', + __('Export'), + 'b_export', + 'export' + ); + $retval .= Util::getButtonOrImage( + 'submit_mult', + 'mult_submit', + __('Drop'), + 'b_drop', + 'drop' + ); + $retval .= '
    '; + } + + $retval .= "
    \n"; + $retval .= "
    \n"; + $retval .= "\n"; + + return $retval; + } + + /** + * Creates the contents for a row in the list of routines + * + * @param array $routine An array of routine data + * @param string $rowclass Additional class + * + * @return string HTML code of a row for the list of routines + */ + public function getRoutineRow(array $routine, $rowclass = '') + { + global $url_query, $db, $titles; + + $sql_drop = sprintf( + 'DROP %s IF EXISTS %s', + $routine['type'], + Util::backquote($routine['name']) + ); + $type_link = "item_type={$routine['type']}"; + + $retval = " \n"; + $retval .= " \n"; + $retval .= ' '; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " " + . htmlspecialchars($sql_drop) . "\n"; + $retval .= " \n"; + $retval .= " " + . htmlspecialchars($routine['name']) . "\n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + + // this is for our purpose to decide whether to + // show the edit link or not, so we need the DEFINER for the routine + $where = "ROUTINE_SCHEMA " . Util::getCollateForIS() . "=" + . "'" . $this->dbi->escapeString($db) . "' " + . "AND SPECIFIC_NAME='" . $this->dbi->escapeString($routine['name']) . "'" + . "AND ROUTINE_TYPE='" . $this->dbi->escapeString($routine['type']) . "'"; + $query = "SELECT `DEFINER` FROM INFORMATION_SCHEMA.ROUTINES WHERE $where;"; + $routine_definer = $this->dbi->fetchValue($query); + + $curr_user = $this->dbi->getCurrentUser(); + + // Since editing a procedure involved dropping and recreating, check also for + // CREATE ROUTINE privilege to avoid lost procedures. + if ((Util::currentUserHasPrivilege('CREATE ROUTINE', $db) + && $curr_user == $routine_definer) + || $this->dbi->isSuperuser() + ) { + $retval .= ' ' . $titles['Edit'] . "\n"; + } else { + $retval .= " {$titles['NoEdit']}\n"; + } + $retval .= " \n"; + $retval .= " \n"; + + // There is a problem with Util::currentUserHasPrivilege(): + // it does not detect all kinds of privileges, for example + // a direct privilege on a specific routine. So, at this point, + // we show the Execute link, hoping that the user has the correct rights. + // Also, information_schema might be hiding the ROUTINE_DEFINITION + // but a routine with no input parameters can be nonetheless executed. + + // Check if the routine has any input parameters. If it does, + // we will show a dialog to get values for these parameters, + // otherwise we can execute it directly. + + $definition = $this->dbi->getDefinition( + $db, + $routine['type'], + $routine['name'] + ); + if ($definition !== null) { + $parser = new Parser($definition); + + /** + * @var CreateStatement $stmt + */ + $stmt = $parser->statements[0]; + + $params = Routine::getParameters($stmt); + + if (Util::currentUserHasPrivilege('EXECUTE', $db)) { + $execute_action = 'execute_routine'; + for ($i = 0; $i < $params['num']; $i++) { + if ($routine['type'] == 'PROCEDURE' + && $params['dir'][$i] == 'OUT' + ) { + continue; + } + $execute_action = 'execute_dialog'; + break; + } + $query_part = $execute_action . '=1&item_name=' + . urlencode($routine['name']) . '&' . $type_link; + $retval .= ' ' . $titles['Execute'] . "\n"; + } else { + $retval .= " {$titles['NoExecute']}\n"; + } + } + + $retval .= " \n"; + $retval .= " \n"; + if ((Util::currentUserHasPrivilege('CREATE ROUTINE', $db) + && $curr_user == $routine_definer) + || $this->dbi->isSuperuser() + ) { + $retval .= ' ' . $titles['Export'] . "\n"; + } else { + $retval .= " {$titles['NoExport']}\n"; + } + $retval .= " \n"; + $retval .= " \n"; + $retval .= Util::linkOrButton( + 'sql.php' . $url_query . '&sql_query=' . urlencode($sql_drop) . '&goto=db_routines.php' . urlencode("?db={$db}"), + $titles['Drop'], + ['class' => 'ajax drop_anchor'] + ); + $retval .= " \n"; + $retval .= " \n"; + $retval .= " {$routine['type']}\n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " " + . htmlspecialchars($routine['returns']) . "\n"; + $retval .= " \n"; + $retval .= " \n"; + + return $retval; + } + + /** + * Creates the contents for a row in the list of triggers + * + * @param array $trigger An array of routine data + * @param string $rowclass Additional class + * + * @return string HTML code of a cell for the list of triggers + */ + public function getTriggerRow(array $trigger, $rowclass = '') + { + global $url_query, $db, $table, $titles; + + $retval = " \n"; + $retval .= " \n"; + $retval .= ' '; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " " + . htmlspecialchars($trigger['drop']) . "\n"; + $retval .= " \n"; + $retval .= " " . htmlspecialchars($trigger['name']) . "\n"; + $retval .= " \n"; + $retval .= " \n"; + if (empty($table)) { + $retval .= " \n"; + $retval .= "" + . htmlspecialchars($trigger['table']) . ""; + $retval .= " \n"; + } + $retval .= " \n"; + if (Util::currentUserHasPrivilege('TRIGGER', $db, $table)) { + $retval .= ' ' . $titles['Edit'] . "\n"; + } else { + $retval .= " {$titles['NoEdit']}\n"; + } + $retval .= " \n"; + $retval .= " \n"; + $retval .= ' ' . $titles['Export'] . "\n"; + $retval .= " \n"; + $retval .= " \n"; + if (Util::currentUserHasPrivilege('TRIGGER', $db)) { + $retval .= Util::linkOrButton( + 'sql.php' . $url_query . '&sql_query=' . urlencode($trigger['drop']) . '&goto=db_triggers.php' . urlencode("?db={$db}"), + $titles['Drop'], + ['class' => 'ajax drop_anchor'] + ); + } else { + $retval .= " {$titles['NoDrop']}\n"; + } + $retval .= " \n"; + $retval .= " \n"; + $retval .= " {$trigger['action_timing']}\n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " {$trigger['event_manipulation']}\n"; + $retval .= " \n"; + $retval .= " \n"; + + return $retval; + } + + /** + * Creates the contents for a row in the list of events + * + * @param array $event An array of routine data + * @param string $rowclass Additional class + * + * @return string HTML code of a cell for the list of events + */ + public function getEventRow(array $event, $rowclass = '') + { + global $url_query, $db, $titles; + + $sql_drop = sprintf( + 'DROP EVENT IF EXISTS %s', + Util::backquote($event['name']) + ); + + $retval = " \n"; + $retval .= " \n"; + $retval .= ' '; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " " + . htmlspecialchars($sql_drop) . "\n"; + $retval .= " \n"; + $retval .= " " + . htmlspecialchars($event['name']) . "\n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= " {$event['status']}\n"; + $retval .= " \n"; + $retval .= " \n"; + if (Util::currentUserHasPrivilege('EVENT', $db)) { + $retval .= ' ' . $titles['Edit'] . "\n"; + } else { + $retval .= " {$titles['NoEdit']}\n"; + } + $retval .= " \n"; + $retval .= " \n"; + $retval .= ' ' . $titles['Export'] . "\n"; + $retval .= " \n"; + $retval .= " \n"; + if (Util::currentUserHasPrivilege('EVENT', $db)) { + $retval .= Util::linkOrButton( + 'sql.php' . $url_query . '&sql_query=' . urlencode($sql_drop) . '&goto=db_events.php' . urlencode("?db={$db}"), + $titles['Drop'], + ['class' => 'ajax drop_anchor'] + ); + } else { + $retval .= " {$titles['NoDrop']}\n"; + } + $retval .= " \n"; + $retval .= " \n"; + $retval .= " {$event['type']}\n"; + $retval .= " \n"; + $retval .= " \n"; + + return $retval; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Rte/Triggers.php b/srcs/phpmyadmin/libraries/classes/Rte/Triggers.php new file mode 100644 index 0000000..45ce02f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Rte/Triggers.php @@ -0,0 +1,527 @@ +dbi = $dbi; + $this->export = new Export($this->dbi); + $this->footer = new Footer($this->dbi); + $this->general = new General($this->dbi); + $this->rteList = new RteList($this->dbi); + $this->words = new Words(); + } + + /** + * Sets required globals + * + * @return void + */ + public function setGlobals() + { + global $action_timings, $event_manipulations; + + // Some definitions for triggers + $action_timings = [ + 'BEFORE', + 'AFTER', + ]; + $event_manipulations = [ + 'INSERT', + 'UPDATE', + 'DELETE', + ]; + } + + /** + * Main function for the triggers functionality + * + * @return void + */ + public function main() + { + global $db, $table; + + $this->setGlobals(); + /** + * Process all requests + */ + $this->handleEditor(); + $this->export->triggers(); + /** + * Display a list of available triggers + */ + $items = $this->dbi->getTriggers($db, $table); + echo $this->rteList->get('trigger', $items); + /** + * Display a link for adding a new trigger, + * if the user has the necessary privileges + */ + echo $this->footer->triggers(); + } + + /** + * Handles editor requests for adding or editing an item + * + * @return void + */ + public function handleEditor() + { + global $errors, $db, $table; + + if (! empty($_POST['editor_process_add']) + || ! empty($_POST['editor_process_edit']) + ) { + $sql_query = ''; + + $item_query = $this->getQueryFromRequest(); + + if (! count($errors)) { // set by PhpMyAdmin\Rte\Routines::getQueryFromRequest() + // Execute the created query + if (! empty($_POST['editor_process_edit'])) { + // Backup the old trigger, in case something goes wrong + $trigger = $this->getDataFromName($_POST['item_original_name']); + $create_item = $trigger['create']; + $drop_item = $trigger['drop'] . ';'; + $result = $this->dbi->tryQuery($drop_item); + if (! $result) { + $errors[] = sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($drop_item) + ) + . '
    ' + . __('MySQL said: ') . $this->dbi->getError(); + } else { + $result = $this->dbi->tryQuery($item_query); + if (! $result) { + $errors[] = sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($item_query) + ) + . '
    ' + . __('MySQL said: ') . $this->dbi->getError(); + // We dropped the old item, but were unable to create the + // new one. Try to restore the backup query. + $result = $this->dbi->tryQuery($create_item); + + $errors = $this->general->checkResult( + $result, + __( + 'Sorry, we failed to restore the dropped trigger.' + ), + $create_item, + $errors + ); + } else { + $message = Message::success( + __('Trigger %1$s has been modified.') + ); + $message->addParam( + Util::backquote($_POST['item_name']) + ); + $sql_query = $drop_item . $item_query; + } + } + } else { + // 'Add a new item' mode + $result = $this->dbi->tryQuery($item_query); + if (! $result) { + $errors[] = sprintf( + __('The following query has failed: "%s"'), + htmlspecialchars($item_query) + ) + . '

    ' + . __('MySQL said: ') . $this->dbi->getError(); + } else { + $message = Message::success( + __('Trigger %1$s has been created.') + ); + $message->addParam( + Util::backquote($_POST['item_name']) + ); + $sql_query = $item_query; + } + } + } + + if (count($errors)) { + $message = Message::error( + '' + . __( + 'One or more errors have occurred while processing your request:' + ) + . '' + ); + $message->addHtml('
      '); + foreach ($errors as $string) { + $message->addHtml('
    • ' . $string . '
    • '); + } + $message->addHtml('
    '); + } + + $output = Util::getMessage($message, $sql_query); + $response = Response::getInstance(); + if ($response->isAjax()) { + if ($message->isSuccess()) { + $items = $this->dbi->getTriggers($db, $table, ''); + $trigger = false; + foreach ($items as $value) { + if ($value['name'] == $_POST['item_name']) { + $trigger = $value; + } + } + $insert = false; + if (empty($table) + || ($trigger !== false && $table == $trigger['table']) + ) { + $insert = true; + $response->addJSON('new_row', $this->rteList->getTriggerRow($trigger)); + $response->addJSON( + 'name', + htmlspecialchars( + mb_strtoupper( + $_POST['item_name'] + ) + ) + ); + } + $response->addJSON('insert', $insert); + $response->addJSON('message', $output); + } else { + $response->addJSON('message', $message); + $response->setRequestStatus(false); + } + exit; + } + } + + /** + * Display a form used to add/edit a trigger, if necessary + */ + if (count($errors) + || (empty($_POST['editor_process_add']) + && empty($_POST['editor_process_edit']) + && (! empty($_REQUEST['add_item']) + || ! empty($_REQUEST['edit_item']))) // FIXME: this must be simpler than that + ) { + // Get the data for the form (if any) + if (! empty($_REQUEST['add_item'])) { + $title = $this->words->get('add'); + $item = $this->getDataFromRequest(); + $mode = 'add'; + } elseif (! empty($_REQUEST['edit_item'])) { + $title = __("Edit trigger"); + if (! empty($_REQUEST['item_name']) + && empty($_POST['editor_process_edit']) + ) { + $item = $this->getDataFromName($_REQUEST['item_name']); + if ($item !== false) { + $item['item_original_name'] = $item['item_name']; + } + } else { + $item = $this->getDataFromRequest(); + } + $mode = 'edit'; + } + $this->general->sendEditor('TRI', $mode, $item, $title, $db); + } + } + + /** + * This function will generate the values that are required to for the editor + * + * @return array Data necessary to create the editor. + */ + public function getDataFromRequest() + { + $retval = []; + $indices = [ + 'item_name', + 'item_table', + 'item_original_name', + 'item_action_timing', + 'item_event_manipulation', + 'item_definition', + 'item_definer', + ]; + foreach ($indices as $index) { + $retval[$index] = isset($_POST[$index]) ? $_POST[$index] : ''; + } + return $retval; + } + + /** + * This function will generate the values that are required to complete + * the "Edit trigger" form given the name of a trigger. + * + * @param string $name The name of the trigger. + * + * @return array|bool Data necessary to create the editor. + */ + public function getDataFromName($name) + { + global $db, $table; + + $temp = []; + $items = $this->dbi->getTriggers($db, $table, ''); + foreach ($items as $value) { + if ($value['name'] == $name) { + $temp = $value; + } + } + if (empty($temp)) { + return false; + } else { + $retval = []; + $retval['create'] = $temp['create']; + $retval['drop'] = $temp['drop']; + $retval['item_name'] = $temp['name']; + $retval['item_table'] = $temp['table']; + $retval['item_action_timing'] = $temp['action_timing']; + $retval['item_event_manipulation'] = $temp['event_manipulation']; + $retval['item_definition'] = $temp['definition']; + $retval['item_definer'] = $temp['definer']; + return $retval; + } + } + + /** + * Displays a form used to add/edit a trigger + * + * @param string $mode If the editor will be used to edit a trigger + * or add a new one: 'edit' or 'add'. + * @param array $item Data for the trigger returned by getDataFromRequest() + * or getDataFromName() + * + * @return string HTML code for the editor. + */ + public function getEditorForm($mode, array $item) + { + global $db, $table, $event_manipulations, $action_timings; + + $modeToUpper = mb_strtoupper($mode); + $response = Response::getInstance(); + + // Escape special characters + $need_escape = [ + 'item_original_name', + 'item_name', + 'item_definition', + 'item_definer', + ]; + foreach ($need_escape as $key => $index) { + $item[$index] = htmlentities($item[$index], ENT_QUOTES, 'UTF-8'); + } + $original_data = ''; + if ($mode == 'edit') { + $original_data = "\n"; + } + $query = "SELECT `TABLE_NAME` FROM `INFORMATION_SCHEMA`.`TABLES` "; + $query .= "WHERE `TABLE_SCHEMA`='" . $this->dbi->escapeString($db) . "' "; + $query .= "AND `TABLE_TYPE` IN ('BASE TABLE', 'SYSTEM VERSIONED')"; + $tables = $this->dbi->fetchResult($query); + + // Create the output + $retval = ""; + $retval .= "\n\n"; + $retval .= "
    \n"; + $retval .= "\n"; + $retval .= $original_data; + $retval .= Url::getHiddenInputs($db, $table) . "\n"; + $retval .= "
    \n"; + $retval .= "" . __('Details') . "\n"; + $retval .= "\n"; + $retval .= "\n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= "\n"; + $retval .= "\n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= "\n"; + $retval .= "\n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= "\n"; + $retval .= "\n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= "\n"; + $retval .= "\n"; + $retval .= " \n"; + $retval .= " '; + + $html_output .= ''; + + $html_output .= '' + . '' + . '' + . '' + . ''; + $current_user = $row['User']; + $current_host = $row['Host']; + $routine = $row['Routine_name']; + $html_output .= ''; + $html_output .= ''; + + $html_output .= ''; + } + return $html_output; + } + + /** + * Get the HTML for user form and check the privileges for a particular database. + * + * @param string $db database name + * + * @return string + */ + public function getHtmlForSpecificDbPrivileges($db) + { + $html_output = ''; + + if ($this->dbi->isSuperuser()) { + // check the privileges for a particular database. + $html_output = ''; + $html_output .= Url::getHiddenInputs($db); + $html_output .= '
    '; + $html_output .= '
    '; + $html_output .= '' . "\n" + . Util::getIcon('b_usrcheck') + . ' ' + . sprintf( + __('Users having access to "%s"'), + '' + . htmlspecialchars($db) + . '' + ) + . "\n" + . '' . "\n"; + + $html_output .= '
    '; + $html_output .= '
    " . __('Trigger name') . "\n"; + $retval .= " \n"; + $retval .= " \n"; + $retval .= "
    " . _pgettext('Trigger action time', 'Time') . "
    " . __('Event') . "
    " . __('Definition') . "
    " . __('Definer') . "isAjax()) { + $retval .= "\n"; + $retval .= "\n"; + } + $retval .= "\n\n"; + $retval .= "\n\n"; + + return $retval; + } + + /** + * Composes the query necessary to create a trigger from an HTTP request. + * + * @return string The CREATE TRIGGER query. + */ + public function getQueryFromRequest() + { + global $db, $errors, $action_timings, $event_manipulations; + + $query = 'CREATE '; + if (! empty($_POST['item_definer'])) { + if (mb_strpos($_POST['item_definer'], '@') !== false + ) { + $arr = explode('@', $_POST['item_definer']); + $query .= 'DEFINER=' . Util::backquote($arr[0]); + $query .= '@' . Util::backquote($arr[1]) . ' '; + } else { + $errors[] = __('The definer must be in the "username@hostname" format!'); + } + } + $query .= 'TRIGGER '; + if (! empty($_POST['item_name'])) { + $query .= Util::backquote($_POST['item_name']) . ' '; + } else { + $errors[] = __('You must provide a trigger name!'); + } + if (! empty($_POST['item_timing']) + && in_array($_POST['item_timing'], $action_timings) + ) { + $query .= $_POST['item_timing'] . ' '; + } else { + $errors[] = __('You must provide a valid timing for the trigger!'); + } + if (! empty($_POST['item_event']) + && in_array($_POST['item_event'], $event_manipulations) + ) { + $query .= $_POST['item_event'] . ' '; + } else { + $errors[] = __('You must provide a valid event for the trigger!'); + } + $query .= 'ON '; + if (! empty($_POST['item_table']) + && in_array($_POST['item_table'], $this->dbi->getTables($db)) + ) { + $query .= Util::backquote($_POST['item_table']); + } else { + $errors[] = __('You must provide a valid table name!'); + } + $query .= ' FOR EACH ROW '; + if (! empty($_POST['item_definition'])) { + $query .= $_POST['item_definition']; + } else { + $errors[] = __('You must provide a trigger definition.'); + } + + return $query; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Rte/Words.php b/srcs/phpmyadmin/libraries/classes/Rte/Words.php new file mode 100644 index 0000000..f308003 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Rte/Words.php @@ -0,0 +1,89 @@ + __('Add routine'), + 'docu' => 'STORED_ROUTINES', + 'export' => __('Export of routine %s'), + 'human' => __('routine'), + 'no_create' => __( + 'You do not have the necessary privileges to create a routine.' + ), + 'no_edit' => __( + 'No routine with name %1$s found in database %2$s. ' + . 'You might be lacking the necessary privileges to edit this routine.' + ), + 'no_view' => __( + 'No routine with name %1$s found in database %2$s. ' + . 'You might be lacking the necessary privileges to view/export this routine.' + ), + 'not_found' => __('No routine with name %1$s found in database %2$s.'), + 'nothing' => __('There are no routines to display.'), + 'title' => __('Routines'), + ]; + break; + case 'TRI': + $words = [ + 'add' => __('Add trigger'), + 'docu' => 'TRIGGERS', + 'export' => __('Export of trigger %s'), + 'human' => __('trigger'), + 'no_create' => __( + 'You do not have the necessary privileges to create a trigger.' + ), + 'not_found' => __('No trigger with name %1$s found in database %2$s.'), + 'nothing' => __('There are no triggers to display.'), + 'title' => __('Triggers'), + ]; + break; + case 'EVN': + $words = [ + 'add' => __('Add event'), + 'docu' => 'EVENTS', + 'export' => __('Export of event %s'), + 'human' => __('event'), + 'no_create' => __( + 'You do not have the necessary privileges to create an event.' + ), + 'not_found' => __('No event with name %1$s found in database %2$s.'), + 'nothing' => __('There are no events to display.'), + 'title' => __('Events'), + ]; + break; + default: + $words = []; + break; + } + + return isset($words[$index]) ? $words[$index] : ''; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Sanitize.php b/srcs/phpmyadmin/libraries/classes/Sanitize.php new file mode 100644 index 0000000..bbb58c4 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Sanitize.php @@ -0,0 +1,469 @@ +get('is_setup'); + // Adjust path to setup script location + if ($is_setup) { + foreach ($valid_starts as $key => $value) { + if (substr($value, 0, 2) === './') { + $valid_starts[$key] = '.' . $value; + } + } + } + if ($other) { + $valid_starts[] = 'mailto:'; + $valid_starts[] = 'ftp://'; + } + if ($http) { + $valid_starts[] = 'http://'; + } + if ($is_setup) { + $valid_starts[] = '?page=form&'; + $valid_starts[] = '?page=servers&'; + } + foreach ($valid_starts as $val) { + if (substr($url, 0, strlen($val)) == $val) { + return true; + } + } + return false; + } + + /** + * Callback function for replacing [a@link@target] links in bb code. + * + * @param array $found Array of preg matches + * + * @return string Replaced string + */ + public static function replaceBBLink(array $found) + { + /* Check for valid link */ + if (! self::checkLink($found[1])) { + return $found[0]; + } + /* a-z and _ allowed in target */ + if (! empty($found[3]) && preg_match('/[^a-z_]+/i', $found[3])) { + return $found[0]; + } + + /* Construct target */ + $target = ''; + if (! empty($found[3])) { + $target = ' target="' . $found[3] . '"'; + if ($found[3] == '_blank') { + $target .= ' rel="noopener noreferrer"'; + } + } + + /* Construct url */ + if (substr($found[1], 0, 4) == 'http') { + $url = Core::linkURL($found[1]); + } else { + $url = $found[1]; + } + + return ''; + } + + /** + * Callback function for replacing [doc@anchor] links in bb code. + * + * @param array $found Array of preg matches + * + * @return string Replaced string + */ + public static function replaceDocLink(array $found) + { + if (count($found) >= 4) { + $page = $found[1]; + $anchor = $found[3]; + } else { + $anchor = $found[1]; + if (strncmp('faq', $anchor, 3) == 0) { + $page = 'faq'; + } elseif (strncmp('cfg', $anchor, 3) == 0) { + $page = 'config'; + } else { + /* Guess */ + $page = 'setup'; + } + } + $link = Util::getDocuLink($page, $anchor); + return ''; + } + + /** + * Sanitizes $message, taking into account our special codes + * for formatting. + * + * If you want to include result in element attribute, you should escape it. + * + * Examples: + * + *

    + * + *
    bar + * + * @param string $message the message + * @param boolean $escape whether to escape html in result + * @param boolean $safe whether string is safe (can keep < and > chars) + * + * @return string the sanitized message + */ + public static function sanitizeMessage($message, $escape = false, $safe = false) + { + if (! $safe) { + $message = strtr((string) $message, ['<' => '<', '>' => '>']); + } + + /* Interpret bb code */ + $replace_pairs = [ + '[em]' => '', + '[/em]' => '', + '[strong]' => '', + '[/strong]' => '', + '[code]' => '', + '[/code]' => '', + '[kbd]' => '', + '[/kbd]' => '', + '[br]' => '
    ', + '[/a]' => '', + '[/doc]' => '', + '[sup]' => '', + '[/sup]' => '', + // used in common.inc.php: + '[conferr]' => '', + // used in libraries/Util.php + '[dochelpicon]' => Util::getImage('b_help', __('Documentation')), + ]; + + $message = strtr($message, $replace_pairs); + + /* Match links in bb code ([a@url@target], where @target is options) */ + $pattern = '/\[a@([^]"@]*)(@([^]"]*))?\]/'; + + /* Find and replace all links */ + $message = preg_replace_callback($pattern, function ($match) { + return self::replaceBBLink($match); + }, $message); + + /* Replace documentation links */ + $message = preg_replace_callback( + '/\[doc@([a-zA-Z0-9_-]+)(@([a-zA-Z0-9_-]*))?\]/', + function ($match) { + return self::replaceDocLink($match); + }, + $message + ); + + /* Possibly escape result */ + if ($escape) { + $message = htmlspecialchars($message); + } + + return $message; + } + + + /** + * Sanitize a filename by removing anything besides legit characters + * + * Intended usecase: + * When using a filename in a Content-Disposition header + * the value should not contain ; or " + * + * When exporting, avoiding generation of an unexpected double-extension file + * + * @param string $filename The filename + * @param boolean $replaceDots Whether to also replace dots + * + * @return string the sanitized filename + * + */ + public static function sanitizeFilename($filename, $replaceDots = false) + { + $pattern = '/[^A-Za-z0-9_'; + // if we don't have to replace dots + if (! $replaceDots) { + // then add the dot to the list of legit characters + $pattern .= '.'; + } + $pattern .= '-]/'; + $filename = preg_replace($pattern, '_', $filename); + return $filename; + } + + /** + * Format a string so it can be a string inside JavaScript code inside an + * eventhandler (onclick, onchange, on..., ). + * This function is used to displays a javascript confirmation box for + * "DROP/DELETE/ALTER" queries. + * + * @param string $a_string the string to format + * @param boolean $add_backquotes whether to add backquotes to the string or not + * + * @return string the formatted string + * + * @access public + */ + public static function jsFormat($a_string = '', $add_backquotes = true) + { + $a_string = htmlspecialchars((string) $a_string); + $a_string = self::escapeJsString($a_string); + // Needed for inline javascript to prevent some browsers + // treating it as a anchor + $a_string = str_replace('#', '\\#', $a_string); + + return $add_backquotes + ? Util::backquote($a_string) + : $a_string; + } // end of the 'jsFormat' function + + /** + * escapes a string to be inserted as string a JavaScript block + * enclosed by + * this requires only to escape ' with \' and end of script block + * + * We also remove NUL byte as some browsers (namely MSIE) ignore it and + * inserting it anywhere inside '', + '\\' => '\\\\', + '\'' => '\\\'', + '"' => '\"', + "\n" => '\n', + "\r" => '\r', + ] + ) + ); + } + + /** + * Formats a value for javascript code. + * + * @param string $value String to be formatted. + * + * @return string formatted value. + */ + public static function formatJsVal($value) + { + if (is_bool($value)) { + if ($value) { + return 'true'; + } + + return 'false'; + } + + if (is_int($value)) { + return (int) $value; + } + + return '"' . self::escapeJsString($value) . '"'; + } + + /** + * Formats an javascript assignment with proper escaping of a value + * and support for assigning array of strings. + * + * @param string $key Name of value to set + * @param mixed $value Value to set, can be either string or array of strings + * @param bool $escape Whether to escape value or keep it as it is + * (for inclusion of js code) + * + * @return string Javascript code. + */ + public static function getJsValue($key, $value, $escape = true) + { + $result = $key . ' = '; + if (! $escape) { + $result .= $value; + } elseif (is_array($value)) { + $result .= '['; + foreach ($value as $val) { + $result .= self::formatJsVal($val) . ","; + } + $result .= "];\n"; + } else { + $result .= self::formatJsVal($value) . ";\n"; + } + return $result; + } + + /** + * Prints an javascript assignment with proper escaping of a value + * and support for assigning array of strings. + * + * @param string $key Name of value to set + * @param mixed $value Value to set, can be either string or array of strings + * + * @return void + */ + public static function printJsValue($key, $value) + { + echo self::getJsValue($key, $value); + } + + /** + * Formats javascript assignment for form validation api + * with proper escaping of a value. + * + * @param string $key Name of value to set + * @param string $value Value to set + * @param boolean $addOn Check if $.validator.format is required or not + * @param boolean $comma Check if comma is required + * + * @return string Javascript code. + */ + public static function getJsValueForFormValidation($key, $value, $addOn, $comma) + { + $result = $key . ': '; + if ($addOn) { + $result .= '$.validator.format('; + } + $result .= self::formatJsVal($value); + if ($addOn) { + $result .= ')'; + } + if ($comma) { + $result .= ', '; + } + return $result; + } + + /** + * Prints javascript assignment for form validation api + * with proper escaping of a value. + * + * @param string $key Name of value to set + * @param string $value Value to set + * @param boolean $addOn Check if $.validator.format is required or not + * @param boolean $comma Check if comma is required + * + * @return void + */ + public static function printJsValueForFormValidation($key, $value, $addOn = false, $comma = true) + { + echo self::getJsValueForFormValidation($key, $value, $addOn, $comma); + } + + /** + * Removes all variables from request except whitelisted ones. + * + * @param string[] $whitelist list of variables to allow + * + * @return void + * @access public + */ + public static function removeRequestVars(&$whitelist): void + { + // do not check only $_REQUEST because it could have been overwritten + // and use type casting because the variables could have become + // strings + if (! isset($_REQUEST)) { + $_REQUEST = []; + } + if (! isset($_GET)) { + $_GET = []; + } + if (! isset($_POST)) { + $_POST = []; + } + if (! isset($_COOKIE)) { + $_COOKIE = []; + } + $keys = array_keys( + array_merge((array) $_REQUEST, (array) $_GET, (array) $_POST, (array) $_COOKIE) + ); + + foreach ($keys as $key) { + if (! in_array($key, $whitelist)) { + unset($_REQUEST[$key], $_GET[$key], $_POST[$key]); + continue; + } + + // allowed stuff could be compromised so escape it + // we require it to be a string + if (isset($_REQUEST[$key]) && ! is_string($_REQUEST[$key])) { + unset($_REQUEST[$key]); + } + if (isset($_POST[$key]) && ! is_string($_POST[$key])) { + unset($_POST[$key]); + } + if (isset($_COOKIE[$key]) && ! is_string($_COOKIE[$key])) { + unset($_COOKIE[$key]); + } + if (isset($_GET[$key]) && ! is_string($_GET[$key])) { + unset($_GET[$key]); + } + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/SavedSearches.php b/srcs/phpmyadmin/libraries/classes/SavedSearches.php new file mode 100644 index 0000000..a89d661 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/SavedSearches.php @@ -0,0 +1,466 @@ +setConfig($config); + $this->relation = $relation; + } + + /** + * Setter of id + * + * @param int|null $searchId Id of search + * + * @return static + */ + public function setId($searchId) + { + $searchId = (int) $searchId; + if (empty($searchId)) { + $searchId = null; + } + + $this->_id = $searchId; + return $this; + } + + /** + * Getter of id + * + * @return int|null + */ + public function getId() + { + return $this->_id; + } + + /** + * Setter of searchName + * + * @param string $searchName Saved search name + * + * @return static + */ + public function setSearchName($searchName) + { + $this->_searchName = $searchName; + return $this; + } + + /** + * Getter of searchName + * + * @return string + */ + public function getSearchName() + { + return $this->_searchName; + } + + /** + * Setter of config + * + * @param array $config Global configuration + * + * @return static + */ + public function setConfig(array $config) + { + $this->_config = $config; + return $this; + } + + /** + * Getter of config + * + * @return array + */ + public function getConfig() + { + return $this->_config; + } + + /** + * Setter for criterias + * + * @param array|string $criterias Criterias of saved searches + * @param bool $json Criterias are in JSON format + * + * @return static + */ + public function setCriterias($criterias, $json = false) + { + if (true === $json && is_string($criterias)) { + $this->_criterias = json_decode($criterias, true); + return $this; + } + + $aListFieldsToGet = [ + 'criteriaColumn', + 'criteriaSort', + 'criteriaShow', + 'criteria', + 'criteriaAndOrRow', + 'criteriaAndOrColumn', + 'rows', + 'TableList', + ]; + + $data = []; + + $data['criteriaColumnCount'] = count($criterias['criteriaColumn']); + + foreach ($aListFieldsToGet as $field) { + if (isset($criterias[$field])) { + $data[$field] = $criterias[$field]; + } + } + + /* Limit amount of rows */ + if (! isset($data['rows'])) { + $data['rows'] = 0; + } else { + $data['rows'] = min( + max(0, intval($data['rows'])), + 100 + ); + } + + for ($i = 0; $i <= $data['rows']; $i++) { + $data['Or' . $i] = $criterias['Or' . $i]; + } + + $this->_criterias = $data; + return $this; + } + + /** + * Getter for criterias + * + * @return array + */ + public function getCriterias() + { + return $this->_criterias; + } + + /** + * Setter for username + * + * @param string $username Username + * + * @return static + */ + public function setUsername($username) + { + $this->_username = $username; + return $this; + } + + /** + * Getter for username + * + * @return string + */ + public function getUsername() + { + return $this->_username; + } + + /** + * Setter for DB name + * + * @param string $dbname DB name + * + * @return static + */ + public function setDbname($dbname) + { + $this->_dbname = $dbname; + return $this; + } + + /** + * Getter for DB name + * + * @return string + */ + public function getDbname() + { + return $this->_dbname; + } + + /** + * Save the search + * + * @return boolean + */ + public function save() + { + if (null == $this->getSearchName()) { + $message = Message::error( + __('Please provide a name for this bookmarked search.') + ); + $response = Response::getInstance(); + $response->setRequestStatus($message->isSuccess()); + $response->addJSON('fieldWithError', 'searchName'); + $response->addJSON('message', $message); + exit; + } + + if (null == $this->getUsername() + || null == $this->getDbname() + || null == $this->getSearchName() + || null == $this->getCriterias() + ) { + $message = Message::error( + __('Missing information to save the bookmarked search.') + ); + $response = Response::getInstance(); + $response->setRequestStatus($message->isSuccess()); + $response->addJSON('message', $message); + exit; + } + + $savedSearchesTbl + = Util::backquote($this->_config['cfgRelation']['db']) . "." + . Util::backquote($this->_config['cfgRelation']['savedsearches']); + + //If it's an insert. + if (null === $this->getId()) { + $wheres = [ + "search_name = '" . $GLOBALS['dbi']->escapeString($this->getSearchName()) + . "'", + ]; + $existingSearches = $this->getList($wheres); + + if (! empty($existingSearches)) { + $message = Message::error( + __('An entry with this name already exists.') + ); + $response = Response::getInstance(); + $response->setRequestStatus($message->isSuccess()); + $response->addJSON('fieldWithError', 'searchName'); + $response->addJSON('message', $message); + exit; + } + + $sqlQuery = "INSERT INTO " . $savedSearchesTbl + . "(`username`, `db_name`, `search_name`, `search_data`)" + . " VALUES (" + . "'" . $GLOBALS['dbi']->escapeString($this->getUsername()) . "'," + . "'" . $GLOBALS['dbi']->escapeString($this->getDbname()) . "'," + . "'" . $GLOBALS['dbi']->escapeString($this->getSearchName()) . "'," + . "'" . $GLOBALS['dbi']->escapeString(json_encode($this->getCriterias())) + . "')"; + + $result = (bool) $this->relation->queryAsControlUser($sqlQuery); + if (! $result) { + return false; + } + + $this->setId($GLOBALS['dbi']->insertId()); + + return true; + } + + //Else, it's an update. + $wheres = [ + "id != " . $this->getId(), + "search_name = '" . $GLOBALS['dbi']->escapeString($this->getSearchName()) . "'", + ]; + $existingSearches = $this->getList($wheres); + + if (! empty($existingSearches)) { + $message = Message::error( + __('An entry with this name already exists.') + ); + $response = Response::getInstance(); + $response->setRequestStatus($message->isSuccess()); + $response->addJSON('fieldWithError', 'searchName'); + $response->addJSON('message', $message); + exit; + } + + $sqlQuery = "UPDATE " . $savedSearchesTbl + . "SET `search_name` = '" + . $GLOBALS['dbi']->escapeString($this->getSearchName()) . "', " + . "`search_data` = '" + . $GLOBALS['dbi']->escapeString(json_encode($this->getCriterias())) . "' " + . "WHERE id = " . $this->getId(); + return (bool) $this->relation->queryAsControlUser($sqlQuery); + } + + /** + * Delete the search + * + * @return boolean + */ + public function delete() + { + if (null == $this->getId()) { + $message = Message::error( + __('Missing information to delete the search.') + ); + $response = Response::getInstance(); + $response->setRequestStatus($message->isSuccess()); + $response->addJSON('fieldWithError', 'searchId'); + $response->addJSON('message', $message); + exit; + } + + $savedSearchesTbl + = Util::backquote($this->_config['cfgRelation']['db']) . "." + . Util::backquote($this->_config['cfgRelation']['savedsearches']); + + $sqlQuery = "DELETE FROM " . $savedSearchesTbl + . "WHERE id = '" . $GLOBALS['dbi']->escapeString($this->getId()) . "'"; + + return (bool) $this->relation->queryAsControlUser($sqlQuery); + } + + /** + * Load the current search from an id. + * + * @return bool Success + */ + public function load() + { + if (null == $this->getId()) { + $message = Message::error( + __('Missing information to load the search.') + ); + $response = Response::getInstance(); + $response->setRequestStatus($message->isSuccess()); + $response->addJSON('fieldWithError', 'searchId'); + $response->addJSON('message', $message); + exit; + } + + $savedSearchesTbl = Util::backquote($this->_config['cfgRelation']['db']) + . "." + . Util::backquote($this->_config['cfgRelation']['savedsearches']); + $sqlQuery = "SELECT id, search_name, search_data " + . "FROM " . $savedSearchesTbl . " " + . "WHERE id = '" . $GLOBALS['dbi']->escapeString($this->getId()) . "' "; + + $resList = $this->relation->queryAsControlUser($sqlQuery); + + if (false === ($oneResult = $GLOBALS['dbi']->fetchArray($resList))) { + $message = Message::error(__('Error while loading the search.')); + $response = Response::getInstance(); + $response->setRequestStatus($message->isSuccess()); + $response->addJSON('fieldWithError', 'searchId'); + $response->addJSON('message', $message); + exit; + } + + $this->setSearchName($oneResult['search_name']) + ->setCriterias($oneResult['search_data'], true); + + return true; + } + + /** + * Get the list of saved searches of a user on a DB + * + * @param string[] $wheres List of filters + * + * @return array List of saved searches or empty array on failure + */ + public function getList(array $wheres = []) + { + if (null == $this->getUsername() + || null == $this->getDbname() + ) { + return []; + } + + $savedSearchesTbl = Util::backquote($this->_config['cfgRelation']['db']) + . "." + . Util::backquote($this->_config['cfgRelation']['savedsearches']); + $sqlQuery = "SELECT id, search_name " + . "FROM " . $savedSearchesTbl . " " + . "WHERE " + . "username = '" . $GLOBALS['dbi']->escapeString($this->getUsername()) . "' " + . "AND db_name = '" . $GLOBALS['dbi']->escapeString($this->getDbname()) . "' "; + + foreach ($wheres as $where) { + $sqlQuery .= "AND " . $where . " "; + } + + $sqlQuery .= "order by search_name ASC "; + + $resList = $this->relation->queryAsControlUser($sqlQuery); + + $list = []; + while ($oneResult = $GLOBALS['dbi']->fetchArray($resList)) { + $list[$oneResult['id']] = $oneResult['search_name']; + } + + return $list; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Scripts.php b/srcs/phpmyadmin/libraries/classes/Scripts.php new file mode 100644 index 0000000..498f419 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Scripts.php @@ -0,0 +1,164 @@ +template = new Template(); + $this->_files = []; + $this->_code = ''; + } + + /** + * Adds a new file to the list of scripts + * + * @param string $filename The name of the file to include + * @param array $params Additional parameters to pass to the file + * + * @return void + */ + public function addFile( + $filename, + array $params = [] + ) { + $hash = md5($filename); + if (! empty($this->_files[$hash])) { + return; + } + + $has_onload = $this->_eventBlacklist($filename); + $this->_files[$hash] = [ + 'has_onload' => $has_onload, + 'filename' => $filename, + 'params' => $params, + ]; + } + + /** + * Add new files to the list of scripts + * + * @param array $filelist The array of file names + * + * @return void + */ + public function addFiles(array $filelist) + { + foreach ($filelist as $filename) { + $this->addFile($filename); + } + } + + /** + * Determines whether to fire up an onload event for a file + * + * @param string $filename The name of the file to be checked + * against the blacklist + * + * @return int 1 to fire up the event, 0 not to + */ + private function _eventBlacklist($filename) + { + if (strpos($filename, 'jquery') !== false + || strpos($filename, 'codemirror') !== false + || strpos($filename, 'messages.php') !== false + || strpos($filename, 'ajax.js') !== false + || strpos($filename, 'cross_framing_protection.js') !== false + ) { + return 0; + } + + return 1; + } + + /** + * Adds a new code snippet to the code to be executed + * + * @param string $code The JS code to be added + * + * @return void + */ + public function addCode($code) + { + $this->_code .= "$code\n"; + } + + /** + * Returns a list with filenames and a flag to indicate + * whether to register onload events for this file + * + * @return array + */ + public function getFiles() + { + $retval = []; + foreach ($this->_files as $file) { + //If filename contains a "?", continue. + if (strpos($file['filename'], "?") !== false) { + continue; + } + $retval[] = [ + 'name' => $file['filename'], + 'fire' => $file['has_onload'], + ]; + } + return $retval; + } + + /** + * Renders all the JavaScript file inclusions, code and events + * + * @return string + */ + public function getDisplay() + { + return $this->template->render('scripts', [ + 'files' => $this->_files, + 'version' => PMA_VERSION, + 'code' => $this->_code, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Server/Plugin.php b/srcs/phpmyadmin/libraries/classes/Server/Plugin.php new file mode 100644 index 0000000..9b45297 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Server/Plugin.php @@ -0,0 +1,274 @@ +name = $name; + $this->version = $version; + $this->status = $status; + $this->type = $type; + $this->typeVersion = $typeVersion; + $this->library = $library; + $this->libraryVersion = $libraryVersion; + $this->author = $author; + $this->description = $description; + $this->license = $license; + $this->loadOption = $loadOption; + $this->maturity = $maturity; + $this->authVersion = $authVersion; + } + + /** + * @param array $state array with the properties + * @return self + */ + public static function fromState(array $state): self + { + return new self( + $state['name'] ?? '', + $state['version'] ?? null, + $state['status'] ?? '', + $state['type'] ?? '', + $state['typeVersion'] ?? null, + $state['library'] ?? null, + $state['libraryVersion'] ?? null, + $state['author'] ?? null, + $state['description'] ?? null, + $state['license'] ?? '', + $state['loadOption'] ?? null, + $state['maturity'] ?? null, + $state['authVersion'] ?? null + ); + } + + /** + * @return array + */ + public function toArray(): array + { + return [ + 'name' => $this->getName(), + 'version' => $this->getVersion(), + 'status' => $this->getStatus(), + 'type' => $this->getType(), + 'type_version' => $this->getTypeVersion(), + 'library' => $this->getLibrary(), + 'library_version' => $this->getLibraryVersion(), + 'author' => $this->getAuthor(), + 'description' => $this->getDescription(), + 'license' => $this->getLicense(), + 'load_option' => $this->getLoadOption(), + 'maturity' => $this->getMaturity(), + 'auth_version' => $this->getAuthVersion(), + ]; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return string|null + */ + public function getVersion(): ?string + { + return $this->version; + } + + /** + * @return string + */ + public function getStatus(): string + { + return $this->status; + } + + /** + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** + * @return string|null + */ + public function getTypeVersion(): ?string + { + return $this->typeVersion; + } + + /** + * @return string|null + */ + public function getLibrary(): ?string + { + return $this->library; + } + + /** + * @return string|null + */ + public function getLibraryVersion(): ?string + { + return $this->libraryVersion; + } + + /** + * @return string|null + */ + public function getAuthor(): ?string + { + return $this->author; + } + + /** + * @return string|null + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * @return string + */ + public function getLicense(): string + { + return $this->license; + } + + /** + * @return string|null + */ + public function getLoadOption(): ?string + { + return $this->loadOption; + } + + /** + * @return string|null + */ + public function getMaturity(): ?string + { + return $this->maturity; + } + + /** + * @return string|null + */ + public function getAuthVersion(): ?string + { + return $this->authVersion; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Server/Plugins.php b/srcs/phpmyadmin/libraries/classes/Server/Plugins.php new file mode 100644 index 0000000..eb8e85a --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Server/Plugins.php @@ -0,0 +1,74 @@ +dbi = $dbi; + } + + /** + * @return Plugin[] + */ + public function getAll(): array + { + global $cfg; + + $sql = 'SHOW PLUGINS'; + if (! $cfg['Server']['DisableIS']) { + $sql = 'SELECT * FROM information_schema.PLUGINS ORDER BY PLUGIN_TYPE, PLUGIN_NAME'; + } + $result = $this->dbi->query($sql); + $plugins = []; + while ($row = $this->dbi->fetchAssoc($result)) { + $plugins[] = $this->mapRowToPlugin($row); + } + $this->dbi->freeResult($result); + + return $plugins; + } + + /** + * @param array $row Row fetched from database + * @return Plugin + */ + private function mapRowToPlugin(array $row): Plugin + { + return Plugin::fromState([ + 'name' => $row['PLUGIN_NAME'] ?? $row['Name'], + 'version' => $row['PLUGIN_VERSION'] ?? null, + 'status' => $row['PLUGIN_STATUS'] ?? $row['Status'], + 'type' => $row['PLUGIN_TYPE'] ?? $row['Type'], + 'typeVersion' => $row['PLUGIN_TYPE_VERSION'] ?? null, + 'library' => $row['PLUGIN_LIBRARY'] ?? $row['Library'] ?? null, + 'libraryVersion' => $row['PLUGIN_LIBRARY_VERSION'] ?? null, + 'author' => $row['PLUGIN_AUTHOR'] ?? null, + 'description' => $row['PLUGIN_DESCRIPTION'] ?? null, + 'license' => $row['PLUGIN_LICENSE'] ?? $row['License'], + 'loadOption' => $row['LOAD_OPTION'] ?? null, + 'maturity' => $row['PLUGIN_MATURITY'] ?? null, + 'authVersion' => $row['PLUGIN_AUTH_VERSION'] ?? null, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Server/Privileges.php b/srcs/phpmyadmin/libraries/classes/Server/Privileges.php new file mode 100644 index 0000000..1e50fbb --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Server/Privileges.php @@ -0,0 +1,5649 @@ +template = $template; + $this->dbi = $dbi; + $this->relation = $relation; + $this->relationCleanup = $relationCleanup; + } + + /** + * Get Html for User Group Dialog + * + * @param string $username username + * @param bool $is_menuswork Is menuswork set in configuration + * + * @return string html + */ + public function getHtmlForUserGroupDialog($username, $is_menuswork) + { + $html = ''; + if (! empty($_GET['edit_user_group_dialog']) && $is_menuswork) { + $dialog = $this->getHtmlToChooseUserGroup($username); + $response = Response::getInstance(); + if ($response->isAjax()) { + $response->addJSON('message', $dialog); + exit; + } else { + $html .= $dialog; + } + } + + return $html; + } + + /** + * Escapes wildcard in a database+table specification + * before using it in a GRANT statement. + * + * Escaping a wildcard character in a GRANT is only accepted at the global + * or database level, not at table level; this is why I remove + * the escaping character. Internally, in mysql.tables_priv.Db there are + * no escaping (for example test_db) but in mysql.db you'll see test\_db + * for a db-specific privilege. + * + * @param string $dbname Database name + * @param string $tablename Table name + * + * @return string the escaped (if necessary) database.table + */ + public function wildcardEscapeForGrant($dbname, $tablename) + { + if (strlen($dbname) === 0) { + $db_and_table = '*.*'; + } else { + if (strlen($tablename) > 0) { + $db_and_table = Util::backquote( + Util::unescapeMysqlWildcards($dbname) + ) + . '.' . Util::backquote($tablename); + } else { + $db_and_table = Util::backquote($dbname) . '.*'; + } + } + return $db_and_table; + } + + /** + * Generates a condition on the user name + * + * @param string $initial the user's initial + * + * @return string the generated condition + */ + public function rangeOfUsers($initial = '') + { + // strtolower() is used because the User field + // might be BINARY, so LIKE would be case sensitive + if ($initial === null || $initial === '') { + return ''; + } + + $ret = " WHERE `User` LIKE '" + . $this->dbi->escapeString($initial) . "%'" + . " OR `User` LIKE '" + . $this->dbi->escapeString(mb_strtolower($initial)) + . "%'"; + return $ret; + } // end function + + /** + * Formats privilege name for a display + * + * @param array $privilege Privilege information + * @param boolean $html Whether to use HTML + * + * @return string + */ + public function formatPrivilege(array $privilege, $html) + { + if ($html) { + return '' + . $privilege[1] . ''; + } + + return $privilege[1]; + } + + /** + * Parses privileges into an array, it modifies the array + * + * @param array $row Results row from + * + * @return void + */ + public function fillInTablePrivileges(array &$row) + { + $row1 = $this->dbi->fetchSingleRow( + 'SHOW COLUMNS FROM `mysql`.`tables_priv` LIKE \'Table_priv\';', + 'ASSOC' + ); + // note: in MySQL 5.0.3 we get "Create View', 'Show view'; + // the View for Create is spelled with uppercase V + // the view for Show is spelled with lowercase v + // and there is a space between the words + + $av_grants = explode( + '\',\'', + mb_substr( + $row1['Type'], + mb_strpos($row1['Type'], '(') + 2, + mb_strpos($row1['Type'], ')') + - mb_strpos($row1['Type'], '(') - 3 + ) + ); + + $users_grants = explode(',', $row['Table_priv']); + + foreach ($av_grants as $current_grant) { + $row[$current_grant . '_priv'] + = in_array($current_grant, $users_grants) ? 'Y' : 'N'; + } + unset($row['Table_priv']); + } + + + /** + * Extracts the privilege information of a priv table row + * + * @param array|null $row the row + * @param boolean $enableHTML add tag with tooltips + * @param boolean $tablePrivs whether row contains table privileges + * + * @global resource $user_link the database connection + * + * @return array + */ + public function extractPrivInfo($row = null, $enableHTML = false, $tablePrivs = false) + { + if ($tablePrivs) { + $grants = $this->getTableGrantsArray(); + } else { + $grants = $this->getGrantsArray(); + } + + if ($row !== null && isset($row['Table_priv'])) { + $this->fillInTablePrivileges($row); + } + + $privs = []; + $allPrivileges = true; + foreach ($grants as $current_grant) { + if (($row !== null && isset($row[$current_grant[0]])) + || ($row === null && isset($GLOBALS[$current_grant[0]])) + ) { + if (($row !== null && $row[$current_grant[0]] == 'Y') + || ($row === null + && ($GLOBALS[$current_grant[0]] == 'Y' + || (is_array($GLOBALS[$current_grant[0]]) + && count($GLOBALS[$current_grant[0]]) == $_REQUEST['column_count'] + && empty($GLOBALS[$current_grant[0] . '_none'])))) + ) { + $privs[] = $this->formatPrivilege($current_grant, $enableHTML); + } elseif (! empty($GLOBALS[$current_grant[0]]) + && is_array($GLOBALS[$current_grant[0]]) + && empty($GLOBALS[$current_grant[0] . '_none']) + ) { + // Required for proper escaping of ` (backtick) in a column name + $grant_cols = array_map( + function ($val) { + return Util::backquote($val); + }, + $GLOBALS[$current_grant[0]] + ); + + $privs[] = $this->formatPrivilege($current_grant, $enableHTML) + . ' (' . implode(', ', $grant_cols) . ')'; + } else { + $allPrivileges = false; + } + } + } + if (empty($privs)) { + if ($enableHTML) { + $privs[] = 'USAGE'; + } else { + $privs[] = 'USAGE'; + } + } elseif ($allPrivileges + && (! isset($_POST['grant_count']) || count($privs) == $_POST['grant_count']) + ) { + if ($enableHTML) { + $privs = ['ALL PRIVILEGES', + ]; + } else { + $privs = ['ALL PRIVILEGES']; + } + } + return $privs; + } + + /** + * Returns an array of table grants and their descriptions + * + * @return array array of table grants + */ + public function getTableGrantsArray() + { + return [ + [ + 'Delete', + 'DELETE', + $GLOBALS['strPrivDescDelete'], + ], + [ + 'Create', + 'CREATE', + $GLOBALS['strPrivDescCreateTbl'], + ], + [ + 'Drop', + 'DROP', + $GLOBALS['strPrivDescDropTbl'], + ], + [ + 'Index', + 'INDEX', + $GLOBALS['strPrivDescIndex'], + ], + [ + 'Alter', + 'ALTER', + $GLOBALS['strPrivDescAlter'], + ], + [ + 'Create View', + 'CREATE_VIEW', + $GLOBALS['strPrivDescCreateView'], + ], + [ + 'Show view', + 'SHOW_VIEW', + $GLOBALS['strPrivDescShowView'], + ], + [ + 'Trigger', + 'TRIGGER', + $GLOBALS['strPrivDescTrigger'], + ], + ]; + } + + /** + * Get the grants array which contains all the privilege types + * and relevant grant messages + * + * @return array + */ + public function getGrantsArray() + { + return [ + [ + 'Select_priv', + 'SELECT', + __('Allows reading data.'), + ], + [ + 'Insert_priv', + 'INSERT', + __('Allows inserting and replacing data.'), + ], + [ + 'Update_priv', + 'UPDATE', + __('Allows changing data.'), + ], + [ + 'Delete_priv', + 'DELETE', + __('Allows deleting data.'), + ], + [ + 'Create_priv', + 'CREATE', + __('Allows creating new databases and tables.'), + ], + [ + 'Drop_priv', + 'DROP', + __('Allows dropping databases and tables.'), + ], + [ + 'Reload_priv', + 'RELOAD', + __('Allows reloading server settings and flushing the server\'s caches.'), + ], + [ + 'Shutdown_priv', + 'SHUTDOWN', + __('Allows shutting down the server.'), + ], + [ + 'Process_priv', + 'PROCESS', + __('Allows viewing processes of all users.'), + ], + [ + 'File_priv', + 'FILE', + __('Allows importing data from and exporting data into files.'), + ], + [ + 'References_priv', + 'REFERENCES', + __('Has no effect in this MySQL version.'), + ], + [ + 'Index_priv', + 'INDEX', + __('Allows creating and dropping indexes.'), + ], + [ + 'Alter_priv', + 'ALTER', + __('Allows altering the structure of existing tables.'), + ], + [ + 'Show_db_priv', + 'SHOW DATABASES', + __('Gives access to the complete list of databases.'), + ], + [ + 'Super_priv', + 'SUPER', + __( + 'Allows connecting, even if maximum number of connections ' + . 'is reached; required for most administrative operations ' + . 'like setting global variables or killing threads of other users.' + ), + ], + [ + 'Create_tmp_table_priv', + 'CREATE TEMPORARY TABLES', + __('Allows creating temporary tables.'), + ], + [ + 'Lock_tables_priv', + 'LOCK TABLES', + __('Allows locking tables for the current thread.'), + ], + [ + 'Repl_slave_priv', + 'REPLICATION SLAVE', + __('Needed for the replication slaves.'), + ], + [ + 'Repl_client_priv', + 'REPLICATION CLIENT', + __('Allows the user to ask where the slaves / masters are.'), + ], + [ + 'Create_view_priv', + 'CREATE VIEW', + __('Allows creating new views.'), + ], + [ + 'Event_priv', + 'EVENT', + __('Allows to set up events for the event scheduler.'), + ], + [ + 'Trigger_priv', + 'TRIGGER', + __('Allows creating and dropping triggers.'), + ], + // for table privs: + [ + 'Create View_priv', + 'CREATE VIEW', + __('Allows creating new views.'), + ], + [ + 'Show_view_priv', + 'SHOW VIEW', + __('Allows performing SHOW CREATE VIEW queries.'), + ], + // for table privs: + [ + 'Show view_priv', + 'SHOW VIEW', + __('Allows performing SHOW CREATE VIEW queries.'), + ], + [ + 'Delete_history_priv', + 'DELETE HISTORY', + $GLOBALS['strPrivDescDeleteHistoricalRows'], + ], + [ + 'Delete versioning rows_priv', + 'DELETE HISTORY', + $GLOBALS['strPrivDescDeleteHistoricalRows'], + ], + [ + 'Create_routine_priv', + 'CREATE ROUTINE', + __('Allows creating stored routines.'), + ], + [ + 'Alter_routine_priv', + 'ALTER ROUTINE', + __('Allows altering and dropping stored routines.'), + ], + [ + 'Create_user_priv', + 'CREATE USER', + __('Allows creating, dropping and renaming user accounts.'), + ], + [ + 'Execute_priv', + 'EXECUTE', + __('Allows executing stored routines.'), + ], + ]; + } + + /** + * Displays on which column(s) a table-specific privilege is granted + * + * @param array $columns columns array + * @param array $row first row from result or boolean false + * @param string $name_for_select privilege types - Select_priv, Insert_priv + * Update_priv, References_priv + * @param string $priv_for_header privilege for header + * @param string $name privilege name: insert, select, update, references + * @param string $name_for_dfn name for dfn + * @param string $name_for_current name for current + * + * @return string html snippet + */ + public function getHtmlForColumnPrivileges( + array $columns, + array $row, + $name_for_select, + $priv_for_header, + $name, + $name_for_dfn, + $name_for_current + ) { + return $this->template->render('server/privileges/column_privileges', [ + 'columns' => $columns, + 'row' => $row, + 'name_for_select' => $name_for_select, + 'priv_for_header' => $priv_for_header, + 'name' => $name, + 'name_for_dfn' => $name_for_dfn, + 'name_for_current' => $name_for_current, + ]); + } + + /** + * Get sql query for display privileges table + * + * @param string $db the database + * @param string $table the table + * @param string $username username for database connection + * @param string $hostname hostname for database connection + * + * @return string sql query + */ + public function getSqlQueryForDisplayPrivTable($db, $table, $username, $hostname) + { + if ($db == '*') { + return "SELECT * FROM `mysql`.`user`" + . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'" + . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "';"; + } elseif ($table == '*') { + return "SELECT * FROM `mysql`.`db`" + . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'" + . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "'" + . " AND '" . $this->dbi->escapeString(Util::unescapeMysqlWildcards($db)) . "'" + . " LIKE `Db`;"; + } + return "SELECT `Table_priv`" + . " FROM `mysql`.`tables_priv`" + . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'" + . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "'" + . " AND `Db` = '" . $this->dbi->escapeString(Util::unescapeMysqlWildcards($db)) . "'" + . " AND `Table_name` = '" . $this->dbi->escapeString($table) . "';"; + } + + /** + * Displays a dropdown to select the user group + * with menu items configured to each of them. + * + * @param string $username username + * + * @return string html to select the user group + */ + public function getHtmlToChooseUserGroup($username) + { + $cfgRelation = $this->relation->getRelationsParam(); + $groupTable = Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['usergroups']); + $userTable = Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['users']); + + $userGroup = ''; + if (isset($GLOBALS['username'])) { + $sql_query = "SELECT `usergroup` FROM " . $userTable + . " WHERE `username` = '" . $this->dbi->escapeString($username) . "'"; + $userGroup = $this->dbi->fetchValue( + $sql_query, + 0, + 0, + DatabaseInterface::CONNECT_CONTROL + ); + } + + $allUserGroups = ['' => '']; + $sql_query = "SELECT DISTINCT `usergroup` FROM " . $groupTable; + $result = $this->relation->queryAsControlUser($sql_query, false); + if ($result) { + while ($row = $this->dbi->fetchRow($result)) { + $allUserGroups[$row[0]] = $row[0]; + } + } + $this->dbi->freeResult($result); + + return $this->template->render('server/privileges/choose_user_group', [ + 'all_user_groups' => $allUserGroups, + 'user_group' => $userGroup, + 'params' => ['username' => $username], + ]); + } + + /** + * Sets the user group from request values + * + * @param string $username username + * @param string $userGroup user group to set + * + * @return void + */ + public function setUserGroup($username, $userGroup) + { + $userGroup = $userGroup === null ? '' : $userGroup; + $cfgRelation = $this->relation->getRelationsParam(); + if (empty($cfgRelation['db']) || empty($cfgRelation['users']) || empty($cfgRelation['usergroups'])) { + return; + } + + $userTable = Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['users']); + + $sql_query = "SELECT `usergroup` FROM " . $userTable + . " WHERE `username` = '" . $this->dbi->escapeString($username) . "'"; + $oldUserGroup = $this->dbi->fetchValue( + $sql_query, + 0, + 0, + DatabaseInterface::CONNECT_CONTROL + ); + + if ($oldUserGroup === false) { + $upd_query = "INSERT INTO " . $userTable . "(`username`, `usergroup`)" + . " VALUES ('" . $this->dbi->escapeString($username) . "', " + . "'" . $this->dbi->escapeString($userGroup) . "')"; + } else { + if (empty($userGroup)) { + $upd_query = "DELETE FROM " . $userTable + . " WHERE `username`='" . $this->dbi->escapeString($username) . "'"; + } elseif ($oldUserGroup != $userGroup) { + $upd_query = "UPDATE " . $userTable + . " SET `usergroup`='" . $this->dbi->escapeString($userGroup) . "'" + . " WHERE `username`='" . $this->dbi->escapeString($username) . "'"; + } + } + if (isset($upd_query)) { + $this->relation->queryAsControlUser($upd_query); + } + } + + /** + * Displays the privileges form table + * + * @param string $db the database + * @param string $table the table + * @param boolean $submit whether to display the submit button or not + * + * @global array $cfg the phpMyAdmin configuration + * @global resource $user_link the database connection + * + * @return string html snippet + */ + public function getHtmlToDisplayPrivilegesTable( + $db = '*', + $table = '*', + $submit = true + ) { + $html_output = ''; + $sql_query = ''; + + if ($db == '*') { + $table = '*'; + } + $username = ''; + $hostname = ''; + if (isset($GLOBALS['username'])) { + $username = $GLOBALS['username']; + $hostname = $GLOBALS['hostname']; + $sql_query = $this->getSqlQueryForDisplayPrivTable( + $db, + $table, + $username, + $hostname + ); + $row = $this->dbi->fetchSingleRow($sql_query); + } + if (empty($row)) { + if ($table == '*' && $this->dbi->isSuperuser()) { + $row = []; + if ($db == '*') { + $sql_query = 'SHOW COLUMNS FROM `mysql`.`user`;'; + } elseif ($table == '*') { + $sql_query = 'SHOW COLUMNS FROM `mysql`.`db`;'; + } + $res = $this->dbi->query($sql_query); + while ($row1 = $this->dbi->fetchRow($res)) { + if (mb_substr($row1[0], 0, 4) == 'max_') { + $row[$row1[0]] = 0; + } elseif (mb_substr($row1[0], 0, 5) == 'x509_' + || mb_substr($row1[0], 0, 4) == 'ssl_' + ) { + $row[$row1[0]] = ''; + } else { + $row[$row1[0]] = 'N'; + } + } + $this->dbi->freeResult($res); + } elseif ($table == '*') { + $row = []; + } else { + $row = ['Table_priv' => '']; + } + } + if (isset($row['Table_priv'])) { + $this->fillInTablePrivileges($row); + + // get columns + $res = $this->dbi->tryQuery( + 'SHOW COLUMNS FROM ' + . Util::backquote( + Util::unescapeMysqlWildcards($db) + ) + . '.' . Util::backquote($table) . ';' + ); + $columns = []; + if ($res) { + while ($row1 = $this->dbi->fetchRow($res)) { + $columns[$row1[0]] = [ + 'Select' => false, + 'Insert' => false, + 'Update' => false, + 'References' => false, + ]; + } + $this->dbi->freeResult($res); + } + unset($res, $row1); + } + // table-specific privileges + if (! empty($columns)) { + $html_output .= $this->getHtmlForTableSpecificPrivileges( + $username, + $hostname, + $db, + $table, + $columns, + $row + ); + } else { + // global or db-specific + $html_output .= $this->getHtmlForGlobalOrDbSpecificPrivs($db, $table, $row); + } + $html_output .= '' . "\n"; + if ($submit) { + $html_output .= '' . "\n"; + } + return $html_output; + } // end of the 'PMA_displayPrivTable()' function + + /** + * Get HTML for "Require" + * + * @param array $row privilege array + * + * @return string html snippet + */ + public function getHtmlForRequires(array $row) + { + $specified = (isset($row['ssl_type']) && $row['ssl_type'] == 'SPECIFIED'); + $require_options = [ + [ + 'name' => 'ssl_type', + 'value' => 'NONE', + 'description' => __( + 'Does not require SSL-encrypted connections.' + ), + 'label' => 'REQUIRE NONE', + 'checked' => isset($row['ssl_type']) + && ($row['ssl_type'] == 'NONE' + || $row['ssl_type'] == '') + ? 'checked="checked"' + : '', + 'disabled' => false, + 'radio' => true, + ], + [ + 'name' => 'ssl_type', + 'value' => 'ANY', + 'description' => __( + 'Requires SSL-encrypted connections.' + ), + 'label' => 'REQUIRE SSL', + 'checked' => isset($row['ssl_type']) && ($row['ssl_type'] == 'ANY') + ? 'checked="checked"' + : '', + 'disabled' => false, + 'radio' => true, + ], + [ + 'name' => 'ssl_type', + 'value' => 'X509', + 'description' => __( + 'Requires a valid X509 certificate.' + ), + 'label' => 'REQUIRE X509', + 'checked' => isset($row['ssl_type']) && ($row['ssl_type'] == 'X509') + ? 'checked="checked"' + : '', + 'disabled' => false, + 'radio' => true, + ], + [ + 'name' => 'ssl_type', + 'value' => 'SPECIFIED', + 'description' => '', + 'label' => 'SPECIFIED', + 'checked' => $specified ? 'checked="checked"' : '', + 'disabled' => false, + 'radio' => true, + ], + [ + 'name' => 'ssl_cipher', + 'value' => isset($row['ssl_cipher']) + ? htmlspecialchars($row['ssl_cipher']) : '', + 'description' => __( + 'Requires that a specific cipher method be used for a connection.' + ), + 'label' => 'REQUIRE CIPHER', + 'checked' => '', + 'disabled' => ! $specified, + 'radio' => false, + ], + [ + 'name' => 'x509_issuer', + 'value' => isset($row['x509_issuer']) + ? htmlspecialchars($row['x509_issuer']) : '', + 'description' => __( + 'Requires that a valid X509 certificate issued by this CA be presented.' + ), + 'label' => 'REQUIRE ISSUER', + 'checked' => '', + 'disabled' => ! $specified, + 'radio' => false, + ], + [ + 'name' => 'x509_subject', + 'value' => isset($row['x509_subject']) + ? htmlspecialchars($row['x509_subject']) : '', + 'description' => __( + 'Requires that a valid X509 certificate with this subject be presented.' + ), + 'label' => 'REQUIRE SUBJECT', + 'checked' => '', + 'disabled' => ! $specified, + 'radio' => false, + ], + ]; + + return $this->template->render('server/privileges/require_options', [ + 'require_options' => $require_options, + ]); + } + + /** + * Get HTML for "Resource limits" + * + * @param array $row first row from result or boolean false + * + * @return string html snippet + */ + public function getHtmlForResourceLimits(array $row) + { + $limits = [ + [ + 'input_name' => 'max_questions', + 'name_main' => 'MAX QUERIES PER HOUR', + 'value' => isset($row['max_questions']) ? $row['max_questions'] : '0', + 'description' => __( + 'Limits the number of queries the user may send to the server per hour.' + ), + ], + [ + 'input_name' => 'max_updates', + 'name_main' => 'MAX UPDATES PER HOUR', + 'value' => isset($row['max_updates']) ? $row['max_updates'] : '0', + 'description' => __( + 'Limits the number of commands that change any table ' + . 'or database the user may execute per hour.' + ), + ], + [ + 'input_name' => 'max_connections', + 'name_main' => 'MAX CONNECTIONS PER HOUR', + 'value' => isset($row['max_connections']) ? $row['max_connections'] : '0', + 'description' => __( + 'Limits the number of new connections the user may open per hour.' + ), + ], + [ + 'input_name' => 'max_user_connections', + 'name_main' => 'MAX USER_CONNECTIONS', + 'value' => isset($row['max_user_connections']) ? + $row['max_user_connections'] : '0', + 'description' => __( + 'Limits the number of simultaneous connections ' + . 'the user may have.' + ), + ], + ]; + + return $this->template->render('server/privileges/resource_limits', [ + 'limits' => $limits, + ]); + } + + /** + * Get the HTML snippet for routine specific privileges + * + * @param string $username username for database connection + * @param string $hostname hostname for database connection + * @param string $db the database + * @param string $routine the routine + * @param string $url_dbname url encoded db name + * + * @return string + */ + public function getHtmlForRoutineSpecificPrivileges( + $username, + $hostname, + $db, + $routine, + $url_dbname + ) { + $header = $this->getHtmlHeaderForUserProperties( + false, + $url_dbname, + $db, + $username, + $hostname, + $routine, + 'routine' + ); + + $sql = "SELECT `Proc_priv`" + . " FROM `mysql`.`procs_priv`" + . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'" + . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "'" + . " AND `Db` = '" + . $this->dbi->escapeString(Util::unescapeMysqlWildcards($db)) . "'" + . " AND `Routine_name` LIKE '" . $this->dbi->escapeString($routine) . "';"; + $res = $this->dbi->fetchValue($sql); + + $privs = $this->parseProcPriv($res); + + $routineArray = [$this->getTriggerPrivilegeTable()]; + $privTableNames = [__('Routine')]; + $privCheckboxes = $this->getHtmlForGlobalPrivTableWithCheckboxes( + $routineArray, + $privTableNames, + $privs + ); + + return $this->template->render('server/privileges/edit_routine_privileges', [ + 'username' => $username, + 'hostname' => $hostname, + 'database' => $db, + 'routine' => $routine, + 'grant_count' => count($privs), + 'priv_checkboxes' => $privCheckboxes, + 'header' => $header, + ]); + } + + /** + * Get routine privilege table as an array + * + * @return array privilege type array + */ + public function getTriggerPrivilegeTable() + { + $routinePrivTable = [ + [ + 'Grant', + 'GRANT', + __( + 'Allows user to give to other users or remove from other users ' + . 'privileges that user possess on this routine.' + ), + ], + [ + 'Alter_routine', + 'ALTER ROUTINE', + __('Allows altering and dropping this routine.'), + ], + [ + 'Execute', + 'EXECUTE', + __('Allows executing this routine.'), + ], + ]; + return $routinePrivTable; + } + + /** + * Get the HTML snippet for table specific privileges + * + * @param string $username username for database connection + * @param string $hostname hostname for database connection + * @param string $db the database + * @param string $table the table + * @param array $columns columns array + * @param array $row current privileges row + * + * @return string + */ + public function getHtmlForTableSpecificPrivileges( + $username, + $hostname, + $db, + $table, + array $columns, + array $row + ) { + $res = $this->dbi->query( + 'SELECT `Column_name`, `Column_priv`' + . ' FROM `mysql`.`columns_priv`' + . ' WHERE `User`' + . ' = \'' . $this->dbi->escapeString($username) . "'" + . ' AND `Host`' + . ' = \'' . $this->dbi->escapeString($hostname) . "'" + . ' AND `Db`' + . ' = \'' . $this->dbi->escapeString( + Util::unescapeMysqlWildcards($db) + ) . "'" + . ' AND `Table_name`' + . ' = \'' . $this->dbi->escapeString($table) . '\';' + ); + + while ($row1 = $this->dbi->fetchRow($res)) { + $row1[1] = explode(',', $row1[1]); + foreach ($row1[1] as $current) { + $columns[$row1[0]][$current] = true; + } + } + $this->dbi->freeResult($res); + unset($res, $row1, $current); + + $html_output = '' . "\n" + . '' . "\n" + . '
    ' . "\n" + . '' . __('Table-specific privileges') + . '' + . '

    ' + . __('Note: MySQL privilege names are expressed in English.') + . '

    '; + + // privs that are attached to a specific column + $html_output .= $this->getHtmlForAttachedPrivilegesToTableSpecificColumn( + $columns, + $row + ); + + // privs that are not attached to a specific column + $html_output .= '
    ' . "\n" + . $this->getHtmlForNotAttachedPrivilegesToTableSpecificColumn($row) + . '
    ' . "\n"; + + // for Safari 2.0.2 + $html_output .= '
    ' . "\n"; + + return $html_output; + } + + /** + * Get HTML snippet for privileges that are attached to a specific column + * + * @param array $columns columns array + * @param array $row first row from result or boolean false + * + * @return string + */ + public function getHtmlForAttachedPrivilegesToTableSpecificColumn(array $columns, array $row) + { + $html_output = $this->getHtmlForColumnPrivileges( + $columns, + $row, + 'Select_priv', + 'SELECT', + 'select', + __('Allows reading data.'), + 'Select' + ); + + $html_output .= $this->getHtmlForColumnPrivileges( + $columns, + $row, + 'Insert_priv', + 'INSERT', + 'insert', + __('Allows inserting and replacing data.'), + 'Insert' + ); + + $html_output .= $this->getHtmlForColumnPrivileges( + $columns, + $row, + 'Update_priv', + 'UPDATE', + 'update', + __('Allows changing data.'), + 'Update' + ); + + $html_output .= $this->getHtmlForColumnPrivileges( + $columns, + $row, + 'References_priv', + 'REFERENCES', + 'references', + __('Has no effect in this MySQL version.'), + 'References' + ); + return $html_output; + } + + /** + * Get HTML for privileges that are not attached to a specific column + * + * @param array $row first row from result or boolean false + * + * @return string + */ + public function getHtmlForNotAttachedPrivilegesToTableSpecificColumn(array $row) + { + $html_output = ''; + + foreach ($row as $current_grant => $current_grant_value) { + $grant_type = substr($current_grant, 0, -5); + if (in_array($grant_type, ['Select', 'Insert', 'Update', 'References']) + ) { + continue; + } + // make a substitution to match the messages variables; + // also we must substitute the grant we get, because we can't generate + // a form variable containing blanks (those would get changed to + // an underscore when receiving the POST) + if ($current_grant == 'Create View_priv') { + $tmp_current_grant = 'CreateView_priv'; + $current_grant = 'Create_view_priv'; + } elseif ($current_grant == 'Show view_priv') { + $tmp_current_grant = 'ShowView_priv'; + $current_grant = 'Show_view_priv'; + } elseif ($current_grant == 'Delete versioning rows_priv') { + $tmp_current_grant = 'DeleteHistoricalRows_priv'; + $current_grant = 'Delete_history_priv'; + } else { + $tmp_current_grant = $current_grant; + } + + $html_output .= '
    ' . "\n" + . '' . "\n"; + + $privGlobalName1 = 'strPrivDesc' + . mb_substr( + $tmp_current_grant, + 0, + - 5 + ); + $html_output .= '' . "\n" + . '
    ' . "\n"; + } // end foreach () + return $html_output; + } + + /** + * Get HTML for global or database specific privileges + * + * @param string $db the database + * @param string $table the table + * @param array $row first row from result or boolean false + * + * @return string + */ + public function getHtmlForGlobalOrDbSpecificPrivs($db, $table, array $row) + { + $privTable_names = [ + 0 => __('Data'), + 1 => __('Structure'), + 2 => __('Administration'), + ]; + $privTable = []; + $privTable[0] = $this->getDataPrivilegeTable($db); + $privTable[1] = $this->getStructurePrivilegeTable($table, $row); + $privTable[2] = $this->getAdministrationPrivilegeTable($db); + + $html_output = ''; + if ($db == '*') { + $legend = __('Global privileges'); + $menu_label = __('Global'); + } elseif ($table == '*') { + $legend = __('Database-specific privileges'); + $menu_label = __('Database'); + } else { + $legend = __('Table-specific privileges'); + $menu_label = __('Table'); + } + $html_output .= '
    ' + . '' . $legend + . ' ' + . ' ' + . '' + . '

    ' + . __('Note: MySQL privilege names are expressed in English.') + . '

    '; + + // Output the Global privilege tables with checkboxes + $html_output .= $this->getHtmlForGlobalPrivTableWithCheckboxes( + $privTable, + $privTable_names, + $row + ); + + // The "Resource limits" box is not displayed for db-specific privs + if ($db == '*') { + $html_output .= $this->getHtmlForResourceLimits($row); + $html_output .= $this->getHtmlForRequires($row); + } + // for Safari 2.0.2 + $html_output .= '
    '; + + return $html_output; + } + + /** + * Get data privilege table as an array + * + * @param string $db the database + * + * @return array data privilege table + */ + public function getDataPrivilegeTable($db) + { + $data_privTable = [ + [ + 'Select', + 'SELECT', + __('Allows reading data.'), + ], + [ + 'Insert', + 'INSERT', + __('Allows inserting and replacing data.'), + ], + [ + 'Update', + 'UPDATE', + __('Allows changing data.'), + ], + [ + 'Delete', + 'DELETE', + __('Allows deleting data.'), + ], + ]; + if ($db == '*') { + $data_privTable[] + = [ + 'File', + 'FILE', + __('Allows importing data from and exporting data into files.'), + ]; + } + return $data_privTable; + } + + /** + * Get structure privilege table as an array + * + * @param string $table the table + * @param array $row first row from result or boolean false + * + * @return array structure privilege table + */ + public function getStructurePrivilegeTable($table, array $row) + { + $structure_privTable = [ + [ + 'Create', + 'CREATE', + $table == '*' + ? __('Allows creating new databases and tables.') + : __('Allows creating new tables.'), + ], + [ + 'Alter', + 'ALTER', + __('Allows altering the structure of existing tables.'), + ], + [ + 'Index', + 'INDEX', + __('Allows creating and dropping indexes.'), + ], + [ + 'Drop', + 'DROP', + $table == '*' + ? __('Allows dropping databases and tables.') + : __('Allows dropping tables.'), + ], + [ + 'Create_tmp_table', + 'CREATE TEMPORARY TABLES', + __('Allows creating temporary tables.'), + ], + [ + 'Show_view', + 'SHOW VIEW', + __('Allows performing SHOW CREATE VIEW queries.'), + ], + [ + 'Create_routine', + 'CREATE ROUTINE', + __('Allows creating stored routines.'), + ], + [ + 'Alter_routine', + 'ALTER ROUTINE', + __('Allows altering and dropping stored routines.'), + ], + [ + 'Execute', + 'EXECUTE', + __('Allows executing stored routines.'), + ], + ]; + // this one is for a db-specific priv: Create_view_priv + if (isset($row['Create_view_priv'])) { + $structure_privTable[] = [ + 'Create_view', + 'CREATE VIEW', + __('Allows creating new views.'), + ]; + } + // this one is for a table-specific priv: Create View_priv + if (isset($row['Create View_priv'])) { + $structure_privTable[] = [ + 'Create View', + 'CREATE VIEW', + __('Allows creating new views.'), + ]; + } + if (isset($row['Event_priv'])) { + // MySQL 5.1.6 + $structure_privTable[] = [ + 'Event', + 'EVENT', + __('Allows to set up events for the event scheduler.'), + ]; + $structure_privTable[] = [ + 'Trigger', + 'TRIGGER', + __('Allows creating and dropping triggers.'), + ]; + } + return $structure_privTable; + } + + /** + * Get administration privilege table as an array + * + * @param string $db the table + * + * @return array administration privilege table + */ + public function getAdministrationPrivilegeTable($db) + { + if ($db == '*') { + $adminPrivTable = [ + [ + 'Grant', + 'GRANT', + __( + 'Allows adding users and privileges ' + . 'without reloading the privilege tables.' + ), + ], + ]; + $adminPrivTable[] = [ + 'Super', + 'SUPER', + __( + 'Allows connecting, even if maximum number ' + . 'of connections is reached; required for ' + . 'most administrative operations like ' + . 'setting global variables or killing threads of other users.' + ), + ]; + $adminPrivTable[] = [ + 'Process', + 'PROCESS', + __('Allows viewing processes of all users.'), + ]; + $adminPrivTable[] = [ + 'Reload', + 'RELOAD', + __('Allows reloading server settings and flushing the server\'s caches.'), + ]; + $adminPrivTable[] = [ + 'Shutdown', + 'SHUTDOWN', + __('Allows shutting down the server.'), + ]; + $adminPrivTable[] = [ + 'Show_db', + 'SHOW DATABASES', + __('Gives access to the complete list of databases.'), + ]; + } else { + $adminPrivTable = [ + [ + 'Grant', + 'GRANT', + __( + 'Allows user to give to other users or remove from other' + . ' users the privileges that user possess yourself.' + ), + ], + ]; + } + $adminPrivTable[] = [ + 'Lock_tables', + 'LOCK TABLES', + __('Allows locking tables for the current thread.'), + ]; + $adminPrivTable[] = [ + 'References', + 'REFERENCES', + __('Has no effect in this MySQL version.'), + ]; + if ($db == '*') { + $adminPrivTable[] = [ + 'Repl_client', + 'REPLICATION CLIENT', + __('Allows the user to ask where the slaves / masters are.'), + ]; + $adminPrivTable[] = [ + 'Repl_slave', + 'REPLICATION SLAVE', + __('Needed for the replication slaves.'), + ]; + $adminPrivTable[] = [ + 'Create_user', + 'CREATE USER', + __('Allows creating, dropping and renaming user accounts.'), + ]; + } + return $adminPrivTable; + } + + /** + * Get HTML snippet for global privileges table with check boxes + * + * @param array $privTable privileges table array + * @param array $privTableNames names of the privilege tables + * (Data, Structure, Administration) + * @param array $row first row from result or boolean false + * + * @return string + */ + public function getHtmlForGlobalPrivTableWithCheckboxes( + array $privTable, + array $privTableNames, + array $row + ) { + return $this->template->render('server/privileges/global_priv_table', [ + 'priv_table' => $privTable, + 'priv_table_names' => $privTableNames, + 'row' => $row, + ]); + } + + /** + * Gets the currently active authentication plugins + * + * @param string $orig_auth_plugin Default Authentication plugin + * @param string $mode are we creating a new user or are we just + * changing one? + * (allowed values: 'new', 'edit', 'change_pw') + * @param string $versions Is MySQL version newer or older than 5.5.7 + * + * @return string + */ + public function getHtmlForAuthPluginsDropdown( + $orig_auth_plugin, + $mode = 'new', + $versions = 'new' + ) { + $select_id = 'select_authentication_plugin' + . ($mode == 'change_pw' ? '_cp' : ''); + + if ($versions == 'new') { + $active_auth_plugins = $this->getActiveAuthPlugins(); + + if (isset($active_auth_plugins['mysql_old_password'])) { + unset($active_auth_plugins['mysql_old_password']); + } + } else { + $active_auth_plugins = [ + 'mysql_native_password' => __('Native MySQL authentication'), + ]; + } + + $html_output = Util::getDropdown( + 'authentication_plugin', + $active_auth_plugins, + $orig_auth_plugin, + $select_id + ); + + return $html_output; + } + + /** + * Gets the currently active authentication plugins + * + * @return array array of plugin names and descriptions + */ + public function getActiveAuthPlugins() + { + $get_plugins_query = "SELECT `PLUGIN_NAME`, `PLUGIN_DESCRIPTION`" + . " FROM `information_schema`.`PLUGINS` " + . "WHERE `PLUGIN_TYPE` = 'AUTHENTICATION';"; + $resultset = $this->dbi->query($get_plugins_query); + + $result = []; + + while ($row = $this->dbi->fetchAssoc($resultset)) { + // if description is known, enable its translation + if ('mysql_native_password' == $row['PLUGIN_NAME']) { + $row['PLUGIN_DESCRIPTION'] = __('Native MySQL authentication'); + } elseif ('sha256_password' == $row['PLUGIN_NAME']) { + $row['PLUGIN_DESCRIPTION'] = __('SHA256 password authentication'); + } + + $result[$row['PLUGIN_NAME']] = $row['PLUGIN_DESCRIPTION']; + } + + return $result; + } + + /** + * Displays the fields used by the "new user" form as well as the + * "change login information / copy user" form. + * + * @param string $mode are we creating a new user or are we just + * changing one? (allowed values: 'new', 'change') + * @param string $username User name + * @param string $hostname Host name + * + * @global array $cfg the phpMyAdmin configuration + * @global resource $user_link the database connection + * + * @return string a HTML snippet + */ + public function getHtmlForLoginInformationFields( + $mode = 'new', + $username = null, + $hostname = null + ) { + list($username_length, $hostname_length) = $this->getUsernameAndHostnameLength(); + + if (isset($GLOBALS['username']) && strlen($GLOBALS['username']) === 0) { + $GLOBALS['pred_username'] = 'any'; + } + $html_output = '
    ' . "\n" + . '' . __('Login Information') . '' . "\n" + . '
    ' . "\n" + . '' . "\n" + . '' . "\n"; + + $html_output .= '' . "\n" + . '' . "\n"; + + $html_output .= '' . "\n"; + + $html_output .= '
    ' + . Message::notice( + __( + 'An account already exists with the same username ' + . 'but possibly a different hostname.' + ) + )->getDisplay() + . '
    '; + $html_output .= '
    '; + + $html_output .= '
    ' . "\n" + . '' . "\n"; + + $html_output .= '' . "\n" + . ' ' . "\n" + . '' . "\n"; + + $html_output .= '' . "\n" + . Util::showHint( + __( + 'When Host table is used, this field is ignored ' + . 'and values stored in Host table are used instead.' + ) + ) + . '
    ' . "\n"; + + $html_output .= '
    ' . "\n" + . '' . "\n" + . '' . "\n" + . '' . "\n" + . '' . "\n" + . '' . "\n" + . 'Strength: ' + . ' ' + . '' . "\n" + . '
    ' . "\n"; + + $html_output .= '
    ' . "\n" + . '' . "\n" + . ' ' . "\n" + . '' . "\n" + . '
    ' . "\n" + . '
    ' + . ' ' . "\n"; + + $auth_plugin_dropdown = $this->getHtmlForAuthPluginsDropdown( + $orig_auth_plugin, + $mode, + 'new' + ); + } else { + $html_output .= __('Password Hashing Method') + . ' ' . "\n"; + $auth_plugin_dropdown = $this->getHtmlForAuthPluginsDropdown( + $orig_auth_plugin, + $mode, + 'old' + ); + } + $html_output .= $auth_plugin_dropdown; + + $html_output .= '' + . Message::notice( + __( + 'This method requires using an \'SSL connection\' ' + . 'or an \'unencrypted connection that encrypts the password ' + . 'using RSA\'; while connecting to the server.' + ) + . Util::showMySQLDocu('sha256-authentication-plugin') + ) + ->getDisplay() + . '
    '; + + $html_output .= '' . "\n" + // Generate password added here via jQuery + . '
    ' . "\n"; + + return $html_output; + } + + /** + * Get username and hostname length + * + * @return array username length and hostname length + */ + public function getUsernameAndHostnameLength() + { + /* Fallback values */ + $username_length = 16; + $hostname_length = 41; + + /* Try to get real lengths from the database */ + $fields_info = $this->dbi->fetchResult( + 'SELECT COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH ' + . 'FROM information_schema.columns ' + . "WHERE table_schema = 'mysql' AND table_name = 'user' " + . "AND COLUMN_NAME IN ('User', 'Host')" + ); + foreach ($fields_info as $val) { + if ($val['COLUMN_NAME'] == 'User') { + $username_length = $val['CHARACTER_MAXIMUM_LENGTH']; + } elseif ($val['COLUMN_NAME'] == 'Host') { + $hostname_length = $val['CHARACTER_MAXIMUM_LENGTH']; + } + } + return [ + $username_length, + $hostname_length, + ]; + } + + /** + * Get current authentication plugin in use - for a user or globally + * + * @param string $mode are we creating a new user or are we just + * changing one? (allowed values: 'new', 'change') + * @param string $username User name + * @param string $hostname Host name + * + * @return string authentication plugin in use + */ + public function getCurrentAuthenticationPlugin( + $mode = 'new', + $username = null, + $hostname = null + ) { + /* Fallback (standard) value */ + $authentication_plugin = 'mysql_native_password'; + $serverVersion = $this->dbi->getVersion(); + + if (isset($username) && isset($hostname) + && $mode == 'change' + ) { + $row = $this->dbi->fetchSingleRow( + 'SELECT `plugin` FROM `mysql`.`user` WHERE ' + . '`User` = "' . $username . '" AND `Host` = "' . $hostname . '" LIMIT 1' + ); + // Table 'mysql'.'user' may not exist for some previous + // versions of MySQL - in that case consider fallback value + if (is_array($row) && isset($row['plugin'])) { + $authentication_plugin = $row['plugin']; + } + } elseif ($mode == 'change') { + list($username, $hostname) = $this->dbi->getCurrentUserAndHost(); + + $row = $this->dbi->fetchSingleRow( + 'SELECT `plugin` FROM `mysql`.`user` WHERE ' + . '`User` = "' . $username . '" AND `Host` = "' . $hostname . '"' + ); + if (is_array($row) && isset($row['plugin'])) { + $authentication_plugin = $row['plugin']; + } + } elseif ($serverVersion >= 50702) { + $row = $this->dbi->fetchSingleRow( + 'SELECT @@default_authentication_plugin' + ); + $authentication_plugin = is_array($row) ? $row['@@default_authentication_plugin'] : null; + } + + return $authentication_plugin; + } + + /** + * Returns all the grants for a certain user on a certain host + * Used in the export privileges for all users section + * + * @param string $user User name + * @param string $host Host name + * + * @return string containing all the grants text + */ + public function getGrants($user, $host) + { + $grants = $this->dbi->fetchResult( + "SHOW GRANTS FOR '" + . $this->dbi->escapeString($user) . "'@'" + . $this->dbi->escapeString($host) . "'" + ); + $response = ''; + foreach ($grants as $one_grant) { + $response .= $one_grant . ";\n\n"; + } + return $response; + } + + /** + * Update password and get message for password updating + * + * @param string $err_url error url + * @param string $username username + * @param string $hostname hostname + * + * @return Message success or error message after updating password + */ + public function updatePassword($err_url, $username, $hostname) + { + // similar logic in user_password.php + $message = null; + + if (empty($_POST['nopass']) + && isset($_POST['pma_pw']) + && isset($_POST['pma_pw2']) + ) { + if ($_POST['pma_pw'] != $_POST['pma_pw2']) { + $message = Message::error(__('The passwords aren\'t the same!')); + } elseif (empty($_POST['pma_pw']) || empty($_POST['pma_pw2'])) { + $message = Message::error(__('The password is empty!')); + } + } + + // here $nopass could be == 1 + if ($message === null) { + $hashing_function = 'PASSWORD'; + $serverType = Util::getServerType(); + $serverVersion = $this->dbi->getVersion(); + $authentication_plugin + = (isset($_POST['authentication_plugin']) + ? $_POST['authentication_plugin'] + : $this->getCurrentAuthenticationPlugin( + 'change', + $username, + $hostname + )); + + // Use 'ALTER USER ...' syntax for MySQL 5.7.6+ + if ($serverType == 'MySQL' + && $serverVersion >= 50706 + ) { + if ($authentication_plugin != 'mysql_old_password') { + $query_prefix = "ALTER USER '" + . $this->dbi->escapeString($username) + . "'@'" . $this->dbi->escapeString($hostname) . "'" + . " IDENTIFIED WITH " + . $authentication_plugin + . " BY '"; + } else { + $query_prefix = "ALTER USER '" + . $this->dbi->escapeString($username) + . "'@'" . $this->dbi->escapeString($hostname) . "'" + . " IDENTIFIED BY '"; + } + + // in $sql_query which will be displayed, hide the password + $sql_query = $query_prefix . "*'"; + + $local_query = $query_prefix + . $this->dbi->escapeString($_POST['pma_pw']) . "'"; + } elseif ($serverType == 'MariaDB' && $serverVersion >= 10000) { + // MariaDB uses "SET PASSWORD" syntax to change user password. + // On Galera cluster only DDL queries are replicated, since + // users are stored in MyISAM storage engine. + $query_prefix = "SET PASSWORD FOR '" + . $this->dbi->escapeString($username) + . "'@'" . $this->dbi->escapeString($hostname) . "'" + . " = PASSWORD ('"; + $sql_query = $local_query = $query_prefix + . $this->dbi->escapeString($_POST['pma_pw']) . "')"; + } elseif ($serverType == 'MariaDB' + && $serverVersion >= 50200 + && $this->dbi->isSuperuser() + ) { + // Use 'UPDATE `mysql`.`user` ...' Syntax for MariaDB 5.2+ + if ($authentication_plugin == 'mysql_native_password') { + // Set the hashing method used by PASSWORD() + // to be 'mysql_native_password' type + $this->dbi->tryQuery('SET old_passwords = 0;'); + } elseif ($authentication_plugin == 'sha256_password') { + // Set the hashing method used by PASSWORD() + // to be 'sha256_password' type + $this->dbi->tryQuery('SET `old_passwords` = 2;'); + } + + $hashedPassword = $this->getHashedPassword($_POST['pma_pw']); + + $sql_query = 'SET PASSWORD FOR \'' + . $this->dbi->escapeString($username) + . '\'@\'' . $this->dbi->escapeString($hostname) . '\' = ' + . ($_POST['pma_pw'] == '' + ? '\'\'' + : $hashing_function . '(\'' + . preg_replace('@.@s', '*', $_POST['pma_pw']) . '\')'); + + $local_query = "UPDATE `mysql`.`user` SET " + . " `authentication_string` = '" . $hashedPassword + . "', `Password` = '', " + . " `plugin` = '" . $authentication_plugin . "'" + . " WHERE `User` = '" . $username . "' AND Host = '" + . $hostname . "';"; + } else { + // USE 'SET PASSWORD ...' syntax for rest of the versions + // Backup the old value, to be reset later + $row = $this->dbi->fetchSingleRow( + 'SELECT @@old_passwords;' + ); + $orig_value = $row['@@old_passwords']; + $update_plugin_query = "UPDATE `mysql`.`user` SET" + . " `plugin` = '" . $authentication_plugin . "'" + . " WHERE `User` = '" . $username . "' AND Host = '" + . $hostname . "';"; + + // Update the plugin for the user + if (! $this->dbi->tryQuery($update_plugin_query)) { + Util::mysqlDie( + $this->dbi->getError(), + $update_plugin_query, + false, + $err_url + ); + } + $this->dbi->tryQuery("FLUSH PRIVILEGES;"); + + if ($authentication_plugin == 'mysql_native_password') { + // Set the hashing method used by PASSWORD() + // to be 'mysql_native_password' type + $this->dbi->tryQuery('SET old_passwords = 0;'); + } elseif ($authentication_plugin == 'sha256_password') { + // Set the hashing method used by PASSWORD() + // to be 'sha256_password' type + $this->dbi->tryQuery('SET `old_passwords` = 2;'); + } + $sql_query = 'SET PASSWORD FOR \'' + . $this->dbi->escapeString($username) + . '\'@\'' . $this->dbi->escapeString($hostname) . '\' = ' + . ($_POST['pma_pw'] == '' + ? '\'\'' + : $hashing_function . '(\'' + . preg_replace('@.@s', '*', $_POST['pma_pw']) . '\')'); + + $local_query = 'SET PASSWORD FOR \'' + . $this->dbi->escapeString($username) + . '\'@\'' . $this->dbi->escapeString($hostname) . '\' = ' + . ($_POST['pma_pw'] == '' ? '\'\'' : $hashing_function + . '(\'' . $this->dbi->escapeString($_POST['pma_pw']) . '\')'); + } + + if (! $this->dbi->tryQuery($local_query)) { + Util::mysqlDie( + $this->dbi->getError(), + $sql_query, + false, + $err_url + ); + } + // Flush privileges after successful password change + $this->dbi->tryQuery("FLUSH PRIVILEGES;"); + + $message = Message::success( + __('The password for %s was changed successfully.') + ); + $message->addParam('\'' . $username . '\'@\'' . $hostname . '\''); + if (isset($orig_value)) { + $this->dbi->tryQuery( + 'SET `old_passwords` = ' . $orig_value . ';' + ); + } + } + return $message; + } + + /** + * Revokes privileges and get message and SQL query for privileges revokes + * + * @param string $dbname database name + * @param string $tablename table name + * @param string $username username + * @param string $hostname host name + * @param string $itemType item type + * + * @return array ($message, $sql_query) + */ + public function getMessageAndSqlQueryForPrivilegesRevoke( + $dbname, + $tablename, + $username, + $hostname, + $itemType + ) { + $db_and_table = $this->wildcardEscapeForGrant($dbname, $tablename); + + $sql_query0 = 'REVOKE ALL PRIVILEGES ON ' . $itemType . ' ' . $db_and_table + . ' FROM \'' + . $this->dbi->escapeString($username) . '\'@\'' + . $this->dbi->escapeString($hostname) . '\';'; + + $sql_query1 = 'REVOKE GRANT OPTION ON ' . $itemType . ' ' . $db_and_table + . ' FROM \'' . $this->dbi->escapeString($username) . '\'@\'' + . $this->dbi->escapeString($hostname) . '\';'; + + $this->dbi->query($sql_query0); + if (! $this->dbi->tryQuery($sql_query1)) { + // this one may fail, too... + $sql_query1 = ''; + } + $sql_query = $sql_query0 . ' ' . $sql_query1; + $message = Message::success( + __('You have revoked the privileges for %s.') + ); + $message->addParam('\'' . $username . '\'@\'' . $hostname . '\''); + + return [ + $message, + $sql_query, + ]; + } + + /** + * Get REQUIRE cluase + * + * @return string REQUIRE clause + */ + public function getRequireClause() + { + $arr = isset($_POST['ssl_type']) ? $_POST : $GLOBALS; + if (isset($arr['ssl_type']) && $arr['ssl_type'] == 'SPECIFIED') { + $require = []; + if (! empty($arr['ssl_cipher'])) { + $require[] = "CIPHER '" + . $this->dbi->escapeString($arr['ssl_cipher']) . "'"; + } + if (! empty($arr['x509_issuer'])) { + $require[] = "ISSUER '" + . $this->dbi->escapeString($arr['x509_issuer']) . "'"; + } + if (! empty($arr['x509_subject'])) { + $require[] = "SUBJECT '" + . $this->dbi->escapeString($arr['x509_subject']) . "'"; + } + if (count($require)) { + $require_clause = " REQUIRE " . implode(" AND ", $require); + } else { + $require_clause = " REQUIRE NONE"; + } + } elseif (isset($arr['ssl_type']) && $arr['ssl_type'] == 'X509') { + $require_clause = " REQUIRE X509"; + } elseif (isset($arr['ssl_type']) && $arr['ssl_type'] == 'ANY') { + $require_clause = " REQUIRE SSL"; + } else { + $require_clause = " REQUIRE NONE"; + } + + return $require_clause; + } + + /** + * Get a WITH clause for 'update privileges' and 'add user' + * + * @return string + */ + public function getWithClauseForAddUserAndUpdatePrivs() + { + $sql_query = ''; + if (((isset($_POST['Grant_priv']) && $_POST['Grant_priv'] == 'Y') + || (isset($GLOBALS['Grant_priv']) && $GLOBALS['Grant_priv'] == 'Y')) + && ! ((Util::getServerType() == 'MySQL' || Util::getServerType() == 'Percona Server') + && $this->dbi->getVersion() >= 80011) + ) { + $sql_query .= ' GRANT OPTION'; + } + if (isset($_POST['max_questions']) || isset($GLOBALS['max_questions'])) { + $max_questions = isset($_POST['max_questions']) + ? (int) $_POST['max_questions'] : (int) $GLOBALS['max_questions']; + $max_questions = max(0, $max_questions); + $sql_query .= ' MAX_QUERIES_PER_HOUR ' . $max_questions; + } + if (isset($_POST['max_connections']) || isset($GLOBALS['max_connections'])) { + $max_connections = isset($_POST['max_connections']) + ? (int) $_POST['max_connections'] : (int) $GLOBALS['max_connections']; + $max_connections = max(0, $max_connections); + $sql_query .= ' MAX_CONNECTIONS_PER_HOUR ' . $max_connections; + } + if (isset($_POST['max_updates']) || isset($GLOBALS['max_updates'])) { + $max_updates = isset($_POST['max_updates']) + ? (int) $_POST['max_updates'] : (int) $GLOBALS['max_updates']; + $max_updates = max(0, $max_updates); + $sql_query .= ' MAX_UPDATES_PER_HOUR ' . $max_updates; + } + if (isset($_POST['max_user_connections']) + || isset($GLOBALS['max_user_connections']) + ) { + $max_user_connections = isset($_POST['max_user_connections']) + ? (int) $_POST['max_user_connections'] + : (int) $GLOBALS['max_user_connections']; + $max_user_connections = max(0, $max_user_connections); + $sql_query .= ' MAX_USER_CONNECTIONS ' . $max_user_connections; + } + return (! empty($sql_query) ? ' WITH' . $sql_query : ''); + } + + /** + * Get HTML for addUsersForm, This function call if isset($_GET['adduser']) + * + * @param string $dbname database name + * + * @return string HTML for addUserForm + */ + public function getHtmlForAddUser($dbname) + { + $html_output = '

    ' . "\n" + . Util::getIcon('b_usradd') . __('Add user account') . "\n" + . '

    ' . "\n" + . '
    ' . "\n" + . Url::getHiddenInputs('', '') + . $this->getHtmlForLoginInformationFields('new'); + + $html_output .= '
    ' . "\n" + . '' . __('Database for user account') . '' . "\n"; + + $html_output .= $this->template->render('checkbox', [ + 'html_field_name' => 'createdb-1', + 'label' => __('Create database with same name and grant all privileges.'), + 'checked' => false, + 'onclick' => false, + 'html_field_id' => 'createdb-1', + ]); + $html_output .= '
    ' . "\n"; + $html_output .= $this->template->render('checkbox', [ + 'html_field_name' => 'createdb-2', + 'label' => __('Grant all privileges on wildcard name (username\\_%).'), + 'checked' => false, + 'onclick' => false, + 'html_field_id' => 'createdb-2', + ]); + $html_output .= '
    ' . "\n"; + + if (! empty($dbname)) { + $html_output .= $this->template->render('checkbox', [ + 'html_field_name' => 'createdb-3', + 'label' => sprintf(__('Grant all privileges on database %s.'), htmlspecialchars($dbname)), + 'checked' => true, + 'onclick' => false, + 'html_field_id' => 'createdb-3', + ]); + $html_output .= '' . "\n"; + $html_output .= '
    ' . "\n"; + } + + $html_output .= '
    ' . "\n"; + if ($GLOBALS['is_grantuser']) { + $html_output .= $this->getHtmlToDisplayPrivilegesTable('*', '*', false); + } + $html_output .= '' . "\n" + . '
    ' . "\n"; + + return $html_output; + } + + /** + * Get the list of privileges and list of compared privileges as strings + * and return a array that contains both strings + * + * @return array $list_of_privileges, $list_of_compared_privileges + */ + public function getListOfPrivilegesAndComparedPrivileges() + { + $list_of_privileges + = '`User`, ' + . '`Host`, ' + . '`Select_priv`, ' + . '`Insert_priv`, ' + . '`Update_priv`, ' + . '`Delete_priv`, ' + . '`Create_priv`, ' + . '`Drop_priv`, ' + . '`Grant_priv`, ' + . '`Index_priv`, ' + . '`Alter_priv`, ' + . '`References_priv`, ' + . '`Create_tmp_table_priv`, ' + . '`Lock_tables_priv`, ' + . '`Create_view_priv`, ' + . '`Show_view_priv`, ' + . '`Create_routine_priv`, ' + . '`Alter_routine_priv`, ' + . '`Execute_priv`'; + + $listOfComparedPrivs + = '`Select_priv` = \'N\'' + . ' AND `Insert_priv` = \'N\'' + . ' AND `Update_priv` = \'N\'' + . ' AND `Delete_priv` = \'N\'' + . ' AND `Create_priv` = \'N\'' + . ' AND `Drop_priv` = \'N\'' + . ' AND `Grant_priv` = \'N\'' + . ' AND `References_priv` = \'N\'' + . ' AND `Create_tmp_table_priv` = \'N\'' + . ' AND `Lock_tables_priv` = \'N\'' + . ' AND `Create_view_priv` = \'N\'' + . ' AND `Show_view_priv` = \'N\'' + . ' AND `Create_routine_priv` = \'N\'' + . ' AND `Alter_routine_priv` = \'N\'' + . ' AND `Execute_priv` = \'N\''; + + $list_of_privileges .= + ', `Event_priv`, ' + . '`Trigger_priv`'; + $listOfComparedPrivs .= + ' AND `Event_priv` = \'N\'' + . ' AND `Trigger_priv` = \'N\''; + return [ + $list_of_privileges, + $listOfComparedPrivs, + ]; + } + + /** + * Get the HTML for routine based privileges + * + * @param string $db database name + * @param string $index_checkbox starting index for rows to be added + * + * @return string + */ + public function getHtmlTableBodyForSpecificDbRoutinePrivs($db, $index_checkbox) + { + $sql_query = 'SELECT * FROM `mysql`.`procs_priv` WHERE Db = \'' . $this->dbi->escapeString($db) . '\';'; + $res = $this->dbi->query($sql_query); + $html_output = ''; + while ($row = $this->dbi->fetchAssoc($res)) { + $html_output .= '
    ' . htmlspecialchars($row['User']) + . '' . htmlspecialchars($row['Host']) + . 'routine' + . '' . htmlspecialchars($row['Routine_name']) . '' + . 'Yes' + . ''; + $specific_db = ''; + $specific_table = ''; + if ($GLOBALS['is_grantuser']) { + $specific_db = isset($row['Db']) && $row['Db'] != '*' + ? $row['Db'] : ''; + $specific_table = isset($row['Table_name']) + && $row['Table_name'] != '*' + ? $row['Table_name'] : ''; + $html_output .= $this->getUserLink( + 'edit', + $current_user, + $current_host, + $specific_db, + $specific_table, + $routine + ); + } + $html_output .= ''; + $html_output .= $this->getUserLink( + 'export', + $current_user, + $current_host, + $specific_db, + $specific_table, + $routine + ); + $html_output .= '
    '; + $html_output .= $this->getHtmlForPrivsTableHead(); + $privMap = $this->getPrivMap($db); + $html_output .= $this->getHtmlTableBodyForSpecificDbOrTablePrivs($privMap, $db); + $html_output .= '
    '; + $html_output .= ''; + + $html_output .= '
    '; + $html_output .= $this->template->render('select_all', [ + 'pma_theme_image' => $GLOBALS['pmaThemeImage'], + 'text_dir' => $GLOBALS['text_dir'], + 'form_name' => "usersForm", + ]); + $html_output .= Util::getButtonOrImage( + 'submit_mult', + 'mult_submit', + __('Export'), + 'b_tblexport', + 'export' + ); + + $html_output .= '
    '; + $html_output .= ''; + $html_output .= ''; + } else { + $html_output .= $this->getHtmlForViewUsersError(); + } + + $response = Response::getInstance(); + if ($response->isAjax() === true + && empty($_REQUEST['ajax_page_request']) + ) { + $message = Message::success(__('User has been added.')); + $response->addJSON('message', $message); + $response->addJSON('user_form', $html_output); + exit; + } else { + // Offer to create a new user for the current database + $html_output .= $this->getAddUserHtmlFieldset($db); + } + return $html_output; + } + + /** + * Get the HTML for user form and check the privileges for a particular table. + * + * @param string $db database name + * @param string $table table name + * + * @return string + */ + public function getHtmlForSpecificTablePrivileges($db, $table) + { + $html_output = ''; + if ($this->dbi->isSuperuser()) { + // check the privileges for a particular table. + $html_output = '
    '; + $html_output .= Url::getHiddenInputs($db, $table); + $html_output .= '
    '; + $html_output .= '' + . Util::getIcon('b_usrcheck') + . sprintf( + __('Users having access to "%s"'), + '' + . htmlspecialchars($db) . '.' . htmlspecialchars($table) + . '' + ) + . ''; + + $html_output .= '
    '; + $html_output .= ''; + $html_output .= $this->getHtmlForPrivsTableHead(); + $privMap = $this->getPrivMap($db); + $sql_query = "SELECT `User`, `Host`, `Db`," + . " 't' AS `Type`, `Table_name`, `Table_priv`" + . " FROM `mysql`.`tables_priv`" + . " WHERE '" . $this->dbi->escapeString($db) . "' LIKE `Db`" + . " AND '" . $this->dbi->escapeString($table) . "' LIKE `Table_name`" + . " AND NOT (`Table_priv` = '' AND Column_priv = '')" + . " ORDER BY `User` ASC, `Host` ASC, `Db` ASC, `Table_priv` ASC;"; + $res = $this->dbi->query($sql_query); + $this->mergePrivMapFromResult($privMap, $res); + $html_output .= $this->getHtmlTableBodyForSpecificDbOrTablePrivs($privMap, $db); + $html_output .= '
    '; + + $html_output .= '
    '; + $html_output .= $this->template->render('select_all', [ + 'pma_theme_image' => $GLOBALS['pmaThemeImage'], + 'text_dir' => $GLOBALS['text_dir'], + 'form_name' => "usersForm", + ]); + $html_output .= Util::getButtonOrImage( + 'submit_mult', + 'mult_submit', + __('Export'), + 'b_tblexport', + 'export' + ); + + $html_output .= '
    '; + $html_output .= '
    '; + } else { + $html_output .= $this->getHtmlForViewUsersError(); + } + // Offer to create a new user for the current database + $html_output .= $this->getAddUserHtmlFieldset($db, $table); + return $html_output; + } + + /** + * gets privilege map + * + * @param string $db the database + * + * @return array the privilege map + */ + public function getPrivMap($db) + { + list($listOfPrivs, $listOfComparedPrivs) + = $this->getListOfPrivilegesAndComparedPrivileges(); + $sql_query + = "(" + . " SELECT " . $listOfPrivs . ", '*' AS `Db`, 'g' AS `Type`" + . " FROM `mysql`.`user`" + . " WHERE NOT (" . $listOfComparedPrivs . ")" + . ")" + . " UNION " + . "(" + . " SELECT " . $listOfPrivs . ", `Db`, 'd' AS `Type`" + . " FROM `mysql`.`db`" + . " WHERE '" . $this->dbi->escapeString($db) . "' LIKE `Db`" + . " AND NOT (" . $listOfComparedPrivs . ")" + . ")" + . " ORDER BY `User` ASC, `Host` ASC, `Db` ASC;"; + $res = $this->dbi->query($sql_query); + $privMap = []; + $this->mergePrivMapFromResult($privMap, $res); + return $privMap; + } + + /** + * merge privilege map and rows from resultset + * + * @param array $privMap the privilege map reference + * @param object $result the resultset of query + * + * @return void + */ + public function mergePrivMapFromResult(array &$privMap, $result) + { + while ($row = $this->dbi->fetchAssoc($result)) { + $user = $row['User']; + $host = $row['Host']; + if (! isset($privMap[$user])) { + $privMap[$user] = []; + } + if (! isset($privMap[$user][$host])) { + $privMap[$user][$host] = []; + } + $privMap[$user][$host][] = $row; + } + } + + /** + * Get HTML snippet for privileges table head + * + * @return string + */ + public function getHtmlForPrivsTableHead() + { + return '' + . '' + . '' + . '' . __('User name') . '' + . '' . __('Host name') . '' + . '' . __('Type') . '' + . '' . __('Privileges') . '' + . '' . __('Grant') . '' + . '' . __('Action') . '' + . '' + . ''; + } + + /** + * Get HTML error for View Users form + * For non superusers such as grant/create users + * + * @return string + */ + public function getHtmlForViewUsersError() + { + return Message::error( + __('Not enough privilege to view users.') + )->getDisplay(); + } + + /** + * Get HTML snippet for table body of specific database or table privileges + * + * @param array $privMap privilege map + * @param string $db database + * + * @return string + */ + public function getHtmlTableBodyForSpecificDbOrTablePrivs($privMap, $db) + { + $html_output = ''; + $index_checkbox = 0; + if (empty($privMap)) { + $html_output .= '' + . '' + . __('No user found.') + . '' + . '' + . ''; + return $html_output; + } + + foreach ($privMap as $current_user => $val) { + foreach ($val as $current_host => $current_privileges) { + $nbPrivileges = count($current_privileges); + $html_output .= ''; + + $value = htmlspecialchars($current_user . '&#27;' . $current_host); + $html_output .= ' 1) { + $html_output .= ' rowspan="' . $nbPrivileges . '"'; + } + $html_output .= '>'; + $html_output .= '' . "\n"; + + // user + $html_output .= ' 1) { + $html_output .= ' rowspan="' . $nbPrivileges . '"'; + } + $html_output .= '>'; + if (empty($current_user)) { + $html_output .= '' + . __('Any') . ''; + } else { + $html_output .= htmlspecialchars($current_user); + } + $html_output .= ''; + + // host + $html_output .= ' 1) { + $html_output .= ' rowspan="' . $nbPrivileges . '"'; + } + $html_output .= '>'; + $html_output .= htmlspecialchars($current_host); + $html_output .= ''; + + $html_output .= $this->getHtmlListOfPrivs( + $db, + $current_privileges, + $current_user, + $current_host + ); + } + } + + //For fetching routine based privileges + $html_output .= $this->getHtmlTableBodyForSpecificDbRoutinePrivs($db, $index_checkbox); + $html_output .= ''; + + return $html_output; + } + + /** + * Get HTML to display privileges + * + * @param string $db Database name + * @param array $current_privileges List of privileges + * @param string $current_user Current user + * @param string $current_host Current host + * + * @return string HTML to display privileges + */ + public function getHtmlListOfPrivs( + $db, + array $current_privileges, + $current_user, + $current_host + ) { + $nbPrivileges = count($current_privileges); + $html_output = null; + for ($i = 0; $i < $nbPrivileges; $i++) { + $current = $current_privileges[$i]; + + // type + $html_output .= ''; + if ($current['Type'] == 'g') { + $html_output .= __('global'); + } elseif ($current['Type'] == 'd') { + if ($current['Db'] == Util::escapeMysqlWildcards($db)) { + $html_output .= __('database-specific'); + } else { + $html_output .= __('wildcard') . ': ' + . '' + . htmlspecialchars($current['Db']) + . ''; + } + } elseif ($current['Type'] == 't') { + $html_output .= __('table-specific'); + } + $html_output .= ''; + + // privileges + $html_output .= ''; + if (isset($current['Table_name'])) { + $privList = explode(',', $current['Table_priv']); + $privs = []; + $grantsArr = $this->getTableGrantsArray(); + foreach ($grantsArr as $grant) { + $privs[$grant[0]] = 'N'; + foreach ($privList as $priv) { + if ($grant[0] == $priv) { + $privs[$grant[0]] = 'Y'; + } + } + } + $html_output .= '' + . implode( + ',', + $this->extractPrivInfo($privs, true, true) + ) + . ''; + } else { + $html_output .= '' + . implode( + ',', + $this->extractPrivInfo($current, true, false) + ) + . ''; + } + $html_output .= ''; + + // grant + $html_output .= ''; + $containsGrant = false; + if (isset($current['Table_name'])) { + $privList = explode(',', $current['Table_priv']); + foreach ($privList as $priv) { + if ($priv == 'Grant') { + $containsGrant = true; + } + } + } else { + $containsGrant = $current['Grant_priv'] == 'Y'; + } + $html_output .= ($containsGrant ? __('Yes') : __('No')); + $html_output .= ''; + + // action + $html_output .= ''; + $specific_db = isset($current['Db']) && $current['Db'] != '*' + ? $current['Db'] : ''; + $specific_table = isset($current['Table_name']) + && $current['Table_name'] != '*' + ? $current['Table_name'] : ''; + if ($GLOBALS['is_grantuser']) { + $html_output .= $this->getUserLink( + 'edit', + $current_user, + $current_host, + $specific_db, + $specific_table + ); + } + $html_output .= ''; + $html_output .= '' + . $this->getUserLink( + 'export', + $current_user, + $current_host, + $specific_db, + $specific_table + ) + . ''; + + $html_output .= ''; + if (($i + 1) < $nbPrivileges) { + $html_output .= ''; + } + } + return $html_output; + } + + /** + * Returns edit, revoke or export link for a user. + * + * @param string $linktype The link type (edit | revoke | export) + * @param string $username User name + * @param string $hostname Host name + * @param string $dbname Database name + * @param string $tablename Table name + * @param string $routinename Routine name + * @param string $initial Initial value + * + * @return string HTML code with link + */ + public function getUserLink( + $linktype, + $username, + $hostname, + $dbname = '', + $tablename = '', + $routinename = '', + $initial = '' + ) { + $html = ' $username, + 'hostname' => $hostname, + ]; + switch ($linktype) { + case 'edit': + $params['dbname'] = $dbname; + $params['tablename'] = $tablename; + $params['routinename'] = $routinename; + break; + case 'revoke': + $params['dbname'] = $dbname; + $params['tablename'] = $tablename; + $params['routinename'] = $routinename; + $params['revokeall'] = 1; + break; + case 'export': + $params['initial'] = $initial; + $params['export'] = 1; + break; + } + + $html .= ' href="server_privileges.php'; + if ($linktype == 'revoke') { + $html .= '" data-post="' . Url::getCommon($params, ''); + } else { + $html .= Url::getCommon($params); + } + $html .= '">'; + + switch ($linktype) { + case 'edit': + $html .= Util::getIcon('b_usredit', __('Edit privileges')); + break; + case 'revoke': + $html .= Util::getIcon('b_usrdrop', __('Revoke')); + break; + case 'export': + $html .= Util::getIcon('b_tblexport', __('Export')); + break; + } + $html .= ''; + + return $html; + } + + /** + * Returns user group edit link + * + * @param string $username User name + * + * @return string HTML code with link + */ + public function getUserGroupEditLink($username) + { + return '' + . Util::getIcon('b_usrlist', __('Edit user group')) + . ''; + } + + /** + * Returns number of defined user groups + * + * @return integer + */ + public function getUserGroupCount() + { + $cfgRelation = $this->relation->getRelationsParam(); + $user_group_table = Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['usergroups']); + $sql_query = 'SELECT COUNT(*) FROM ' . $user_group_table; + $user_group_count = $this->dbi->fetchValue( + $sql_query, + 0, + 0, + DatabaseInterface::CONNECT_CONTROL + ); + + return $user_group_count; + } + + /** + * Returns name of user group that user is part of + * + * @param string $username User name + * + * @return mixed usergroup if found or null if not found + */ + public function getUserGroupForUser($username) + { + $cfgRelation = $this->relation->getRelationsParam(); + + if (empty($cfgRelation['db']) + || empty($cfgRelation['users']) + ) { + return null; + } + + $user_table = Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['users']); + $sql_query = 'SELECT `usergroup` FROM ' . $user_table + . ' WHERE `username` = \'' . $username . '\'' + . ' LIMIT 1'; + + $usergroup = $this->dbi->fetchValue( + $sql_query, + 0, + 0, + DatabaseInterface::CONNECT_CONTROL + ); + + if ($usergroup === false) { + return null; + } + + return $usergroup; + } + + /** + * This function return the extra data array for the ajax behavior + * + * @param string $password password + * @param string $sql_query sql query + * @param string $hostname hostname + * @param string $username username + * + * @return array + */ + public function getExtraDataForAjaxBehavior( + $password, + $sql_query, + $hostname, + $username + ) { + if (isset($GLOBALS['dbname'])) { + //if (preg_match('/\\\\(?:_|%)/i', $dbname)) { + if (preg_match('/(?getUserGroupCount(); + } + + $extra_data = []; + if (strlen($sql_query) > 0) { + $extra_data['sql_query'] = Util::getMessage(null, $sql_query); + } + + if (isset($_POST['change_copy'])) { + /** + * generate html on the fly for the new user that was just created. + */ + $new_user_string = '' . "\n" + . ' ' + . '' . "\n" + . '' . "\n" + . '' . htmlspecialchars($hostname) . '' . "\n"; + + $new_user_string .= ''; + + if (! empty($password) || isset($_POST['pma_pw'])) { + $new_user_string .= __('Yes'); + } else { + $new_user_string .= '' + . __('No') + . ''; + } + + $new_user_string .= '' . "\n"; + $new_user_string .= '' + . '' . implode(', ', $this->extractPrivInfo(null, true)) . '' + . ''; //Fill in privileges here + + // if $cfg['Servers'][$i]['users'] and $cfg['Servers'][$i]['usergroups'] are + // enabled + $cfgRelation = $this->relation->getRelationsParam(); + if (! empty($cfgRelation['users']) && ! empty($cfgRelation['usergroups'])) { + $new_user_string .= ''; + } + + $new_user_string .= ''; + if (isset($_POST['Grant_priv']) && $_POST['Grant_priv'] == 'Y') { + $new_user_string .= __('Yes'); + } else { + $new_user_string .= __('No'); + } + $new_user_string .= ''; + + if ($GLOBALS['is_grantuser']) { + $new_user_string .= '' + . $this->getUserLink('edit', $username, $hostname) + . '' . "\n"; + } + + if ($cfgRelation['menuswork'] && $user_group_count > 0) { + $new_user_string .= '' + . $this->getUserGroupEditLink($username) + . '' . "\n"; + } + + $new_user_string .= '' + . $this->getUserLink( + 'export', + $username, + $hostname, + '', + '', + '', + isset($_GET['initial']) ? $_GET['initial'] : '' + ) + . '' . "\n"; + + $new_user_string .= ''; + + $extra_data['new_user_string'] = $new_user_string; + + /** + * Generate the string for this alphabet's initial, to update the user + * pagination + */ + $new_user_initial = mb_strtoupper( + mb_substr($username, 0, 1) + ); + $newUserInitialString = '' + . $new_user_initial . ''; + $extra_data['new_user_initial'] = $new_user_initial; + $extra_data['new_user_initial_string'] = $newUserInitialString; + } + + if (isset($_POST['update_privs'])) { + $extra_data['db_specific_privs'] = false; + $extra_data['db_wildcard_privs'] = false; + if (isset($dbname_is_wildcard)) { + $extra_data['db_specific_privs'] = ! $dbname_is_wildcard; + $extra_data['db_wildcard_privs'] = $dbname_is_wildcard; + } + $new_privileges = implode(', ', $this->extractPrivInfo(null, true)); + + $extra_data['new_privileges'] = $new_privileges; + } + + if (isset($_GET['validate_username'])) { + $sql_query = "SELECT * FROM `mysql`.`user` WHERE `User` = '" + . $this->dbi->escapeString($_GET['username']) . "';"; + $res = $this->dbi->query($sql_query); + $row = $this->dbi->fetchRow($res); + if (empty($row)) { + $extra_data['user_exists'] = false; + } else { + $extra_data['user_exists'] = true; + } + } + + return $extra_data; + } + + /** + * Get the HTML snippet for change user login information + * + * @param string $username username + * @param string $hostname host name + * + * @return string HTML snippet + */ + public function getChangeLoginInformationHtmlForm($username, $hostname) + { + $choices = [ + '4' => __('… keep the old one.'), + '1' => __('… delete the old one from the user tables.'), + '2' => __( + '… revoke all active privileges from ' + . 'the old one and delete it afterwards.' + ), + '3' => __( + '… delete the old one from the user tables ' + . 'and reload the privileges afterwards.' + ), + ]; + + $html_output = '' . "\n"; + + return $html_output; + } + + /** + * Provide a line with links to the relevant database and table + * + * @param string $url_dbname url database name that urlencode() string + * @param string $dbname database name + * @param string $tablename table name + * + * @return string HTML snippet + */ + public function getLinkToDbAndTable($url_dbname, $dbname, $tablename) + { + $html_output = '[ ' . __('Database') + . ' ' + . htmlspecialchars(Util::unescapeMysqlWildcards($dbname)) . ': ' + . Util::getTitleForTarget( + $GLOBALS['cfg']['DefaultTabDatabase'] + ) + . " ]\n"; + + if (strlen($tablename) > 0) { + $html_output .= ' [ ' . __('Table') . ' ' . htmlspecialchars($tablename) . ': ' + . Util::getTitleForTarget( + $GLOBALS['cfg']['DefaultTabTable'] + ) + . " ]\n"; + } + return $html_output; + } + + /** + * no db name given, so we want all privs for the given user + * db name was given, so we want all user specific rights for this db + * So this function returns user rights as an array + * + * @param string $username username + * @param string $hostname host name + * @param string $type database or table + * @param string $dbname database name + * + * @return array database rights + */ + public function getUserSpecificRights($username, $hostname, $type, $dbname = '') + { + $user_host_condition = " WHERE `User`" + . " = '" . $this->dbi->escapeString($username) . "'" + . " AND `Host`" + . " = '" . $this->dbi->escapeString($hostname) . "'"; + + if ($type == 'database') { + $tables_to_search_for_users = [ + 'tables_priv', + 'columns_priv', + 'procs_priv', + ]; + $dbOrTableName = 'Db'; + } elseif ($type == 'table') { + $user_host_condition .= " AND `Db` LIKE '" + . $this->dbi->escapeString($dbname) . "'"; + $tables_to_search_for_users = ['columns_priv']; + $dbOrTableName = 'Table_name'; + } else { // routine + $user_host_condition .= " AND `Db` LIKE '" + . $this->dbi->escapeString($dbname) . "'"; + $tables_to_search_for_users = ['procs_priv']; + $dbOrTableName = 'Routine_name'; + } + + // we also want privileges for this user not in table `db` but in other table + $tables = $this->dbi->fetchResult('SHOW TABLES FROM `mysql`;'); + + $db_rights_sqls = []; + foreach ($tables_to_search_for_users as $table_search_in) { + if (in_array($table_search_in, $tables)) { + $db_rights_sqls[] = ' + SELECT DISTINCT `' . $dbOrTableName . '` + FROM `mysql`.' . Util::backquote($table_search_in) + . $user_host_condition; + } + } + + $user_defaults = [ + $dbOrTableName => '', + 'Grant_priv' => 'N', + 'privs' => ['USAGE'], + 'Column_priv' => true, + ]; + + // for the rights + $db_rights = []; + + $db_rights_sql = '(' . implode(') UNION (', $db_rights_sqls) . ')' + . ' ORDER BY `' . $dbOrTableName . '` ASC'; + + $db_rights_result = $this->dbi->query($db_rights_sql); + + while ($db_rights_row = $this->dbi->fetchAssoc($db_rights_result)) { + $db_rights_row = array_merge($user_defaults, $db_rights_row); + if ($type == 'database') { + // only Db names in the table `mysql`.`db` uses wildcards + // as we are in the db specific rights display we want + // all db names escaped, also from other sources + $db_rights_row['Db'] = Util::escapeMysqlWildcards( + $db_rights_row['Db'] + ); + } + $db_rights[$db_rights_row[$dbOrTableName]] = $db_rights_row; + } + + $this->dbi->freeResult($db_rights_result); + + if ($type == 'database') { + $sql_query = 'SELECT * FROM `mysql`.`db`' + . $user_host_condition . ' ORDER BY `Db` ASC'; + } elseif ($type == 'table') { + $sql_query = 'SELECT `Table_name`,' + . ' `Table_priv`,' + . ' IF(`Column_priv` = _latin1 \'\', 0, 1)' + . ' AS \'Column_priv\'' + . ' FROM `mysql`.`tables_priv`' + . $user_host_condition + . ' ORDER BY `Table_name` ASC;'; + } else { + $sql_query = "SELECT `Routine_name`, `Proc_priv`" + . " FROM `mysql`.`procs_priv`" + . $user_host_condition + . " ORDER BY `Routine_name`"; + } + + $result = $this->dbi->query($sql_query); + + while ($row = $this->dbi->fetchAssoc($result)) { + if (isset($db_rights[$row[$dbOrTableName]])) { + $db_rights[$row[$dbOrTableName]] + = array_merge($db_rights[$row[$dbOrTableName]], $row); + } else { + $db_rights[$row[$dbOrTableName]] = $row; + } + if ($type == 'database') { + // there are db specific rights for this user + // so we can drop this db rights + $db_rights[$row['Db']]['can_delete'] = true; + } + } + $this->dbi->freeResult($result); + return $db_rights; + } + + /** + * Parses Proc_priv data + * + * @param string $privs Proc_priv + * + * @return array + */ + public function parseProcPriv($privs) + { + $result = [ + 'Alter_routine_priv' => 'N', + 'Execute_priv' => 'N', + 'Grant_priv' => 'N', + ]; + foreach (explode(',', (string) $privs) as $priv) { + if ($priv == 'Alter Routine') { + $result['Alter_routine_priv'] = 'Y'; + } else { + $result[$priv . '_priv'] = 'Y'; + } + } + return $result; + } + + /** + * Get a HTML table for display user's tabel specific or database specific rights + * + * @param string $username username + * @param string $hostname host name + * @param string $type database, table or routine + * @param string $dbname database name + * + * @return string + */ + public function getHtmlForAllTableSpecificRights( + $username, + $hostname, + $type, + $dbname = '' + ) { + $uiData = [ + 'database' => [ + 'form_id' => 'database_specific_priv', + 'sub_menu_label' => __('Database'), + 'legend' => __('Database-specific privileges'), + 'type_label' => __('Database'), + ], + 'table' => [ + 'form_id' => 'table_specific_priv', + 'sub_menu_label' => __('Table'), + 'legend' => __('Table-specific privileges'), + 'type_label' => __('Table'), + ], + 'routine' => [ + 'form_id' => 'routine_specific_priv', + 'sub_menu_label' => __('Routine'), + 'legend' => __('Routine-specific privileges'), + 'type_label' => __('Routine'), + ], + ]; + + /** + * no db name given, so we want all privs for the given user + * db name was given, so we want all user specific rights for this db + */ + $db_rights = $this->getUserSpecificRights($username, $hostname, $type, $dbname); + ksort($db_rights); + + $foundRows = []; + $privileges = []; + foreach ($db_rights as $row) { + $onePrivilege = []; + + $paramTableName = ''; + $paramRoutineName = ''; + + if ($type == 'database') { + $name = $row['Db']; + $onePrivilege['grant'] = $row['Grant_priv'] == 'Y'; + $onePrivilege['table_privs'] = ! empty($row['Table_priv']) + || ! empty($row['Column_priv']); + $onePrivilege['privileges'] = implode(',', $this->extractPrivInfo($row, true)); + + $paramDbName = $row['Db']; + } elseif ($type == 'table') { + $name = $row['Table_name']; + $onePrivilege['grant'] = in_array( + 'Grant', + explode(',', $row['Table_priv']) + ); + $onePrivilege['column_privs'] = ! empty($row['Column_priv']); + $onePrivilege['privileges'] = implode(',', $this->extractPrivInfo($row, true)); + + $paramDbName = $dbname; + $paramTableName = $row['Table_name']; + } else { // routine + $name = $row['Routine_name']; + $onePrivilege['grant'] = in_array( + 'Grant', + explode(',', $row['Proc_priv']) + ); + + $privs = $this->parseProcPriv($row['Proc_priv']); + $onePrivilege['privileges'] = implode( + ',', + $this->extractPrivInfo($privs, true) + ); + + $paramDbName = $dbname; + $paramRoutineName = $row['Routine_name']; + } + + $foundRows[] = $name; + $onePrivilege['name'] = $name; + + $onePrivilege['edit_link'] = ''; + if ($GLOBALS['is_grantuser']) { + $onePrivilege['edit_link'] = $this->getUserLink( + 'edit', + $username, + $hostname, + $paramDbName, + $paramTableName, + $paramRoutineName + ); + } + + $onePrivilege['revoke_link'] = ''; + if ($type != 'database' || ! empty($row['can_delete'])) { + $onePrivilege['revoke_link'] = $this->getUserLink( + 'revoke', + $username, + $hostname, + $paramDbName, + $paramTableName, + $paramRoutineName + ); + } + + $privileges[] = $onePrivilege; + } + + $data = $uiData[$type]; + $data['privileges'] = $privileges; + $data['username'] = $username; + $data['hostname'] = $hostname; + $data['database'] = $dbname; + $data['type'] = $type; + + if ($type == 'database') { + // we already have the list of databases from libraries/common.inc.php + // via $pma = new PMA; + $pred_db_array = $GLOBALS['dblist']->databases; + $databases_to_skip = [ + 'information_schema', + 'performance_schema', + ]; + + $databases = []; + if (! empty($pred_db_array)) { + foreach ($pred_db_array as $current_db) { + if (in_array($current_db, $databases_to_skip)) { + continue; + } + $current_db_escaped = Util::escapeMysqlWildcards($current_db); + // cannot use array_diff() once, outside of the loop, + // because the list of databases has special characters + // already escaped in $foundRows, + // contrary to the output of SHOW DATABASES + if (! in_array($current_db_escaped, $foundRows)) { + $databases[] = $current_db; + } + } + } + $data['databases'] = $databases; + } elseif ($type == 'table') { + $result = @$this->dbi->tryQuery( + "SHOW TABLES FROM " . Util::backquote($dbname), + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + + $tables = []; + if ($result) { + while ($row = $this->dbi->fetchRow($result)) { + if (! in_array($row[0], $foundRows)) { + $tables[] = $row[0]; + } + } + $this->dbi->freeResult($result); + } + $data['tables'] = $tables; + } else { // routine + $routineData = $this->dbi->getRoutines($dbname); + + $routines = []; + foreach ($routineData as $routine) { + if (! in_array($routine['name'], $foundRows)) { + $routines[] = $routine['name']; + } + } + $data['routines'] = $routines; + } + + return $this->template->render('server/privileges/privileges_summary', $data); + } + + /** + * Get HTML for display the users overview + * (if less than 50 users, display them immediately) + * + * @param array $result ran sql query + * @param array $db_rights user's database rights array + * @param string $pmaThemeImage a image source link + * @param string $text_dir text directory + * + * @return string HTML snippet + */ + public function getUsersOverview($result, array $db_rights, $pmaThemeImage, $text_dir) + { + while ($row = $this->dbi->fetchAssoc($result)) { + $row['privs'] = $this->extractPrivInfo($row, true); + $db_rights[$row['User']][$row['Host']] = $row; + } + $this->dbi->freeResult($result); + $user_group_count = 0; + if ($GLOBALS['cfgRelation']['menuswork']) { + $user_group_count = $this->getUserGroupCount(); + } + + $html_output + = '
    ' . "\n" + . Url::getHiddenInputs('', '') + . '
    ' + . '' . "\n" + . '' . "\n" + . '' . "\n" + . '' . "\n" + . '' . "\n" + . '' . "\n" + . '' . "\n"; + if ($GLOBALS['cfgRelation']['menuswork']) { + $html_output .= '' . "\n"; + } + $html_output .= '' . "\n" + . '' . "\n" + . '' . "\n" + . '' . "\n"; + + $html_output .= '' . "\n"; + $html_output .= $this->getHtmlTableBodyForUserRights($db_rights); + $html_output .= '' + . '
    ' . __('User name') . '' . __('Host name') . '' . __('Password') . '' . __('Global privileges') . ' ' + . Util::showHint( + __('Note: MySQL privilege names are expressed in English.') + ) + . '' . __('User group') . '' . __('Grant') . '' + . __('Action') . '
    ' . "\n"; + + $html_output .= '
    ' + . $this->template->render('select_all', [ + 'pma_theme_image' => $pmaThemeImage, + 'text_dir' => $text_dir, + 'form_name' => 'usersForm', + ]) . "\n"; + $html_output .= Util::getButtonOrImage( + 'submit_mult', + 'mult_submit', + __('Export'), + 'b_tblexport', + 'export' + ); + $html_output .= ''; + $html_output .= '
    ' + . '
    '; + + // add/delete user fieldset + $html_output .= $this->getFieldsetForAddDeleteUser(); + $html_output .= '
    ' . "\n"; + + return $html_output; + } + + /** + * Get table body for 'tableuserrights' table in userform + * + * @param array $db_rights user's database rights array + * + * @return string HTML snippet + */ + public function getHtmlTableBodyForUserRights(array $db_rights) + { + $cfgRelation = $this->relation->getRelationsParam(); + $user_group_count = 0; + if ($cfgRelation['menuswork']) { + $users_table = Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['users']); + $sql_query = 'SELECT * FROM ' . $users_table; + $result = $this->relation->queryAsControlUser($sql_query, false); + $group_assignment = []; + if ($result) { + while ($row = $this->dbi->fetchAssoc($result)) { + $group_assignment[$row['username']] = $row['usergroup']; + } + } + $this->dbi->freeResult($result); + + $user_group_count = $this->getUserGroupCount(); + } + + $index_checkbox = 0; + $html_output = ''; + foreach ($db_rights as $user) { + ksort($user); + foreach ($user as $host) { + $index_checkbox++; + $html_output .= '' + . "\n"; + $html_output .= '' + . '' . "\n"; + + $html_output .= '' . "\n" + . '' . htmlspecialchars($host['Host']) . '' . "\n"; + + $html_output .= ''; + + $password_column = 'Password'; + + $check_plugin_query = "SELECT * FROM `mysql`.`user` WHERE " + . "`User` = '" . $host['User'] . "' AND `Host` = '" + . $host['Host'] . "'"; + $res = $this->dbi->fetchSingleRow($check_plugin_query); + + if ((isset($res['authentication_string']) + && ! empty($res['authentication_string'])) + || (isset($res['Password']) + && ! empty($res['Password'])) + ) { + $host[$password_column] = 'Y'; + } else { + $host[$password_column] = 'N'; + } + + switch ($host[$password_column]) { + case 'Y': + $html_output .= __('Yes'); + break; + case 'N': + $html_output .= '' . __('No') + . ''; + break; + // this happens if this is a definition not coming from mysql.user + default: + $html_output .= '--'; // in future version, replace by "not present" + break; + } // end switch + + if (! isset($host['Select_priv'])) { + $html_output .= Util::showHint( + __('The selected user was not found in the privilege table.') + ); + } + + $html_output .= '' . "\n"; + + $html_output .= '' . "\n" + . '' . implode(',' . "\n" . ' ', $host['privs']) . "\n" + . '' . "\n"; + if ($cfgRelation['menuswork']) { + $html_output .= '' . "\n" + . (isset($group_assignment[$host['User']]) + ? htmlspecialchars($group_assignment[$host['User']]) + : '' + ) + . '' . "\n"; + } + $html_output .= '' + . ($host['Grant_priv'] == 'Y' ? __('Yes') : __('No')) + . '' . "\n"; + + if ($GLOBALS['is_grantuser']) { + $html_output .= '' + . $this->getUserLink( + 'edit', + $host['User'], + $host['Host'] + ) + . ''; + } + if ($cfgRelation['menuswork'] && $user_group_count > 0) { + if (empty($host['User'])) { + $html_output .= ''; + } else { + $html_output .= '' + . $this->getUserGroupEditLink($host['User']) + . ''; + } + } + $html_output .= '' + . $this->getUserLink( + 'export', + $host['User'], + $host['Host'], + '', + '', + '', + isset($_GET['initial']) ? $_GET['initial'] : '' + ) + . ''; + $html_output .= ''; + } + } + return $html_output; + } + + /** + * Get HTML fieldset for Add/Delete user + * + * @return string HTML snippet + */ + public function getFieldsetForAddDeleteUser() + { + $html_output = $this->getAddUserHtmlFieldset(); + + $html_output .= $this->template->render('server/privileges/delete_user_fieldset'); + + return $html_output; + } + + /** + * Get HTML for Displays the initials + * + * @param array $array_initials array for all initials, even non A-Z + * + * @return string HTML snippet + */ + public function getHtmlForInitials(array $array_initials) + { + // initialize to false the letters A-Z + for ($letter_counter = 1; $letter_counter < 27; $letter_counter++) { + if (! isset($array_initials[mb_chr($letter_counter + 64)])) { + $array_initials[mb_chr($letter_counter + 64)] = false; + } + } + + $initials = $this->dbi->tryQuery( + 'SELECT DISTINCT UPPER(LEFT(`User`,1)) FROM `user`' + . ' ORDER BY UPPER(LEFT(`User`,1)) ASC', + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + if ($initials) { + while (list($tmp_initial) = $this->dbi->fetchRow($initials)) { + $array_initials[$tmp_initial] = true; + } + } + + // Display the initials, which can be any characters, not + // just letters. For letters A-Z, we add the non-used letters + // as greyed out. + + uksort($array_initials, "strnatcasecmp"); + + return $this->template->render('server/privileges/initials_row', [ + 'array_initials' => $array_initials, + 'initial' => isset($_GET['initial']) ? $_GET['initial'] : null, + ]); + } + + /** + * Get the database rights array for Display user overview + * + * @return array database rights array + */ + public function getDbRightsForUserOverview() + { + // we also want users not in table `user` but in other table + $tables = $this->dbi->fetchResult('SHOW TABLES FROM `mysql`;'); + + $tablesSearchForUsers = [ + 'user', + 'db', + 'tables_priv', + 'columns_priv', + 'procs_priv', + ]; + + $db_rights_sqls = []; + foreach ($tablesSearchForUsers as $table_search_in) { + if (in_array($table_search_in, $tables)) { + $db_rights_sqls[] = 'SELECT DISTINCT `User`, `Host` FROM `mysql`.`' + . $table_search_in . '` ' + . (isset($_GET['initial']) + ? $this->rangeOfUsers($_GET['initial']) + : ''); + } + } + $user_defaults = [ + 'User' => '', + 'Host' => '%', + 'Password' => '?', + 'Grant_priv' => 'N', + 'privs' => ['USAGE'], + ]; + + // for the rights + $db_rights = []; + + $db_rights_sql = '(' . implode(') UNION (', $db_rights_sqls) . ')' + . ' ORDER BY `User` ASC, `Host` ASC'; + + $db_rights_result = $this->dbi->query($db_rights_sql); + + while ($db_rights_row = $this->dbi->fetchAssoc($db_rights_result)) { + $db_rights_row = array_merge($user_defaults, $db_rights_row); + $db_rights[$db_rights_row['User']][$db_rights_row['Host']] + = $db_rights_row; + } + $this->dbi->freeResult($db_rights_result); + ksort($db_rights); + + return $db_rights; + } + + /** + * Delete user and get message and sql query for delete user in privileges + * + * @param array $queries queries + * + * @return array Message + */ + public function deleteUser(array $queries) + { + $sql_query = ''; + if (empty($queries)) { + $message = Message::error(__('No users selected for deleting!')); + } else { + if ($_POST['mode'] == 3) { + $queries[] = '# ' . __('Reloading the privileges') . ' …'; + $queries[] = 'FLUSH PRIVILEGES;'; + } + $drop_user_error = ''; + foreach ($queries as $sql_query) { + if ($sql_query[0] != '#') { + if (! $this->dbi->tryQuery($sql_query)) { + $drop_user_error .= $this->dbi->getError() . "\n"; + } + } + } + // tracking sets this, causing the deleted db to be shown in navi + unset($GLOBALS['db']); + + $sql_query = implode("\n", $queries); + if (! empty($drop_user_error)) { + $message = Message::rawError($drop_user_error); + } else { + $message = Message::success( + __('The selected users have been deleted successfully.') + ); + } + } + return [ + $sql_query, + $message, + ]; + } + + /** + * Update the privileges and return the success or error message + * + * @param string $username username + * @param string $hostname host name + * @param string $tablename table name + * @param string $dbname database name + * @param string $itemType item type + * + * @return array success message or error message for update + */ + public function updatePrivileges($username, $hostname, $tablename, $dbname, $itemType) + { + $db_and_table = $this->wildcardEscapeForGrant($dbname, $tablename); + + $sql_query0 = 'REVOKE ALL PRIVILEGES ON ' . $itemType . ' ' . $db_and_table + . ' FROM \'' . $this->dbi->escapeString($username) + . '\'@\'' . $this->dbi->escapeString($hostname) . '\';'; + + if (! isset($_POST['Grant_priv']) || $_POST['Grant_priv'] != 'Y') { + $sql_query1 = 'REVOKE GRANT OPTION ON ' . $itemType . ' ' . $db_and_table + . ' FROM \'' . $this->dbi->escapeString($username) . '\'@\'' + . $this->dbi->escapeString($hostname) . '\';'; + } else { + $sql_query1 = ''; + } + + // Should not do a GRANT USAGE for a table-specific privilege, it + // causes problems later (cannot revoke it) + if (! (strlen($tablename) > 0 + && 'USAGE' == implode('', $this->extractPrivInfo())) + ) { + $sql_query2 = 'GRANT ' . implode(', ', $this->extractPrivInfo()) + . ' ON ' . $itemType . ' ' . $db_and_table + . ' TO \'' . $this->dbi->escapeString($username) . '\'@\'' + . $this->dbi->escapeString($hostname) . '\''; + + if (strlen($dbname) === 0) { + // add REQUIRE clause + $sql_query2 .= $this->getRequireClause(); + } + + if ((isset($_POST['Grant_priv']) && $_POST['Grant_priv'] == 'Y') + || (strlen($dbname) === 0 + && (isset($_POST['max_questions']) || isset($_POST['max_connections']) + || isset($_POST['max_updates']) + || isset($_POST['max_user_connections']))) + ) { + $sql_query2 .= $this->getWithClauseForAddUserAndUpdatePrivs(); + } + $sql_query2 .= ';'; + } + if (! $this->dbi->tryQuery($sql_query0)) { + // This might fail when the executing user does not have + // ALL PRIVILEGES himself. + // See https://github.com/phpmyadmin/phpmyadmin/issues/9673 + $sql_query0 = ''; + } + if (! empty($sql_query1) && ! $this->dbi->tryQuery($sql_query1)) { + // this one may fail, too... + $sql_query1 = ''; + } + if (! empty($sql_query2)) { + $this->dbi->query($sql_query2); + } else { + $sql_query2 = ''; + } + $sql_query = $sql_query0 . ' ' . $sql_query1 . ' ' . $sql_query2; + $message = Message::success(__('You have updated the privileges for %s.')); + $message->addParam('\'' . $username . '\'@\'' . $hostname . '\''); + + return [ + $sql_query, + $message, + ]; + } + + /** + * Get List of information: Changes / copies a user + * + * @return array + */ + public function getDataForChangeOrCopyUser() + { + $queries = null; + $password = null; + + if (isset($_POST['change_copy'])) { + $user_host_condition = ' WHERE `User` = ' + . "'" . $this->dbi->escapeString($_POST['old_username']) . "'" + . ' AND `Host` = ' + . "'" . $this->dbi->escapeString($_POST['old_hostname']) . "';"; + $row = $this->dbi->fetchSingleRow( + 'SELECT * FROM `mysql`.`user` ' . $user_host_condition + ); + if (! $row) { + $response = Response::getInstance(); + $response->addHTML( + Message::notice(__('No user found.'))->getDisplay() + ); + unset($_POST['change_copy']); + } else { + foreach ($row as $key => $value) { + $GLOBALS[$key] = $value; + } + $serverVersion = $this->dbi->getVersion(); + // Recent MySQL versions have the field "Password" in mysql.user, + // so the previous extract creates $row['Password'] but this script + // uses $password + if (! isset($row['password']) && isset($row['Password'])) { + $row['password'] = $row['Password']; + } + if (Util::getServerType() == 'MySQL' + && $serverVersion >= 50606 + && $serverVersion < 50706 + && ((isset($row['authentication_string']) + && empty($row['password'])) + || (isset($row['plugin']) + && $row['plugin'] == 'sha256_password')) + ) { + $row['password'] = $row['authentication_string']; + } + + if (Util::getServerType() == 'MariaDB' + && $serverVersion >= 50500 + && isset($row['authentication_string']) + && empty($row['password']) + ) { + $row['password'] = $row['authentication_string']; + } + + // Always use 'authentication_string' column + // for MySQL 5.7.6+ since it does not have + // the 'password' column at all + if (in_array(Util::getServerType(), ['MySQL', 'Percona Server']) + && $serverVersion >= 50706 + && isset($row['authentication_string']) + ) { + $row['password'] = $row['authentication_string']; + } + $password = $row['password']; + $queries = []; + } + } + + return [ + $queries, + $password, + ]; + } + + /** + * Update Data for information: Deletes users + * + * @param array $queries queries array + * + * @return array + */ + public function getDataForDeleteUsers($queries) + { + if (isset($_POST['change_copy'])) { + $selected_usr = [ + $_POST['old_username'] . '&#27;' . $_POST['old_hostname'], + ]; + } else { + $selected_usr = $_POST['selected_usr']; + $queries = []; + } + + // this happens, was seen in https://reports.phpmyadmin.net/reports/view/17146 + if (! is_array($selected_usr)) { + return []; + } + + foreach ($selected_usr as $each_user) { + list($this_user, $this_host) = explode('&#27;', $each_user); + $queries[] = '# ' + . sprintf( + __('Deleting %s'), + '\'' . $this_user . '\'@\'' . $this_host . '\'' + ) + . ' ...'; + $queries[] = 'DROP USER \'' + . $this->dbi->escapeString($this_user) + . '\'@\'' . $this->dbi->escapeString($this_host) . '\';'; + $this->relationCleanup->user($this_user); + + if (isset($_POST['drop_users_db'])) { + $queries[] = 'DROP DATABASE IF EXISTS ' + . Util::backquote($this_user) . ';'; + $GLOBALS['reload'] = true; + } + } + return $queries; + } + + /** + * update Message For Reload + * + * @return Message|null + */ + public function updateMessageForReload(): ?Message + { + $message = null; + if (isset($_GET['flush_privileges'])) { + $sql_query = 'FLUSH PRIVILEGES;'; + $this->dbi->query($sql_query); + $message = Message::success( + __('The privileges were reloaded successfully.') + ); + } + + if (isset($_GET['validate_username'])) { + $message = Message::success(); + } + + return $message; + } + + /** + * update Data For Queries from queries_for_display + * + * @param array $queries queries array + * @param array|null $queries_for_display queries array for display + * + * @return array + */ + public function getDataForQueries(array $queries, $queries_for_display) + { + $tmp_count = 0; + foreach ($queries as $sql_query) { + if ($sql_query[0] != '#') { + $this->dbi->query($sql_query); + } + // when there is a query containing a hidden password, take it + // instead of the real query sent + if (isset($queries_for_display[$tmp_count])) { + $queries[$tmp_count] = $queries_for_display[$tmp_count]; + } + $tmp_count++; + } + + return $queries; + } + + /** + * update Data for information: Adds a user + * + * @param string|array|null $dbname db name + * @param string $username user name + * @param string $hostname host name + * @param string|null $password password + * @param bool $is_menuwork is_menuwork set? + * + * @return array + */ + public function addUser( + $dbname, + $username, + $hostname, + ?string $password, + $is_menuwork + ) { + $_add_user_error = false; + $message = null; + $queries = null; + $queries_for_display = null; + $sql_query = null; + + if (! isset($_POST['adduser_submit']) && ! isset($_POST['change_copy'])) { + return [ + $message, + $queries, + $queries_for_display, + $sql_query, + $_add_user_error, + ]; + } + + $sql_query = ''; + if ($_POST['pred_username'] == 'any') { + $username = ''; + } + switch ($_POST['pred_hostname']) { + case 'any': + $hostname = '%'; + break; + case 'localhost': + $hostname = 'localhost'; + break; + case 'hosttable': + $hostname = ''; + break; + case 'thishost': + $_user_name = $this->dbi->fetchValue('SELECT USER()'); + $hostname = mb_substr( + $_user_name, + mb_strrpos($_user_name, '@') + 1 + ); + unset($_user_name); + break; + } + $sql = "SELECT '1' FROM `mysql`.`user`" + . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'" + . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "';"; + if ($this->dbi->fetchValue($sql) == 1) { + $message = Message::error(__('The user %s already exists!')); + $message->addParam('[em]\'' . $username . '\'@\'' . $hostname . '\'[/em]'); + $_GET['adduser'] = true; + $_add_user_error = true; + + return [ + $message, + $queries, + $queries_for_display, + $sql_query, + $_add_user_error, + ]; + } + + list( + $create_user_real, + $create_user_show, + $real_sql_query, + $sql_query, + $password_set_real, + $password_set_show, + $alter_real_sql_query, + $alter_sql_query + ) = $this->getSqlQueriesForDisplayAndAddUser( + $username, + $hostname, + (isset($password) ? $password : '') + ); + + if (empty($_POST['change_copy'])) { + $_error = false; + + if ($create_user_real !== null) { + if (! $this->dbi->tryQuery($create_user_real)) { + $_error = true; + } + if (isset($password_set_real) && ! empty($password_set_real) + && isset($_POST['authentication_plugin']) + ) { + $this->setProperPasswordHashing( + $_POST['authentication_plugin'] + ); + if ($this->dbi->tryQuery($password_set_real)) { + $sql_query .= $password_set_show; + } + } + $sql_query = $create_user_show . $sql_query; + } + + list($sql_query, $message) = $this->addUserAndCreateDatabase( + $_error, + $real_sql_query, + $sql_query, + $username, + $hostname, + $dbname, + $alter_real_sql_query, + $alter_sql_query + ); + if (! empty($_POST['userGroup']) && $is_menuwork) { + $this->setUserGroup($GLOBALS['username'], $_POST['userGroup']); + } + + return [ + $message, + $queries, + $queries_for_display, + $sql_query, + $_add_user_error, + ]; + } + + // Copy the user group while copying a user + $old_usergroup = + isset($_POST['old_usergroup']) ? $_POST['old_usergroup'] : null; + $this->setUserGroup($_POST['username'], $old_usergroup); + + if ($create_user_real === null) { + $queries[] = $create_user_real; + } + $queries[] = $real_sql_query; + + if (isset($password_set_real) && ! empty($password_set_real) + && isset($_POST['authentication_plugin']) + ) { + $this->setProperPasswordHashing( + $_POST['authentication_plugin'] + ); + + $queries[] = $password_set_real; + } + // we put the query containing the hidden password in + // $queries_for_display, at the same position occupied + // by the real query in $queries + $tmp_count = count($queries); + if (isset($create_user_real)) { + $queries_for_display[$tmp_count - 2] = $create_user_show; + } + if (isset($password_set_real) && ! empty($password_set_real)) { + $queries_for_display[$tmp_count - 3] = $create_user_show; + $queries_for_display[$tmp_count - 2] = $sql_query; + $queries_for_display[$tmp_count - 1] = $password_set_show; + } else { + $queries_for_display[$tmp_count - 1] = $sql_query; + } + + return [ + $message, + $queries, + $queries_for_display, + $sql_query, + $_add_user_error, + ]; + } + + /** + * Sets proper value of `old_passwords` according to + * the authentication plugin selected + * + * @param string $auth_plugin authentication plugin selected + * + * @return void + */ + public function setProperPasswordHashing($auth_plugin) + { + // Set the hashing method used by PASSWORD() + // to be of type depending upon $authentication_plugin + if ($auth_plugin == 'sha256_password') { + $this->dbi->tryQuery('SET `old_passwords` = 2;'); + } elseif ($auth_plugin == 'mysql_old_password') { + $this->dbi->tryQuery('SET `old_passwords` = 1;'); + } else { + $this->dbi->tryQuery('SET `old_passwords` = 0;'); + } + } + + /** + * Update DB information: DB, Table, isWildcard + * + * @return array + */ + public function getDataForDBInfo() + { + $username = null; + $hostname = null; + $dbname = null; + $tablename = null; + $routinename = null; + $dbname_is_wildcard = null; + + if (isset($_REQUEST['username'])) { + $username = $_REQUEST['username']; + } + if (isset($_REQUEST['hostname'])) { + $hostname = $_REQUEST['hostname']; + } + /** + * Checks if a dropdown box has been used for selecting a database / table + */ + if (Core::isValid($_POST['pred_tablename'])) { + $tablename = $_POST['pred_tablename']; + } elseif (Core::isValid($_REQUEST['tablename'])) { + $tablename = $_REQUEST['tablename']; + } else { + unset($tablename); + } + + if (Core::isValid($_POST['pred_routinename'])) { + $routinename = $_POST['pred_routinename']; + } elseif (Core::isValid($_REQUEST['routinename'])) { + $routinename = $_REQUEST['routinename']; + } else { + unset($routinename); + } + + if (isset($_POST['pred_dbname'])) { + $is_valid_pred_dbname = true; + foreach ($_POST['pred_dbname'] as $key => $db_name) { + if (! Core::isValid($db_name)) { + $is_valid_pred_dbname = false; + break; + } + } + } + + if (isset($_REQUEST['dbname'])) { + $is_valid_dbname = true; + if (is_array($_REQUEST['dbname'])) { + foreach ($_REQUEST['dbname'] as $key => $db_name) { + if (! Core::isValid($db_name)) { + $is_valid_dbname = false; + break; + } + } + } else { + if (! Core::isValid($_REQUEST['dbname'])) { + $is_valid_dbname = false; + } + } + } + + if (isset($is_valid_pred_dbname) && $is_valid_pred_dbname) { + $dbname = $_POST['pred_dbname']; + // If dbname contains only one database. + if (count($dbname) === 1) { + $dbname = $dbname[0]; + } + } elseif (isset($is_valid_dbname) && $is_valid_dbname) { + $dbname = $_REQUEST['dbname']; + } else { + unset($dbname); + unset($tablename); + } + + if (isset($dbname)) { + if (is_array($dbname)) { + $db_and_table = $dbname; + foreach ($db_and_table as $key => $db_name) { + $db_and_table[$key] .= '.'; + } + } else { + $unescaped_db = Util::unescapeMysqlWildcards($dbname); + $db_and_table = Util::backquote($unescaped_db) . '.'; + } + if (isset($tablename)) { + $db_and_table .= Util::backquote($tablename); + } else { + if (is_array($db_and_table)) { + foreach ($db_and_table as $key => $db_name) { + $db_and_table[$key] .= '*'; + } + } else { + $db_and_table .= '*'; + } + } + } else { + $db_and_table = '*.*'; + } + + // check if given $dbname is a wildcard or not + if (isset($dbname)) { + //if (preg_match('/\\\\(?:_|%)/i', $dbname)) { + if (! is_array($dbname) && preg_match('/(?'; + + if (isset($_POST['selected_usr'])) { + // export privileges for selected users + $title = __('Privileges'); + + //For removing duplicate entries of users + $_POST['selected_usr'] = array_unique($_POST['selected_usr']); + + foreach ($_POST['selected_usr'] as $export_user) { + $export_username = mb_substr( + $export_user, + 0, + mb_strpos($export_user, '&') + ); + $export_hostname = mb_substr( + $export_user, + mb_strrpos($export_user, ';') + 1 + ); + $export .= '# ' + . sprintf( + __('Privileges for %s'), + '`' . htmlspecialchars($export_username) + . '`@`' . htmlspecialchars($export_hostname) . '`' + ) + . "\n\n"; + $export .= $this->getGrants($export_username, $export_hostname) . "\n"; + } + } else { + // export privileges for a single user + $title = __('User') . ' `' . htmlspecialchars($username) + . '`@`' . htmlspecialchars($hostname) . '`'; + $export .= $this->getGrants($username, $hostname); + } + // remove trailing whitespace + $export = trim($export); + + $export .= ''; + + return [ + $title, + $export, + ]; + } + + /** + * Get HTML for display Add userfieldset + * + * @param string $db the database + * @param string $table the table name + * + * @return string html output + */ + public function getAddUserHtmlFieldset($db = '', $table = '') + { + if (! $GLOBALS['is_createuser']) { + return ''; + } + $rel_params = []; + $url_params = [ + 'adduser' => 1, + ]; + if (! empty($db)) { + $url_params['dbname'] + = $rel_params['checkprivsdb'] + = $db; + } + if (! empty($table)) { + $url_params['tablename'] + = $rel_params['checkprivstable'] + = $table; + } + + return $this->template->render('server/privileges/add_user_fieldset', [ + 'url_params' => $url_params, + 'rel_params' => $rel_params, + ]); + } + + /** + * Get HTML header for display User's properties + * + * @param boolean $dbname_is_wildcard whether database name is wildcard or not + * @param string $url_dbname url database name that urlencode() string + * @param string $dbname database name + * @param string $username username + * @param string $hostname host name + * @param string $entity_name entity (table or routine) name + * @param string $entity_type optional, type of entity ('table' or 'routine') + * + * @return string + */ + public function getHtmlHeaderForUserProperties( + $dbname_is_wildcard, + $url_dbname, + $dbname, + $username, + $hostname, + $entity_name, + $entity_type = 'table' + ) { + $html_output = '

    ' . "\n" + . Util::getIcon('b_usredit') + . __('Edit privileges:') . ' ' + . __('User account'); + + if (! empty($dbname)) { + $html_output .= ' \'' . htmlspecialchars($username) + . '\'@\'' . htmlspecialchars($hostname) + . '\'' . "\n"; + + $html_output .= ' - '; + $html_output .= $dbname_is_wildcard + || is_array($dbname) && count($dbname) > 1 + ? __('Databases') : __('Database'); + if (! empty($entity_name) && $entity_type === 'table') { + $html_output .= ' ' . htmlspecialchars($dbname) + . ''; + + $html_output .= ' - ' . __('Table') + . ' ' . htmlspecialchars($entity_name) . ''; + } elseif (! empty($entity_name)) { + $html_output .= ' ' . htmlspecialchars($dbname) + . ''; + + $html_output .= ' - ' . __('Routine') + . ' ' . htmlspecialchars($entity_name) . ''; + } else { + if (! is_array($dbname)) { + $dbname = [$dbname]; + } + $html_output .= ' ' + . htmlspecialchars(implode(', ', $dbname)) + . ''; + } + } else { + $html_output .= ' \'' . htmlspecialchars($username) + . '\'@\'' . htmlspecialchars($hostname) + . '\'' . "\n"; + } + $html_output .= '

    ' . "\n"; + $cur_user = $this->dbi->getCurrentUser(); + $user = $username . '@' . $hostname; + // Add a short notice for the user + // to remind him that he is editing his own privileges + if ($user === $cur_user) { + $html_output .= Message::notice( + __( + 'Note: You are attempting to edit privileges of the ' + . 'user with which you are currently logged in.' + ) + )->getDisplay(); + } + return $html_output; + } + + /** + * Get HTML snippet for display user overview page + * + * @param string $pmaThemeImage a image source link + * @param string $text_dir text directory + * + * @return string + */ + public function getHtmlForUserOverview($pmaThemeImage, $text_dir) + { + $html_output = '

    ' . "\n" + . Util::getIcon('b_usrlist') + . __('User accounts overview') . "\n" + . '

    ' . "\n"; + + $password_column = 'Password'; + $server_type = Util::getServerType(); + $serverVersion = $this->dbi->getVersion(); + if (($server_type == 'MySQL' || $server_type == 'Percona Server') + && $serverVersion >= 50706 + ) { + $password_column = 'authentication_string'; + } + // $sql_query is for the initial-filtered, + // $sql_query_all is for counting the total no. of users + + $sql_query = $sql_query_all = 'SELECT *,' . + " IF(`" . $password_column . "` = _latin1 '', 'N', 'Y') AS 'Password'" . + ' FROM `mysql`.`user`'; + + $sql_query .= (isset($_GET['initial']) + ? $this->rangeOfUsers($_GET['initial']) + : ''); + + $sql_query .= ' ORDER BY `User` ASC, `Host` ASC;'; + $sql_query_all .= ' ;'; + + $res = $this->dbi->tryQuery( + $sql_query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + $res_all = $this->dbi->tryQuery( + $sql_query_all, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + + if (! $res) { + // the query failed! This may have two reasons: + // - the user does not have enough privileges + // - the privilege tables use a structure of an earlier version. + // so let's try a more simple query + + $this->dbi->freeResult($res); + $this->dbi->freeResult($res_all); + $sql_query = 'SELECT * FROM `mysql`.`user`'; + $res = $this->dbi->tryQuery( + $sql_query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + + if (! $res) { + $html_output .= $this->getHtmlForViewUsersError(); + $html_output .= $this->getAddUserHtmlFieldset(); + } else { + // This message is hardcoded because I will replace it by + // a automatic repair feature soon. + $raw = 'Your privilege table structure seems to be older than' + . ' this MySQL version!
    ' + . 'Please run the mysql_upgrade command' + . ' that should be included in your MySQL server distribution' + . ' to solve this problem!'; + $html_output .= Message::rawError($raw)->getDisplay(); + } + $this->dbi->freeResult($res); + } else { + $db_rights = $this->getDbRightsForUserOverview(); + // for all initials, even non A-Z + $array_initials = []; + + foreach ($db_rights as $right) { + foreach ($right as $account) { + if (empty($account['User']) && $account['Host'] == 'localhost') { + $html_output .= Message::notice( + __( + 'A user account allowing any user from localhost to ' + . 'connect is present. This will prevent other users ' + . 'from connecting if the host part of their account ' + . 'allows a connection from any (%) host.' + ) + . Util::showMySQLDocu('problems-connecting') + )->getDisplay(); + break 2; + } + } + } + + /** + * Displays the initials + * Also not necessary if there is less than 20 privileges + */ + if ($this->dbi->numRows($res_all) > 20) { + $html_output .= $this->getHtmlForInitials($array_initials); + } + + /** + * Display the user overview + * (if less than 50 users, display them immediately) + */ + if (isset($_GET['initial']) + || isset($_GET['showall']) + || $this->dbi->numRows($res) < 50 + ) { + $html_output .= $this->getUsersOverview( + $res, + $db_rights, + $pmaThemeImage, + $text_dir + ); + } else { + $html_output .= $this->getAddUserHtmlFieldset(); + } // end if (display overview) + + $response = Response::getInstance(); + if (! $response->isAjax() + || ! empty($_REQUEST['ajax_page_request']) + ) { + if ($GLOBALS['is_reload_priv']) { + $flushnote = new Message( + __( + 'Note: phpMyAdmin gets the users’ privileges directly ' + . 'from MySQL’s privilege tables. The content of these ' + . 'tables may differ from the privileges the server uses, ' + . 'if they have been changed manually. In this case, ' + . 'you should %sreload the privileges%s before you continue.' + ), + Message::NOTICE + ); + $flushnote->addParamHtml( + '' + ); + $flushnote->addParamHtml(''); + } else { + $flushnote = new Message( + __( + 'Note: phpMyAdmin gets the users’ privileges directly ' + . 'from MySQL’s privilege tables. The content of these ' + . 'tables may differ from the privileges the server uses, ' + . 'if they have been changed manually. In this case, ' + . 'the privileges have to be reloaded but currently, you ' + . 'don\'t have the RELOAD privilege.' + ) + . Util::showMySQLDocu( + 'privileges-provided', + false, + null, + null, + 'priv_reload' + ), + Message::NOTICE + ); + } + $html_output .= $flushnote->getDisplay(); + } + } + + return $html_output; + } + + /** + * Get HTML snippet for display user properties + * + * @param boolean $dbname_is_wildcard whether database name is wildcard or not + * @param string $url_dbname url database name that urlencode() string + * @param string $username username + * @param string $hostname host name + * @param string $dbname database name + * @param string $tablename table name + * + * @return string + */ + public function getHtmlForUserProperties( + $dbname_is_wildcard, + $url_dbname, + $username, + $hostname, + $dbname, + $tablename + ) { + $html_output = '
    '; + $html_output .= $this->getHtmlHeaderForUserProperties( + $dbname_is_wildcard, + $url_dbname, + $dbname, + $username, + $hostname, + $tablename, + 'table' + ); + + $sql = "SELECT '1' FROM `mysql`.`user`" + . " WHERE `User` = '" . $this->dbi->escapeString($username) . "'" + . " AND `Host` = '" . $this->dbi->escapeString($hostname) . "';"; + + $user_does_not_exists = (bool) ! $this->dbi->fetchValue($sql); + + if ($user_does_not_exists) { + $html_output .= Message::error( + __('The selected user was not found in the privilege table.') + )->getDisplay(); + $html_output .= $this->getHtmlForLoginInformationFields(); + } + + $_params = [ + 'username' => $username, + 'hostname' => $hostname, + ]; + if (! is_array($dbname) && strlen($dbname) > 0) { + $_params['dbname'] = $dbname; + if (strlen($tablename) > 0) { + $_params['tablename'] = $tablename; + } + } else { + $_params['dbname'] = $dbname; + } + + $html_output .= '' . "\n"; + + if (! is_array($dbname) && strlen($tablename) === 0 + && empty($dbname_is_wildcard) + ) { + // no table name was given, display all table specific rights + // but only if $dbname contains no wildcards + if (strlen($dbname) === 0) { + $html_output .= $this->getHtmlForAllTableSpecificRights( + $username, + $hostname, + 'database' + ); + } else { + // unescape wildcards in dbname at table level + $unescaped_db = Util::unescapeMysqlWildcards($dbname); + + $html_output .= $this->getHtmlForAllTableSpecificRights( + $username, + $hostname, + 'table', + $unescaped_db + ); + $html_output .= $this->getHtmlForAllTableSpecificRights( + $username, + $hostname, + 'routine', + $unescaped_db + ); + } + } + + // Provide a line with links to the relevant database and table + if (! is_array($dbname) && strlen($dbname) > 0 && empty($dbname_is_wildcard)) { + $html_output .= $this->getLinkToDbAndTable($url_dbname, $dbname, $tablename); + } + + if (! is_array($dbname) && strlen($dbname) === 0 && ! $user_does_not_exists) { + //change login information + $html_output .= ChangePassword::getHtml( + 'edit_other', + $username, + $hostname + ); + $html_output .= $this->getChangeLoginInformationHtmlForm($username, $hostname); + } + $html_output .= '
    '; + + return $html_output; + } + + /** + * Get queries for Table privileges to change or copy user + * + * @param string $user_host_condition user host condition to + * select relevant table privileges + * @param array $queries queries array + * @param string $username username + * @param string $hostname host name + * + * @return array + */ + public function getTablePrivsQueriesForChangeOrCopyUser( + $user_host_condition, + array $queries, + $username, + $hostname + ) { + $res = $this->dbi->query( + 'SELECT `Db`, `Table_name`, `Table_priv` FROM `mysql`.`tables_priv`' + . $user_host_condition, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + while ($row = $this->dbi->fetchAssoc($res)) { + $res2 = $this->dbi->query( + 'SELECT `Column_name`, `Column_priv`' + . ' FROM `mysql`.`columns_priv`' + . ' WHERE `User`' + . ' = \'' . $this->dbi->escapeString($_POST['old_username']) . "'" + . ' AND `Host`' + . ' = \'' . $this->dbi->escapeString($_POST['old_username']) . '\'' + . ' AND `Db`' + . ' = \'' . $this->dbi->escapeString($row['Db']) . "'" + . ' AND `Table_name`' + . ' = \'' . $this->dbi->escapeString($row['Table_name']) . "'" + . ';', + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + + $tmp_privs1 = $this->extractPrivInfo($row); + $tmp_privs2 = [ + 'Select' => [], + 'Insert' => [], + 'Update' => [], + 'References' => [], + ]; + + while ($row2 = $this->dbi->fetchAssoc($res2)) { + $tmp_array = explode(',', $row2['Column_priv']); + if (in_array('Select', $tmp_array)) { + $tmp_privs2['Select'][] = $row2['Column_name']; + } + if (in_array('Insert', $tmp_array)) { + $tmp_privs2['Insert'][] = $row2['Column_name']; + } + if (in_array('Update', $tmp_array)) { + $tmp_privs2['Update'][] = $row2['Column_name']; + } + if (in_array('References', $tmp_array)) { + $tmp_privs2['References'][] = $row2['Column_name']; + } + } + if (count($tmp_privs2['Select']) > 0 && ! in_array('SELECT', $tmp_privs1)) { + $tmp_privs1[] = 'SELECT (`' . implode('`, `', $tmp_privs2['Select']) . '`)'; + } + if (count($tmp_privs2['Insert']) > 0 && ! in_array('INSERT', $tmp_privs1)) { + $tmp_privs1[] = 'INSERT (`' . implode('`, `', $tmp_privs2['Insert']) . '`)'; + } + if (count($tmp_privs2['Update']) > 0 && ! in_array('UPDATE', $tmp_privs1)) { + $tmp_privs1[] = 'UPDATE (`' . implode('`, `', $tmp_privs2['Update']) . '`)'; + } + if (count($tmp_privs2['References']) > 0 + && ! in_array('REFERENCES', $tmp_privs1) + ) { + $tmp_privs1[] + = 'REFERENCES (`' . implode('`, `', $tmp_privs2['References']) . '`)'; + } + + $queries[] = 'GRANT ' . implode(', ', $tmp_privs1) + . ' ON ' . Util::backquote($row['Db']) . '.' + . Util::backquote($row['Table_name']) + . ' TO \'' . $this->dbi->escapeString($username) + . '\'@\'' . $this->dbi->escapeString($hostname) . '\'' + . (in_array('Grant', explode(',', $row['Table_priv'])) + ? ' WITH GRANT OPTION;' + : ';'); + } + return $queries; + } + + /** + * Get queries for database specific privileges for change or copy user + * + * @param array $queries queries array with string + * @param string $username username + * @param string $hostname host name + * + * @return array + */ + public function getDbSpecificPrivsQueriesForChangeOrCopyUser( + array $queries, + $username, + $hostname + ) { + $user_host_condition = ' WHERE `User`' + . ' = \'' . $this->dbi->escapeString($_POST['old_username']) . "'" + . ' AND `Host`' + . ' = \'' . $this->dbi->escapeString($_POST['old_hostname']) . '\';'; + + $res = $this->dbi->query( + 'SELECT * FROM `mysql`.`db`' . $user_host_condition + ); + + while ($row = $this->dbi->fetchAssoc($res)) { + $queries[] = 'GRANT ' . implode(', ', $this->extractPrivInfo($row)) + . ' ON ' . Util::backquote($row['Db']) . '.*' + . ' TO \'' . $this->dbi->escapeString($username) + . '\'@\'' . $this->dbi->escapeString($hostname) . '\'' + . ($row['Grant_priv'] == 'Y' ? ' WITH GRANT OPTION;' : ';'); + } + $this->dbi->freeResult($res); + + $queries = $this->getTablePrivsQueriesForChangeOrCopyUser( + $user_host_condition, + $queries, + $username, + $hostname + ); + + return $queries; + } + + /** + * Prepares queries for adding users and + * also create database and return query and message + * + * @param boolean $_error whether user create or not + * @param string $real_sql_query SQL query for add a user + * @param string $sql_query SQL query to be displayed + * @param string $username username + * @param string $hostname host name + * @param string $dbname database name + * @param string $alter_real_sql_query SQL query for ALTER USER + * @param string $alter_sql_query SQL query for ALTER USER to be displayed + * + * @return array, $message + */ + public function addUserAndCreateDatabase( + $_error, + $real_sql_query, + $sql_query, + $username, + $hostname, + $dbname, + $alter_real_sql_query, + $alter_sql_query + ) { + if ($_error || (! empty($real_sql_query) + && ! $this->dbi->tryQuery($real_sql_query)) + ) { + $_POST['createdb-1'] = $_POST['createdb-2'] + = $_POST['createdb-3'] = null; + $message = Message::rawError($this->dbi->getError()); + } elseif ($alter_real_sql_query !== '' && ! $this->dbi->tryQuery($alter_real_sql_query)) { + $_POST['createdb-1'] = $_POST['createdb-2'] + = $_POST['createdb-3'] = null; + $message = Message::rawError($this->dbi->getError()); + } else { + $sql_query .= $alter_sql_query; + $message = Message::success(__('You have added a new user.')); + } + + if (isset($_POST['createdb-1'])) { + // Create database with same name and grant all privileges + $q = 'CREATE DATABASE IF NOT EXISTS ' + . Util::backquote( + $this->dbi->escapeString($username) + ) . ';'; + $sql_query .= $q; + if (! $this->dbi->tryQuery($q)) { + $message = Message::rawError($this->dbi->getError()); + } + + /** + * Reload the navigation + */ + $GLOBALS['reload'] = true; + $GLOBALS['db'] = $username; + + $q = 'GRANT ALL PRIVILEGES ON ' + . Util::backquote( + Util::escapeMysqlWildcards( + $this->dbi->escapeString($username) + ) + ) . '.* TO \'' + . $this->dbi->escapeString($username) + . '\'@\'' . $this->dbi->escapeString($hostname) . '\';'; + $sql_query .= $q; + if (! $this->dbi->tryQuery($q)) { + $message = Message::rawError($this->dbi->getError()); + } + } + + if (isset($_POST['createdb-2'])) { + // Grant all privileges on wildcard name (username\_%) + $q = 'GRANT ALL PRIVILEGES ON ' + . Util::backquote( + Util::escapeMysqlWildcards( + $this->dbi->escapeString($username) + ) . '\_%' + ) . '.* TO \'' + . $this->dbi->escapeString($username) + . '\'@\'' . $this->dbi->escapeString($hostname) . '\';'; + $sql_query .= $q; + if (! $this->dbi->tryQuery($q)) { + $message = Message::rawError($this->dbi->getError()); + } + } + + if (isset($_POST['createdb-3'])) { + // Grant all privileges on the specified database to the new user + $q = 'GRANT ALL PRIVILEGES ON ' + . Util::backquote( + $this->dbi->escapeString($dbname) + ) . '.* TO \'' + . $this->dbi->escapeString($username) + . '\'@\'' . $this->dbi->escapeString($hostname) . '\';'; + $sql_query .= $q; + if (! $this->dbi->tryQuery($q)) { + $message = Message::rawError($this->dbi->getError()); + } + } + return [ + $sql_query, + $message, + ]; + } + + /** + * Get the hashed string for password + * + * @param string $password password + * + * @return string + */ + public function getHashedPassword($password) + { + $password = $this->dbi->escapeString($password); + $result = $this->dbi->fetchSingleRow( + "SELECT PASSWORD('" . $password . "') AS `password`;" + ); + + return $result['password']; + } + + /** + * Check if MariaDB's 'simple_password_check' + * OR 'cracklib_password_check' is ACTIVE + * + * @return boolean if atleast one of the plugins is ACTIVE + */ + public function checkIfMariaDBPwdCheckPluginActive() + { + $serverVersion = $this->dbi->getVersion(); + if (! (Util::getServerType() == 'MariaDB' && $serverVersion >= 100002)) { + return false; + } + + $result = $this->dbi->tryQuery( + 'SHOW PLUGINS SONAME LIKE \'%_password_check%\'' + ); + + /* Plugins are not working, for example directory does not exists */ + if ($result === false) { + return false; + } + + while ($row = $this->dbi->fetchAssoc($result)) { + if ($row['Status'] === 'ACTIVE') { + return true; + } + } + + return false; + } + + + /** + * Get SQL queries for Display and Add user + * + * @param string $username username + * @param string $hostname host name + * @param string $password password + * + * @return array ($create_user_real, $create_user_show, $real_sql_query, $sql_query + * $password_set_real, $password_set_show, $alter_real_sql_query, $alter_sql_query) + */ + public function getSqlQueriesForDisplayAndAddUser($username, $hostname, $password) + { + $slashedUsername = $this->dbi->escapeString($username); + $slashedHostname = $this->dbi->escapeString($hostname); + $slashedPassword = $this->dbi->escapeString($password); + $serverType = Util::getServerType(); + $serverVersion = $this->dbi->getVersion(); + + $create_user_stmt = sprintf( + 'CREATE USER \'%s\'@\'%s\'', + $slashedUsername, + $slashedHostname + ); + $isMariaDBPwdPluginActive = $this->checkIfMariaDBPwdCheckPluginActive(); + + // See https://github.com/phpmyadmin/phpmyadmin/pull/11560#issuecomment-147158219 + // for details regarding details of syntax usage for various versions + + // 'IDENTIFIED WITH auth_plugin' + // is supported by MySQL 5.5.7+ + if (($serverType == 'MySQL' || $serverType == 'Percona Server') + && $serverVersion >= 50507 + && isset($_POST['authentication_plugin']) + ) { + $create_user_stmt .= ' IDENTIFIED WITH ' + . $_POST['authentication_plugin']; + } + + // 'IDENTIFIED VIA auth_plugin' + // is supported by MariaDB 5.2+ + if ($serverType == 'MariaDB' + && $serverVersion >= 50200 + && isset($_POST['authentication_plugin']) + && ! $isMariaDBPwdPluginActive + ) { + $create_user_stmt .= ' IDENTIFIED VIA ' + . $_POST['authentication_plugin']; + } + + $create_user_real = $create_user_stmt; + $create_user_show = $create_user_stmt; + + $password_set_stmt = 'SET PASSWORD FOR \'%s\'@\'%s\' = \'%s\''; + $password_set_show = sprintf( + $password_set_stmt, + $slashedUsername, + $slashedHostname, + '***' + ); + + $sql_query_stmt = sprintf( + 'GRANT %s ON *.* TO \'%s\'@\'%s\'', + implode(', ', $this->extractPrivInfo()), + $slashedUsername, + $slashedHostname + ); + $real_sql_query = $sql_query = $sql_query_stmt; + + // Set the proper hashing method + if (isset($_POST['authentication_plugin'])) { + $this->setProperPasswordHashing( + $_POST['authentication_plugin'] + ); + } + + // Use 'CREATE USER ... WITH ... AS ..' syntax for + // newer MySQL versions + // and 'CREATE USER ... VIA .. USING ..' syntax for + // newer MariaDB versions + if ((($serverType == 'MySQL' || $serverType == 'Percona Server') + && $serverVersion >= 50706) + || ($serverType == 'MariaDB' + && $serverVersion >= 50200) + ) { + $password_set_real = null; + + // Required for binding '%' with '%s' + $create_user_stmt = str_replace( + '%', + '%%', + $create_user_stmt + ); + + // MariaDB uses 'USING' whereas MySQL uses 'AS' + // but MariaDB with validation plugin needs cleartext password + if ($serverType == 'MariaDB' + && ! $isMariaDBPwdPluginActive + ) { + $create_user_stmt .= ' USING \'%s\''; + } elseif ($serverType == 'MariaDB') { + $create_user_stmt .= ' IDENTIFIED BY \'%s\''; + } elseif (($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011) { + $create_user_stmt .= ' BY \'%s\''; + } else { + $create_user_stmt .= ' AS \'%s\''; + } + + if ($_POST['pred_password'] == 'keep') { + $create_user_real = sprintf( + $create_user_stmt, + $slashedPassword + ); + $create_user_show = sprintf( + $create_user_stmt, + '***' + ); + } elseif ($_POST['pred_password'] == 'none') { + $create_user_real = sprintf( + $create_user_stmt, + null + ); + $create_user_show = sprintf( + $create_user_stmt, + '***' + ); + } else { + if (! (($serverType == 'MariaDB' && $isMariaDBPwdPluginActive) + || ($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011)) { + $hashedPassword = $this->getHashedPassword($_POST['pma_pw']); + } else { + // MariaDB with validation plugin needs cleartext password + $hashedPassword = $_POST['pma_pw']; + } + $create_user_real = sprintf( + $create_user_stmt, + $hashedPassword + ); + $create_user_show = sprintf( + $create_user_stmt, + '***' + ); + } + } else { + // Use 'SET PASSWORD' syntax for pre-5.7.6 MySQL versions + // and pre-5.2.0 MariaDB versions + if ($_POST['pred_password'] == 'keep') { + $password_set_real = sprintf( + $password_set_stmt, + $slashedUsername, + $slashedHostname, + $slashedPassword + ); + } elseif ($_POST['pred_password'] == 'none') { + $password_set_real = sprintf( + $password_set_stmt, + $slashedUsername, + $slashedHostname, + null + ); + } else { + $hashedPassword = $this->getHashedPassword($_POST['pma_pw']); + $password_set_real = sprintf( + $password_set_stmt, + $slashedUsername, + $slashedHostname, + $hashedPassword + ); + } + } + + $alter_real_sql_query = ''; + $alter_sql_query = ''; + if (($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011) { + $sql_query_stmt = ''; + if ((isset($_POST['Grant_priv']) && $_POST['Grant_priv'] == 'Y') + || (isset($GLOBALS['Grant_priv']) && $GLOBALS['Grant_priv'] == 'Y') + ) { + $sql_query_stmt = ' WITH GRANT OPTION'; + } + $real_sql_query .= $sql_query_stmt; + $sql_query .= $sql_query_stmt; + + $alter_sql_query_stmt = sprintf( + 'ALTER USER \'%s\'@\'%s\'', + $slashedUsername, + $slashedHostname + ); + $alter_real_sql_query = $alter_sql_query_stmt; + $alter_sql_query = $alter_sql_query_stmt; + } + + // add REQUIRE clause + $require_clause = $this->getRequireClause(); + $with_clause = $this->getWithClauseForAddUserAndUpdatePrivs(); + + if (($serverType == 'MySQL' || $serverType == 'Percona Server') && $serverVersion >= 80011) { + $alter_real_sql_query .= $require_clause; + $alter_sql_query .= $require_clause; + $alter_real_sql_query .= $with_clause; + $alter_sql_query .= $with_clause; + } else { + $real_sql_query .= $require_clause; + $sql_query .= $require_clause; + $real_sql_query .= $with_clause; + $sql_query .= $with_clause; + } + + if ($alter_real_sql_query !== '') { + $alter_real_sql_query .= ';'; + $alter_sql_query .= ';'; + } + $create_user_real .= ';'; + $create_user_show .= ';'; + $real_sql_query .= ';'; + $sql_query .= ';'; + // No Global GRANT_OPTION privilege + if (! $GLOBALS['is_grantuser']) { + $real_sql_query = ''; + $sql_query = ''; + } + + // Use 'SET PASSWORD' for pre-5.7.6 MySQL versions + // and pre-5.2.0 MariaDB + if (($serverType == 'MySQL' + && $serverVersion >= 50706) + || ($serverType == 'MariaDB' + && $serverVersion >= 50200) + ) { + $password_set_real = null; + $password_set_show = null; + } else { + if ($password_set_real !== null) { + $password_set_real .= ";"; + } + $password_set_show .= ";"; + } + + return [ + $create_user_real, + $create_user_show, + $real_sql_query, + $sql_query, + $password_set_real, + $password_set_show, + $alter_real_sql_query, + $alter_sql_query, + ]; + } + + /** + * Returns the type ('PROCEDURE' or 'FUNCTION') of the routine + * + * @param string $dbname database + * @param string $routineName routine + * + * @return string type + */ + public function getRoutineType($dbname, $routineName) + { + $routineData = $this->dbi->getRoutines($dbname); + + foreach ($routineData as $routine) { + if ($routine['name'] === $routineName) { + return $routine['type']; + } + } + return ''; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Server/Select.php b/srcs/phpmyadmin/libraries/classes/Server/Select.php new file mode 100644 index 0000000..bfc1f19 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Server/Select.php @@ -0,0 +1,128 @@ +'; + + if (! $omit_fieldset) { + $retval .= '
    '; + } + + $retval .= Url::getHiddenFields([]); + $retval .= ' '; + + $retval .= ''; + if (! $omit_fieldset) { + $retval .= '
    '; + } + $retval .= ''; + } elseif ($list) { + $retval .= ''; + } + + return $retval; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Server/Status/Data.php b/srcs/phpmyadmin/libraries/classes/Server/Status/Data.php new file mode 100644 index 0000000..7d352f7 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Server/Status/Data.php @@ -0,0 +1,430 @@ + section + // variable names match when they begin with the given string + + 'Com_' => 'com', + 'Innodb_' => 'innodb', + 'Ndb_' => 'ndb', + 'Handler_' => 'handler', + 'Qcache_' => 'qcache', + 'Threads_' => 'threads', + 'Slow_launch_threads' => 'threads', + + 'Binlog_cache_' => 'binlog_cache', + 'Created_tmp_' => 'created_tmp', + 'Key_' => 'key', + + 'Delayed_' => 'delayed', + 'Not_flushed_delayed_rows' => 'delayed', + + 'Flush_commands' => 'query', + 'Last_query_cost' => 'query', + 'Slow_queries' => 'query', + 'Queries' => 'query', + 'Prepared_stmt_count' => 'query', + + 'Select_' => 'select', + 'Sort_' => 'sort', + + 'Open_tables' => 'table', + 'Opened_tables' => 'table', + 'Open_table_definitions' => 'table', + 'Opened_table_definitions' => 'table', + 'Table_locks_' => 'table', + + 'Rpl_status' => 'repl', + 'Slave_' => 'repl', + + 'Tc_' => 'tc', + + 'Ssl_' => 'ssl', + + 'Open_files' => 'files', + 'Open_streams' => 'files', + 'Opened_files' => 'files', + ]; + } + + /** + * Gets the sections for constructor + * + * @return array + */ + private function _getSections() + { + return [ + // section => section name (description) + 'com' => 'Com', + 'query' => __('SQL query'), + 'innodb' => 'InnoDB', + 'ndb' => 'NDB', + 'handler' => __('Handler'), + 'qcache' => __('Query cache'), + 'threads' => __('Threads'), + 'binlog_cache' => __('Binary log'), + 'created_tmp' => __('Temporary data'), + 'delayed' => __('Delayed inserts'), + 'key' => __('Key cache'), + 'select' => __('Joins'), + 'repl' => __('Replication'), + 'sort' => __('Sorting'), + 'table' => __('Tables'), + 'tc' => __('Transaction coordinator'), + 'files' => __('Files'), + 'ssl' => 'SSL', + 'other' => __('Other'), + ]; + } + + /** + * Gets the links for constructor + * + * @return array + */ + private function _getLinks() + { + $links = []; + // variable or section name => (name => url) + + $links['table'][__('Flush (close) all tables')] = [ + 'url' => $this->selfUrl, + 'params' => Url::getCommon(['flush' => 'TABLES'], ''), + ]; + $links['table'][__('Show open tables')] = [ + 'url' => 'sql.php', + 'params' => Url::getCommon([ + 'sql_query' => 'SHOW OPEN TABLES', + 'goto' => $this->selfUrl, + ], ''), + ]; + + if ($GLOBALS['replication_info']['master']['status']) { + $links['repl'][__('Show slave hosts')] = [ + 'url' => 'sql.php', + 'params' => Url::getCommon([ + 'sql_query' => 'SHOW SLAVE HOSTS', + 'goto' => $this->selfUrl, + ], ''), + ]; + $links['repl'][__('Show master status')] = [ + 'url' => '#replication_master', + 'params' => '', + ]; + } + if ($GLOBALS['replication_info']['slave']['status']) { + $links['repl'][__('Show slave status')] = [ + 'url' => '#replication_slave', + 'params' => '', + ]; + } + + $links['repl']['doc'] = 'replication'; + + $links['qcache'][__('Flush query cache')] = [ + 'url' => $this->selfUrl, + 'params' => Url::getCommon(['flush' => 'QUERY CACHE'], ''), + ]; + $links['qcache']['doc'] = 'query_cache'; + + $links['threads']['doc'] = 'mysql_threads'; + + $links['key']['doc'] = 'myisam_key_cache'; + + $links['binlog_cache']['doc'] = 'binary_log'; + + $links['Slow_queries']['doc'] = 'slow_query_log'; + + $links['innodb'][__('Variables')] = [ + 'url' => 'server_engines.php', + 'params' => Url::getCommon(['engine' => 'InnoDB'], ''), + ]; + $links['innodb'][__('InnoDB Status')] = [ + 'url' => 'server_engines.php', + 'params' => Url::getCommon([ + 'engine' => 'InnoDB', + 'page' => 'Status', + ], ''), + ]; + $links['innodb']['doc'] = 'innodb'; + + return $links; + } + + /** + * Calculate some values + * + * @param array $server_status contains results of SHOW GLOBAL STATUS + * @param array $server_variables contains results of SHOW GLOBAL VARIABLES + * + * @return array + */ + private function _calculateValues(array $server_status, array $server_variables) + { + // Key_buffer_fraction + if (isset($server_status['Key_blocks_unused']) + && isset($server_variables['key_cache_block_size']) + && isset($server_variables['key_buffer_size']) + && $server_variables['key_buffer_size'] != 0 + ) { + $server_status['Key_buffer_fraction_%'] + = 100 + - $server_status['Key_blocks_unused'] + * $server_variables['key_cache_block_size'] + / $server_variables['key_buffer_size'] + * 100; + } elseif (isset($server_status['Key_blocks_used']) + && isset($server_variables['key_buffer_size']) + && $server_variables['key_buffer_size'] != 0 + ) { + $server_status['Key_buffer_fraction_%'] + = $server_status['Key_blocks_used'] + * 1024 + / $server_variables['key_buffer_size']; + } + + // Ratio for key read/write + if (isset($server_status['Key_writes']) + && isset($server_status['Key_write_requests']) + && $server_status['Key_write_requests'] > 0 + ) { + $key_writes = $server_status['Key_writes']; + $key_write_requests = $server_status['Key_write_requests']; + $server_status['Key_write_ratio_%'] + = 100 * $key_writes / $key_write_requests; + } + + if (isset($server_status['Key_reads']) + && isset($server_status['Key_read_requests']) + && $server_status['Key_read_requests'] > 0 + ) { + $key_reads = $server_status['Key_reads']; + $key_read_requests = $server_status['Key_read_requests']; + $server_status['Key_read_ratio_%'] + = 100 * $key_reads / $key_read_requests; + } + + // Threads_cache_hitrate + if (isset($server_status['Threads_created']) + && isset($server_status['Connections']) + && $server_status['Connections'] > 0 + ) { + $server_status['Threads_cache_hitrate_%'] + = 100 - $server_status['Threads_created'] + / $server_status['Connections'] * 100; + } + return $server_status; + } + + /** + * Sort variables into arrays + * + * @param array $server_status contains results of SHOW GLOBAL STATUS + * @param array $allocations allocations for sections + * @param array $allocationMap map variables to their section + * @param array $sectionUsed is a section used? + * @param array $used_queries used queries + * + * @return array ($allocationMap, $sectionUsed, $used_queries) + */ + private function _sortVariables( + array $server_status, + array $allocations, + array $allocationMap, + array $sectionUsed, + array $used_queries + ) { + foreach ($server_status as $name => $value) { + $section_found = false; + foreach ($allocations as $filter => $section) { + if (mb_strpos($name, $filter) !== false) { + $allocationMap[$name] = $section; + $sectionUsed[$section] = true; + $section_found = true; + if ($section == 'com' && $value > 0) { + $used_queries[$name] = $value; + } + break; // Only exits inner loop + } + } + if (! $section_found) { + $allocationMap[$name] = 'other'; + $sectionUsed['other'] = true; + } + } + return [ + $allocationMap, + $sectionUsed, + $used_queries, + ]; + } + + /** + * Constructor + */ + public function __construct() + { + $this->selfUrl = basename($GLOBALS['PMA_PHP_SELF']); + + // get status from server + $server_status_result = $GLOBALS['dbi']->tryQuery('SHOW GLOBAL STATUS'); + $server_status = []; + if ($server_status_result === false) { + $this->dataLoaded = false; + } else { + $this->dataLoaded = true; + while ($arr = $GLOBALS['dbi']->fetchRow($server_status_result)) { + $server_status[$arr[0]] = $arr[1]; + } + $GLOBALS['dbi']->freeResult($server_status_result); + } + + // for some calculations we require also some server settings + $server_variables = $GLOBALS['dbi']->fetchResult( + 'SHOW GLOBAL VARIABLES', + 0, + 1 + ); + + // cleanup of some deprecated values + $server_status = self::cleanDeprecated($server_status); + + // calculate some values + $server_status = $this->_calculateValues( + $server_status, + $server_variables + ); + + // split variables in sections + $allocations = $this->_getAllocations(); + + $sections = $this->_getSections(); + + // define some needful links/commands + $links = $this->_getLinks(); + + // Variable to contain all com_ variables (query statistics) + $used_queries = []; + + // Variable to map variable names to their respective section name + // (used for js category filtering) + $allocationMap = []; + + // Variable to mark used sections + $sectionUsed = []; + + // sort vars into arrays + list( + $allocationMap, $sectionUsed, $used_queries + ) = $this->_sortVariables( + $server_status, + $allocations, + $allocationMap, + $sectionUsed, + $used_queries + ); + + // admin commands are not queries (e.g. they include COM_PING, + // which is excluded from $server_status['Questions']) + unset($used_queries['Com_admin_commands']); + + // Set all class properties + $this->db_isLocal = false; + $serverHostToLower = mb_strtolower( + $GLOBALS['cfg']['Server']['host'] + ); + if ($serverHostToLower === 'localhost' + || $GLOBALS['cfg']['Server']['host'] === '127.0.0.1' + || $GLOBALS['cfg']['Server']['host'] === '::1' + ) { + $this->db_isLocal = true; + } + $this->status = $server_status; + $this->sections = $sections; + $this->variables = $server_variables; + $this->used_queries = $used_queries; + $this->allocationMap = $allocationMap; + $this->links = $links; + $this->sectionUsed = $sectionUsed; + } + + /** + * cleanup of some deprecated values + * + * @param array $server_status status array to process + * + * @return array + */ + public static function cleanDeprecated(array $server_status) + { + $deprecated = [ + 'Com_prepare_sql' => 'Com_stmt_prepare', + 'Com_execute_sql' => 'Com_stmt_execute', + 'Com_dealloc_sql' => 'Com_stmt_close', + ]; + foreach ($deprecated as $old => $new) { + if (isset($server_status[$old]) && isset($server_status[$new])) { + unset($server_status[$old]); + } + } + return $server_status; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Server/Status/Monitor.php b/srcs/phpmyadmin/libraries/classes/Server/Status/Monitor.php new file mode 100644 index 0000000..efa9e40 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Server/Status/Monitor.php @@ -0,0 +1,546 @@ +dbi = $dbi; + } + + /** + * Returns JSON for real-time charting data + * + * @param string $requiredData Required data + * + * @return array JSON + */ + public function getJsonForChartingData(string $requiredData): array + { + $ret = json_decode($requiredData, true); + $statusVars = []; + $serverVars = []; + $sysinfo = $cpuload = $memory = 0; + + /* Accumulate all required variables and data */ + list($serverVars, $statusVars, $ret) = $this->getJsonForChartingDataGet( + $ret, + $serverVars, + $statusVars, + $sysinfo, + $cpuload, + $memory + ); + + // Retrieve all required status variables + $statusVarValues = []; + if (count($statusVars)) { + $statusVarValues = $this->dbi->fetchResult( + "SHOW GLOBAL STATUS WHERE Variable_name='" + . implode("' OR Variable_name='", $statusVars) . "'", + 0, + 1 + ); + } + + // Retrieve all required server variables + $serverVarValues = []; + if (count($serverVars)) { + $serverVarValues = $this->dbi->fetchResult( + "SHOW GLOBAL VARIABLES WHERE Variable_name='" + . implode("' OR Variable_name='", $serverVars) . "'", + 0, + 1 + ); + } + + // ...and now assign them + $ret = $this->getJsonForChartingDataSet($ret, $statusVarValues, $serverVarValues); + + $ret['x'] = microtime(true) * 1000; + return $ret; + } + + /** + * Assign the variables for real-time charting data + * + * @param array $ret Real-time charting data + * @param array $statusVarValues Status variable values + * @param array $serverVarValues Server variable values + * + * @return array + */ + private function getJsonForChartingDataSet( + array $ret, + array $statusVarValues, + array $serverVarValues + ): array { + foreach ($ret as $chart_id => $chartNodes) { + foreach ($chartNodes as $node_id => $nodeDataPoints) { + foreach ($nodeDataPoints as $point_id => $dataPoint) { + switch ($dataPoint['type']) { + case 'statusvar': + $ret[$chart_id][$node_id][$point_id]['value'] + = $statusVarValues[$dataPoint['name']]; + break; + case 'servervar': + $ret[$chart_id][$node_id][$point_id]['value'] + = $serverVarValues[$dataPoint['name']]; + break; + } + } + } + } + return $ret; + } + + /** + * Get called to get JSON for charting data + * + * @param array $ret Real-time charting data + * @param array $serverVars Server variable values + * @param array $statusVars Status variable values + * @param mixed $sysinfo System info + * @param mixed $cpuload CPU load + * @param mixed $memory Memory + * + * @return array + */ + private function getJsonForChartingDataGet( + array $ret, + array $serverVars, + array $statusVars, + $sysinfo, + $cpuload, + $memory + ) { + // For each chart + foreach ($ret as $chartId => $chartNodes) { + // For each data series + foreach ($chartNodes as $nodeId => $nodeDataPoints) { + // For each data point in the series (usually just 1) + foreach ($nodeDataPoints as $pointId => $dataPoint) { + list($serverVars, $statusVars, $ret[$chartId][$nodeId][$pointId]) + = $this->getJsonForChartingDataSwitch( + $dataPoint['type'], + $dataPoint['name'], + $serverVars, + $statusVars, + $ret[$chartId][$nodeId][$pointId], + $sysinfo, + $cpuload, + $memory + ); + } /* foreach */ + } /* foreach */ + } + return [ + $serverVars, + $statusVars, + $ret, + ]; + } + + /** + * Switch called to get JSON for charting data + * + * @param string $type Type + * @param string $pName Name + * @param array $serverVars Server variable values + * @param array $statusVars Status variable values + * @param array $ret Real-time charting data + * @param mixed $sysinfo System info + * @param mixed $cpuload CPU load + * @param mixed $memory Memory + * + * @return array + */ + private function getJsonForChartingDataSwitch( + $type, + $pName, + array $serverVars, + array $statusVars, + array $ret, + $sysinfo, + $cpuload, + $memory + ) { + switch ($type) { + /* We only collect the status and server variables here to + * read them all in one query, + * and only afterwards assign them. + * Also do some white list filtering on the names + */ + case 'servervar': + if (! preg_match('/[^a-zA-Z_]+/', $pName)) { + $serverVars[] = $pName; + } + break; + + case 'statusvar': + if (! preg_match('/[^a-zA-Z_]+/', $pName)) { + $statusVars[] = $pName; + } + break; + + case 'proc': + $result = $this->dbi->query('SHOW PROCESSLIST'); + $ret['value'] = $this->dbi->numRows($result); + break; + + case 'cpu': + if (! $sysinfo) { + $sysinfo = SysInfo::get(); + } + if (! $cpuload) { + $cpuload = $sysinfo->loadavg(); + } + + if (SysInfo::getOs() == 'Linux') { + $ret['idle'] = $cpuload['idle']; + $ret['busy'] = $cpuload['busy']; + } else { + $ret['value'] = $cpuload['loadavg']; + } + + break; + + case 'memory': + if (! $sysinfo) { + $sysinfo = SysInfo::get(); + } + if (! $memory) { + $memory = $sysinfo->memory(); + } + + $ret['value'] = isset($memory[$pName]) ? $memory[$pName] : 0; + break; + } + + return [ + $serverVars, + $statusVars, + $ret, + ]; + } + + /** + * Returns JSON for log data with type: slow + * + * @param int $start Unix Time: Start time for query + * @param int $end Unix Time: End time for query + * + * @return array + */ + public function getJsonForLogDataTypeSlow(int $start, int $end): array + { + $query = 'SELECT start_time, user_host, '; + $query .= 'Sec_to_Time(Sum(Time_to_Sec(query_time))) as query_time, '; + $query .= 'Sec_to_Time(Sum(Time_to_Sec(lock_time))) as lock_time, '; + $query .= 'SUM(rows_sent) AS rows_sent, '; + $query .= 'SUM(rows_examined) AS rows_examined, db, sql_text, '; + $query .= 'COUNT(sql_text) AS \'#\' '; + $query .= 'FROM `mysql`.`slow_log` '; + $query .= 'WHERE start_time > FROM_UNIXTIME(' . $start . ') '; + $query .= 'AND start_time < FROM_UNIXTIME(' . $end . ') GROUP BY sql_text'; + + $result = $this->dbi->tryQuery($query); + + $return = [ + 'rows' => [], + 'sum' => [], + ]; + + while ($row = $this->dbi->fetchAssoc($result)) { + $type = mb_strtolower( + mb_substr( + $row['sql_text'], + 0, + mb_strpos($row['sql_text'], ' ') + ) + ); + + switch ($type) { + case 'insert': + case 'update': + //Cut off big inserts and updates, but append byte count instead + if (mb_strlen($row['sql_text']) > 220) { + $implodeSqlText = implode( + ' ', + Util::formatByteDown( + mb_strlen($row['sql_text']), + 2, + 2 + ) + ); + $row['sql_text'] = mb_substr($row['sql_text'], 0, 200) + . '... [' . $implodeSqlText . ']'; + } + break; + default: + break; + } + + if (! isset($return['sum'][$type])) { + $return['sum'][$type] = 0; + } + $return['sum'][$type] += $row['#']; + $return['rows'][] = $row; + } + + $return['sum']['TOTAL'] = array_sum($return['sum']); + $return['numRows'] = count($return['rows']); + + $this->dbi->freeResult($result); + return $return; + } + + /** + * Returns JSon for log data with type: general + * + * @param int $start Unix Time: Start time for query + * @param int $end Unix Time: End time for query + * @param bool $isTypesLimited Whether to limit types or not + * @param bool $removeVariables Whether to remove variables or not + * + * @return array + */ + public function getJsonForLogDataTypeGeneral( + int $start, + int $end, + bool $isTypesLimited, + bool $removeVariables + ): array { + $limitTypes = ''; + if ($isTypesLimited) { + $limitTypes = 'AND argument REGEXP \'^(INSERT|SELECT|UPDATE|DELETE)\' '; + } + + $query = 'SELECT TIME(event_time) as event_time, user_host, thread_id, '; + $query .= 'server_id, argument, count(argument) as \'#\' '; + $query .= 'FROM `mysql`.`general_log` '; + $query .= 'WHERE command_type=\'Query\' '; + $query .= 'AND event_time > FROM_UNIXTIME(' . $start . ') '; + $query .= 'AND event_time < FROM_UNIXTIME(' . $end . ') '; + $query .= $limitTypes . 'GROUP by argument'; // HAVING count > 1'; + + $result = $this->dbi->tryQuery($query); + + $return = [ + 'rows' => [], + 'sum' => [], + ]; + $insertTables = []; + $insertTablesFirst = -1; + $i = 0; + + while ($row = $this->dbi->fetchAssoc($result)) { + preg_match('/^(\w+)\s/', $row['argument'], $match); + $type = mb_strtolower($match[1]); + + if (! isset($return['sum'][$type])) { + $return['sum'][$type] = 0; + } + $return['sum'][$type] += $row['#']; + + switch ($type) { + /** @noinspection PhpMissingBreakStatementInspection */ + case 'insert': + // Group inserts if selected + if ($removeVariables + && preg_match( + '/^INSERT INTO (`|\'|"|)([^\s\\1]+)\\1/i', + $row['argument'], + $matches + ) + ) { + $insertTables[$matches[2]]++; + if ($insertTables[$matches[2]] > 1) { + $return['rows'][$insertTablesFirst]['#'] + = $insertTables[$matches[2]]; + + // Add a ... to the end of this query to indicate that + // there's been other queries + $temp = $return['rows'][$insertTablesFirst]['argument']; + $return['rows'][$insertTablesFirst]['argument'] + .= $this->getSuspensionPoints( + $temp[strlen($temp) - 1] + ); + + // Group this value, thus do not add to the result list + continue 2; + } else { + $insertTablesFirst = $i; + $insertTables[$matches[2]] += $row['#'] - 1; + } + } + // No break here + + case 'update': + // Cut off big inserts and updates, + // but append byte count therefor + if (mb_strlen($row['argument']) > 220) { + $row['argument'] = mb_substr($row['argument'], 0, 200) + . '... [' + . implode( + ' ', + Util::formatByteDown( + mb_strlen($row['argument']), + 2, + 2 + ) + ) + . ']'; + } + break; + + default: + break; + } + + $return['rows'][] = $row; + $i++; + } + + $return['sum']['TOTAL'] = array_sum($return['sum']); + $return['numRows'] = count($return['rows']); + + $this->dbi->freeResult($result); + + return $return; + } + + /** + * Return suspension points if needed + * + * @param string $lastChar Last char + * + * @return string Return suspension points if needed + */ + private function getSuspensionPoints(string $lastChar): string + { + if ($lastChar != '.') { + return '
    ...'; + } + + return ''; + } + + /** + * Returns JSON for logging vars + * + * @param string|null $name Variable name + * @param string|null $value Variable value + * + * @return array JSON + */ + public function getJsonForLoggingVars(?string $name, ?string $value): array + { + if (isset($name) && isset($value)) { + $escapedValue = $this->dbi->escapeString($value); + if (! is_numeric($escapedValue)) { + $escapedValue = "'" . $escapedValue . "'"; + } + + if (! preg_match("/[^a-zA-Z0-9_]+/", $name)) { + $this->dbi->query( + 'SET GLOBAL ' . $name . ' = ' . $escapedValue + ); + } + } + + $loggingVars = $this->dbi->fetchResult( + 'SHOW GLOBAL VARIABLES WHERE Variable_name IN' + . ' ("general_log","slow_query_log","long_query_time","log_output")', + 0, + 1 + ); + return $loggingVars; + } + + /** + * Returns JSON for query_analyzer + * + * @param string $database Database name + * @param string $query SQL query + * + * @return array JSON + */ + public function getJsonForQueryAnalyzer( + string $database, + string $query + ): array { + global $cached_affected_rows; + + $return = []; + + if (strlen($database) > 0) { + $this->dbi->selectDb($database); + } + + if ($profiling = Util::profilingSupported()) { + $this->dbi->query('SET PROFILING=1;'); + } + + // Do not cache query + $sqlQuery = preg_replace( + '/^(\s*SELECT)/i', + '\\1 SQL_NO_CACHE', + $query + ); + + $this->dbi->tryQuery($sqlQuery); + $return['affectedRows'] = $cached_affected_rows; + + $result = $this->dbi->tryQuery('EXPLAIN ' . $sqlQuery); + while ($row = $this->dbi->fetchAssoc($result)) { + $return['explain'][] = $row; + } + + // In case an error happened + $return['error'] = $this->dbi->getError(); + + $this->dbi->freeResult($result); + + if ($profiling) { + $return['profiling'] = []; + $result = $this->dbi->tryQuery( + 'SELECT seq,state,duration FROM INFORMATION_SCHEMA.PROFILING' + . ' WHERE QUERY_ID=1 ORDER BY seq' + ); + while ($row = $this->dbi->fetchAssoc($result)) { + $return['profiling'][] = $row; + } + $this->dbi->freeResult($result); + } + return $return; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Server/UserGroups.php b/srcs/phpmyadmin/libraries/classes/Server/UserGroups.php new file mode 100644 index 0000000..89bc1a3 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Server/UserGroups.php @@ -0,0 +1,390 @@ +' + . sprintf(__('Users of \'%s\' user group'), htmlspecialchars($userGroup)) + . ''; + + $cfgRelation = $relation->getRelationsParam(); + $usersTable = Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['users']); + $sql_query = "SELECT `username` FROM " . $usersTable + . " WHERE `usergroup`='" . $GLOBALS['dbi']->escapeString($userGroup) + . "'"; + $result = $relation->queryAsControlUser($sql_query, false); + if ($result) { + if ($GLOBALS['dbi']->numRows($result) == 0) { + $html_output .= '

    ' + . __('No users were found belonging to this user group.') + . '

    '; + } else { + $html_output .= '' + . '' + . ''; + $i = 0; + while ($row = $GLOBALS['dbi']->fetchRow($result)) { + $i++; + $html_output .= '' + . '' + . '' + . ''; + } + $html_output .= '' + . '
    #' . __('User') . '
    ' . $i . ' ' . htmlspecialchars($row[0]) . '
    '; + } + } + $GLOBALS['dbi']->freeResult($result); + return $html_output; + } + + /** + * Returns HTML for the 'user groups' table + * + * @return string HTML for the 'user groups' table + */ + public static function getHtmlForUserGroupsTable() + { + $relation = new Relation($GLOBALS['dbi']); + $html_output = '

    ' . __('User groups') . '

    '; + $cfgRelation = $relation->getRelationsParam(); + $groupTable = Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['usergroups']); + $sql_query = "SELECT * FROM " . $groupTable . " ORDER BY `usergroup` ASC"; + $result = $relation->queryAsControlUser($sql_query, false); + + if ($result && $GLOBALS['dbi']->numRows($result)) { + $html_output .= '
    '; + $html_output .= Url::getHiddenInputs(); + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + + $userGroups = []; + while ($row = $GLOBALS['dbi']->fetchAssoc($result)) { + $groupName = $row['usergroup']; + if (! isset($userGroups[$groupName])) { + $userGroups[$groupName] = []; + } + $userGroups[$groupName][$row['tab']] = $row['allowed']; + } + foreach ($userGroups as $groupName => $tabs) { + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + $html_output .= ''; + + $html_output .= ''; + + $html_output .= ''; + } + + $html_output .= ''; + $html_output .= '
    ' + . __('User group') . '' . __('Server level tabs') . '' . __('Database level tabs') . '' . __('Table level tabs') . '' . __('Action') . '
    ' . htmlspecialchars($groupName) . '' . self::getAllowedTabNames($tabs, 'server') . '' . self::getAllowedTabNames($tabs, 'db') . '' . self::getAllowedTabNames($tabs, 'table') . ''; + $html_output .= '' + . Util::getIcon('b_usrlist', __('View users')) + . ''; + $html_output .= '  '; + $html_output .= '' + . Util::getIcon('b_edit', __('Edit')) . ''; + $html_output .= '  '; + $html_output .= '' + . Util::getIcon('b_drop', __('Delete')) . ''; + $html_output .= '
    '; + $html_output .= '
    '; + } + $GLOBALS['dbi']->freeResult($result); + + $html_output .= '
    '; + $html_output .= '' + . Util::getIcon('b_usradd') + . __('Add user group') . ''; + $html_output .= '
    '; + + return $html_output; + } + + /** + * Returns the list of allowed menu tab names + * based on a data row from usergroup table. + * + * @param array $row row of usergroup table + * @param string $level 'server', 'db' or 'table' + * + * @return string comma separated list of allowed menu tab names + */ + public static function getAllowedTabNames(array $row, $level) + { + $tabNames = []; + $tabs = Util::getMenuTabList($level); + foreach ($tabs as $tab => $tabName) { + if (! isset($row[$level . '_' . $tab]) + || $row[$level . '_' . $tab] == 'Y' + ) { + $tabNames[] = $tabName; + } + } + return implode(', ', $tabNames); + } + + /** + * Deletes a user group + * + * @param string $userGroup user group name + * + * @return void + */ + public static function delete($userGroup) + { + $relation = new Relation($GLOBALS['dbi']); + $cfgRelation = $relation->getRelationsParam(); + $userTable = Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['users']); + $groupTable = Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['usergroups']); + $sql_query = "DELETE FROM " . $userTable + . " WHERE `usergroup`='" . $GLOBALS['dbi']->escapeString($userGroup) + . "'"; + $relation->queryAsControlUser($sql_query, true); + $sql_query = "DELETE FROM " . $groupTable + . " WHERE `usergroup`='" . $GLOBALS['dbi']->escapeString($userGroup) + . "'"; + $relation->queryAsControlUser($sql_query, true); + } + + /** + * Returns HTML for add/edit user group dialog + * + * @param string $userGroup name of the user group in case of editing + * + * @return string HTML for add/edit user group dialog + */ + public static function getHtmlToEditUserGroup($userGroup = null) + { + $relation = new Relation($GLOBALS['dbi']); + $html_output = ''; + if ($userGroup == null) { + $html_output .= '

    ' . __('Add user group') . '

    '; + } else { + $html_output .= '

    ' + . sprintf(__('Edit user group: \'%s\''), htmlspecialchars($userGroup)) + . '

    '; + } + + $html_output .= '
    '; + $urlParams = []; + if ($userGroup != null) { + $urlParams['userGroup'] = $userGroup; + $urlParams['editUserGroupSubmit'] = '1'; + } else { + $urlParams['addUserGroupSubmit'] = '1'; + } + $html_output .= Url::getHiddenInputs($urlParams); + + $html_output .= '
    '; + $html_output .= '' . __('User group menu assignments') + . '   ' + . '' + . '' + . ''; + + if ($userGroup == null) { + $html_output .= ''; + $html_output .= ''; + $html_output .= '
    '; + } + + $allowedTabs = [ + 'server' => [], + 'db' => [], + 'table' => [], + ]; + if ($userGroup != null) { + $cfgRelation = $relation->getRelationsParam(); + $groupTable = Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['usergroups']); + $sql_query = "SELECT * FROM " . $groupTable + . " WHERE `usergroup`='" . $GLOBALS['dbi']->escapeString($userGroup) + . "'"; + $result = $relation->queryAsControlUser($sql_query, false); + if ($result) { + while ($row = $GLOBALS['dbi']->fetchAssoc($result)) { + $key = $row['tab']; + $value = $row['allowed']; + if (substr($key, 0, 7) == 'server_' && $value == 'Y') { + $allowedTabs['server'][] = mb_substr($key, 7); + } elseif (substr($key, 0, 3) == 'db_' && $value == 'Y') { + $allowedTabs['db'][] = mb_substr($key, 3); + } elseif (substr($key, 0, 6) == 'table_' + && $value == 'Y' + ) { + $allowedTabs['table'][] = mb_substr($key, 6); + } + } + } + $GLOBALS['dbi']->freeResult($result); + } + + $html_output .= self::getTabList( + __('Server-level tabs'), + 'server', + $allowedTabs['server'] + ); + $html_output .= self::getTabList( + __('Database-level tabs'), + 'db', + $allowedTabs['db'] + ); + $html_output .= self::getTabList( + __('Table-level tabs'), + 'table', + $allowedTabs['table'] + ); + + $html_output .= '
    '; + + $html_output .= ''; + + return $html_output; + } + + /** + * Returns HTML for checkbox groups to choose + * tabs of 'server', 'db' or 'table' levels. + * + * @param string $title title of the checkbox group + * @param string $level 'server', 'db' or 'table' + * @param array $selected array of selected allowed tabs + * + * @return string HTML for checkbox groups + */ + public static function getTabList($title, $level, array $selected) + { + $tabs = Util::getMenuTabList($level); + $html_output = '
    '; + $html_output .= '' . $title . ''; + foreach ($tabs as $tab => $tabName) { + $html_output .= '
    '; + $html_output .= ''; + $html_output .= ''; + $html_output .= '
    '; + } + $html_output .= '
    '; + return $html_output; + } + + /** + * Add/update a user group with allowed menu tabs. + * + * @param string $userGroup user group name + * @param boolean $new whether this is a new user group + * + * @return void + */ + public static function edit($userGroup, $new = false) + { + $relation = new Relation($GLOBALS['dbi']); + $tabs = Util::getMenuTabList(); + $cfgRelation = $relation->getRelationsParam(); + $groupTable = Util::backquote($cfgRelation['db']) + . "." . Util::backquote($cfgRelation['usergroups']); + + if (! $new) { + $sql_query = "DELETE FROM " . $groupTable + . " WHERE `usergroup`='" . $GLOBALS['dbi']->escapeString($userGroup) + . "';"; + $relation->queryAsControlUser($sql_query, true); + } + + $sql_query = "INSERT INTO " . $groupTable + . "(`usergroup`, `tab`, `allowed`)" + . " VALUES "; + $first = true; + foreach ($tabs as $tabGroupName => $tabGroup) { + foreach ($tabGroup as $tab => $tabName) { + if (! $first) { + $sql_query .= ", "; + } + $tabName = $tabGroupName . '_' . $tab; + $allowed = isset($_POST[$tabName]) && $_POST[$tabName] == 'Y'; + $sql_query .= "('" . $GLOBALS['dbi']->escapeString($userGroup) . "', '" . $tabName . "', '" + . ($allowed ? "Y" : "N") . "')"; + $first = false; + } + } + $sql_query .= ";"; + $relation->queryAsControlUser($sql_query, true); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Server/Users.php b/srcs/phpmyadmin/libraries/classes/Server/Users.php new file mode 100644 index 0000000..a497241 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Server/Users.php @@ -0,0 +1,64 @@ + __('User accounts overview'), + 'url' => 'server_privileges.php', + 'params' => Url::getCommon(['viewing_mode' => 'server']), + ], + ]; + + if ($GLOBALS['dbi']->isSuperuser()) { + $items[] = [ + 'name' => __('User groups'), + 'url' => 'server_user_groups.php', + 'params' => Url::getCommon(), + ]; + } + + $retval = '
      '; + foreach ($items as $item) { + $class = ''; + if ($item['url'] === $selfUrl) { + $class = ' class="tabactive"'; + } + $retval .= '
    • '; + $retval .= ''; + $retval .= $item['name']; + $retval .= ''; + $retval .= '
    • '; + } + $retval .= '
    '; + $retval .= '
    '; + + return $retval; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Session.php b/srcs/phpmyadmin/libraries/classes/Session.php new file mode 100644 index 0000000..0d43a48 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Session.php @@ -0,0 +1,234 @@ +getMessage()) + ); + } + + /* + * Session initialization is done before selecting language, so we + * can not use translations here. + */ + Core::fatalError( + 'Error during session start; please check your PHP and/or ' + . 'webserver log file and configure your PHP ' + . 'installation properly. Also ensure that cookies are enabled ' + . 'in your browser.' + . '

    ' + . implode('

    ', $messages) + ); + } + + /** + * Set up session + * + * @param Config $config Configuration handler + * @param ErrorHandler $errorHandler Error handler + * @return void + */ + public static function setUp(Config $config, ErrorHandler $errorHandler) + { + // verify if PHP supports session, die if it does not + if (! function_exists('session_name')) { + Core::warnMissingExtension('session', true); + } elseif (! empty(ini_get('session.auto_start')) + && session_name() != 'phpMyAdmin' + && ! empty(session_id())) { + // Do not delete the existing non empty session, it might be used by + // other applications; instead just close it. + if (empty($_SESSION)) { + // Ignore errors as this might have been destroyed in other + // request meanwhile + @session_destroy(); + } elseif (function_exists('session_abort')) { + // PHP 5.6 and newer + session_abort(); + } else { + session_write_close(); + } + } + + // session cookie settings + session_set_cookie_params( + 0, + $config->getRootPath(), + '', + $config->isHttps(), + true + ); + + // cookies are safer (use ini_set() in case this function is disabled) + ini_set('session.use_cookies', 'true'); + + // optionally set session_save_path + $path = $config->get('SessionSavePath'); + if (! empty($path)) { + session_save_path($path); + // We can not do this unconditionally as this would break + // any more complex setup (eg. cluster), see + // https://github.com/phpmyadmin/phpmyadmin/issues/8346 + ini_set('session.save_handler', 'files'); + } + + // use cookies only + ini_set('session.use_only_cookies', '1'); + // strict session mode (do not accept random string as session ID) + ini_set('session.use_strict_mode', '1'); + // make the session cookie HttpOnly + ini_set('session.cookie_httponly', '1'); + // do not force transparent session ids + ini_set('session.use_trans_sid', '0'); + + // delete session/cookies when browser is closed + ini_set('session.cookie_lifetime', '0'); + + // some pages (e.g. stylesheet) may be cached on clients, but not in shared + // proxy servers + session_cache_limiter('private'); + + $httpCookieName = $config->getCookieName('phpMyAdmin'); + @session_name($httpCookieName); + + // Restore correct sesion ID (it might have been reset by auto started session + if ($config->issetCookie('phpMyAdmin')) { + session_id($config->getCookie('phpMyAdmin')); + } + + // on first start of session we check for errors + // f.e. session dir cannot be accessed - session file not created + $orig_error_count = $errorHandler->countErrors(false); + + $session_result = session_start(); + + if ($session_result !== true + || $orig_error_count != $errorHandler->countErrors(false) + ) { + setcookie($httpCookieName, '', 1); + $errors = $errorHandler->sliceErrors($orig_error_count); + self::sessionFailed($errors); + } + unset($orig_error_count, $session_result); + + /** + * Disable setting of session cookies for further session_start() calls. + */ + if (session_status() !== PHP_SESSION_ACTIVE) { + ini_set('session.use_cookies', 'true'); + } + + /** + * Token which is used for authenticating access queries. + * (we use "space PMA_token space" to prevent overwriting) + */ + if (empty($_SESSION[' PMA_token '])) { + self::generateToken(); + + /** + * Check for disk space on session storage by trying to write it. + * + * This seems to be most reliable approach to test if sessions are working, + * otherwise the check would fail with custom session backends. + */ + $orig_error_count = $errorHandler->countErrors(); + session_write_close(); + if ($errorHandler->countErrors() > $orig_error_count) { + $errors = $errorHandler->sliceErrors($orig_error_count); + self::sessionFailed($errors); + } + session_start(); + if (empty($_SESSION[' PMA_token '])) { + Core::fatalError( + 'Failed to store CSRF token in session! ' . + 'Probably sessions are not working properly.' + ); + } + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Setup/ConfigGenerator.php b/srcs/phpmyadmin/libraries/classes/Setup/ConfigGenerator.php new file mode 100644 index 0000000..8ba09ab --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Setup/ConfigGenerator.php @@ -0,0 +1,184 @@ +getConfig(); + + // header + $ret = 'get('PMA_VERSION') + . ' setup script' . $crlf + . ' * Date: ' . gmdate(DATE_RFC1123) . $crlf + . ' */' . $crlf . $crlf; + + //servers + if (! empty($conf['Servers'])) { + $ret .= self::getServerPart($cf, $crlf, $conf['Servers']); + unset($conf['Servers']); + } + + // other settings + $persistKeys = $cf->getPersistKeysMap(); + + foreach ($conf as $k => $v) { + $k = preg_replace('/[^A-Za-z0-9_]/', '_', $k); + $ret .= self::_getVarExport($k, $v, $crlf); + if (isset($persistKeys[$k])) { + unset($persistKeys[$k]); + } + } + // keep 1d array keys which are present in $persist_keys (config.values.php) + foreach (array_keys($persistKeys) as $k) { + if (mb_strpos($k, '/') === false) { + $k = preg_replace('/[^A-Za-z0-9_]/', '_', $k); + $ret .= self::_getVarExport($k, $cf->getDefault($k), $crlf); + } + } + $ret .= '?' . '>'; + + return $ret; + } + + /** + * Returns exported configuration variable + * + * @param string $var_name configuration name + * @param mixed $var_value configuration value(s) + * @param string $crlf line ending + * + * @return string + */ + private static function _getVarExport($var_name, $var_value, $crlf) + { + if (! is_array($var_value) || empty($var_value)) { + return "\$cfg['$var_name'] = " + . var_export($var_value, true) . ';' . $crlf; + } + $ret = ''; + if (self::_isZeroBasedArray($var_value)) { + $ret = "\$cfg['$var_name'] = " + . self::_exportZeroBasedArray($var_value, $crlf) + . ';' . $crlf; + } else { + // string keys: $cfg[key][subkey] = value + foreach ($var_value as $k => $v) { + $k = preg_replace('/[^A-Za-z0-9_]/', '_', $k); + $ret .= "\$cfg['$var_name']['$k'] = " + . var_export($v, true) . ';' . $crlf; + } + } + return $ret; + } + + /** + * Check whether $array is a continuous 0-based array + * + * @param array $array Array to check + * + * @return boolean + */ + private static function _isZeroBasedArray(array $array) + { + for ($i = 0, $nb = count($array); $i < $nb; $i++) { + if (! isset($array[$i])) { + return false; + } + } + return true; + } + + /** + * Exports continuous 0-based array + * + * @param array $array Array to export + * @param string $crlf Newline string + * + * @return string + */ + private static function _exportZeroBasedArray(array $array, $crlf) + { + $retv = []; + foreach ($array as $v) { + $retv[] = var_export($v, true); + } + $ret = "array("; + if (count($retv) <= 4) { + // up to 4 values - one line + $ret .= implode(', ', $retv); + } else { + // more than 4 values - value per line + $imax = count($retv); + for ($i = 0; $i < $imax; $i++) { + $ret .= ($i > 0 ? ',' : '') . $crlf . ' ' . $retv[$i]; + } + } + $ret .= ')'; + return $ret; + } + + /** + * Generate server part of config file + * + * @param ConfigFile $cf Config file + * @param string $crlf Carriage return char + * @param array $servers Servers list + * + * @return string|null + */ + protected static function getServerPart(ConfigFile $cf, $crlf, array $servers) + { + if ($cf->getServerCount() === 0) { + return null; + } + + $ret = "/* Servers configuration */$crlf\$i = 0;" . $crlf . $crlf; + foreach ($servers as $id => $server) { + $ret .= '/* Server: ' + . strtr($cf->getServerName($id) . " [$id] ", '*/', '-') + . "*/" . $crlf + . '$i++;' . $crlf; + foreach ($server as $k => $v) { + $k = preg_replace('/[^A-Za-z0-9_]/', '_', $k); + $ret .= "\$cfg['Servers'][\$i]['$k'] = " + . (is_array($v) && self::_isZeroBasedArray($v) + ? self::_exportZeroBasedArray($v, $crlf) + : var_export($v, true)) + . ';' . $crlf; + } + $ret .= $crlf; + } + $ret .= '/* End of servers configuration */' . $crlf . $crlf; + return $ret; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Setup/FormProcessing.php b/srcs/phpmyadmin/libraries/classes/Setup/FormProcessing.php new file mode 100644 index 0000000..dce9ecc --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Setup/FormProcessing.php @@ -0,0 +1,77 @@ +fixErrors(); + $response = Response::getInstance(); + $response->disable(); + $response->generateHeader303('index.php' . Url::getCommonRaw()); + } + + if (! $form_display->process(false)) { + // handle form view and failed POST + echo $form_display->getDisplay(true, true); + return; + } + + // check for form errors + if (! $form_display->hasErrors()) { + $response = Response::getInstance(); + $response->disable(); + $response->generateHeader303('index.php' . Url::getCommonRaw()); + return; + } + + // form has errors, show warning + $page = isset($_GET['page']) ? $_GET['page'] : ''; + $formset = isset($_GET['formset']) ? $_GET['formset'] : ''; + $formId = Core::isValid($_GET['id'], 'numeric') ? $_GET['id'] : ''; + if ($formId === null && $page == 'servers') { + // we've just added a new server, get its id + $formId = $form_display->getConfigFile()->getServerCount(); + } + + $urlParams = [ + 'page' => $page, + 'formset' => $formset, + 'id' => $formId, + ]; + + $template = new Template(); + echo $template->render('setup/error', [ + 'url_params' => $urlParams, + 'errors' => $form_display->displayErrors(), + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Setup/Index.php b/srcs/phpmyadmin/libraries/classes/Setup/Index.php new file mode 100644 index 0000000..9518221 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Setup/Index.php @@ -0,0 +1,198 @@ + [], + 'notice' => [], + ]; + } else { + // reset message states + foreach ($_SESSION['messages'] as &$messages) { + foreach ($messages as &$msg) { + $msg['fresh'] = false; + $msg['active'] = false; + } + } + } + } + + /** + * Adds a new message to message list + * + * @param string $type one of: notice, error + * @param string $msgId unique message identifier + * @param string $title language string id (in $str array) + * @param string $message message text + * + * @return void + */ + public static function messagesSet($type, $msgId, $title, $message) + { + $fresh = ! isset($_SESSION['messages'][$type][$msgId]); + $_SESSION['messages'][$type][$msgId] = [ + 'fresh' => $fresh, + 'active' => true, + 'title' => $title, + 'message' => $message, + ]; + } + + /** + * Cleans up message list + * + * @return void + */ + public static function messagesEnd() + { + foreach ($_SESSION['messages'] as &$messages) { + $remove_ids = []; + foreach ($messages as $id => &$msg) { + if ($msg['active'] == false) { + $remove_ids[] = $id; + } + } + foreach ($remove_ids as $id) { + unset($messages[$id]); + } + } + } + + /** + * Prints message list, must be called after self::messagesEnd() + * + * @return array + */ + public static function messagesShowHtml() + { + $return = []; + foreach ($_SESSION['messages'] as $type => $messages) { + foreach ($messages as $id => $msg) { + $return[] = [ + 'id' => $id, + 'title' => $msg['title'], + 'type' => $type, + 'message' => $msg['message'], + 'is_hidden' => ! $msg['fresh'] && $type !== 'error', + ]; + } + } + return $return; + } + + /** + * Checks for newest phpMyAdmin version and sets result as a new notice + * + * @return void + */ + public static function versionCheck() + { + // version check messages should always be visible so let's make + // a unique message id each time we run it + $message_id = uniqid('version_check'); + + // Fetch data + $versionInformation = new VersionInformation(); + $version_data = $versionInformation->getLatestVersion(); + + if (empty($version_data)) { + self::messagesSet( + 'error', + $message_id, + __('Version check'), + __( + 'Reading of version failed. ' + . 'Maybe you\'re offline or the upgrade server does not respond.' + ) + ); + return; + } + + $releases = $version_data->releases; + $latestCompatible = $versionInformation->getLatestCompatibleVersion($releases); + if ($latestCompatible != null) { + $version = $latestCompatible['version']; + $date = $latestCompatible['date']; + } else { + return; + } + + $version_upstream = $versionInformation->versionToInt($version); + if ($version_upstream === false) { + self::messagesSet( + 'error', + $message_id, + __('Version check'), + __('Got invalid version string from server') + ); + return; + } + + $version_local = $versionInformation->versionToInt( + $GLOBALS['PMA_Config']->get('PMA_VERSION') + ); + if ($version_local === false) { + self::messagesSet( + 'error', + $message_id, + __('Version check'), + __('Unparsable version string') + ); + return; + } + + if ($version_upstream > $version_local) { + $version = htmlspecialchars($version); + $date = htmlspecialchars($date); + self::messagesSet( + 'notice', + $message_id, + __('Version check'), + sprintf(__('A newer version of phpMyAdmin is available and you should consider upgrading. The newest version is %s, released on %s.'), $version, $date) + ); + } else { + if ($version_local % 100 == 0) { + self::messagesSet( + 'notice', + $message_id, + __('Version check'), + Sanitize::sanitizeMessage(sprintf(__('You are using Git version, run [kbd]git pull[/kbd] :-)[br]The latest stable version is %s, released on %s.'), $version, $date)) + ); + } else { + self::messagesSet( + 'notice', + $message_id, + __('Version check'), + __('No newer stable version is available') + ); + } + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Sql.php b/srcs/phpmyadmin/libraries/classes/Sql.php new file mode 100644 index 0000000..fb4b07f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Sql.php @@ -0,0 +1,2328 @@ +relation = new Relation($GLOBALS['dbi']); + $this->relationCleanup = new RelationCleanup($GLOBALS['dbi'], $this->relation); + $this->operations = new Operations($GLOBALS['dbi'], $this->relation); + $this->transformations = new Transformations(); + $this->template = new Template(); + } + + /** + * Parses and analyzes the given SQL query. + * + * @param string $sql_query SQL query + * @param string $db DB name + * + * @return mixed + */ + public function parseAndAnalyze($sql_query, $db = null) + { + if ($db === null && isset($GLOBALS['db']) && strlen($GLOBALS['db'])) { + $db = $GLOBALS['db']; + } + list($analyzed_sql_results,,) = ParseAnalyze::sqlQuery($sql_query, $db); + return $analyzed_sql_results; + } + + /** + * Handle remembered sorting order, only for single table query + * + * @param string $db database name + * @param string $table table name + * @param array $analyzed_sql_results the analyzed query results + * @param string $full_sql_query SQL query + * + * @return void + */ + private function handleSortOrder( + $db, + $table, + array &$analyzed_sql_results, + &$full_sql_query + ) { + $pmatable = new Table($table, $db); + + if (empty($analyzed_sql_results['order'])) { + // Retrieving the name of the column we should sort after. + $sortCol = $pmatable->getUiProp(Table::PROP_SORTED_COLUMN); + if (empty($sortCol)) { + return; + } + + // Remove the name of the table from the retrieved field name. + $sortCol = str_replace( + Util::backquote($table) . '.', + '', + $sortCol + ); + + // Create the new query. + $full_sql_query = Query::replaceClause( + $analyzed_sql_results['statement'], + $analyzed_sql_results['parser']->list, + 'ORDER BY ' . $sortCol + ); + + // TODO: Avoid reparsing the query. + $analyzed_sql_results = Query::getAll($full_sql_query); + } else { + // Store the remembered table into session. + $pmatable->setUiProp( + Table::PROP_SORTED_COLUMN, + Query::getClause( + $analyzed_sql_results['statement'], + $analyzed_sql_results['parser']->list, + 'ORDER BY' + ) + ); + } + } + + /** + * Append limit clause to SQL query + * + * @param array $analyzed_sql_results the analyzed query results + * + * @return string limit clause appended SQL query + */ + private function getSqlWithLimitClause(array &$analyzed_sql_results) + { + return Query::replaceClause( + $analyzed_sql_results['statement'], + $analyzed_sql_results['parser']->list, + 'LIMIT ' . $_SESSION['tmpval']['pos'] . ', ' + . $_SESSION['tmpval']['max_rows'] + ); + } + + /** + * Verify whether the result set has columns from just one table + * + * @param array $fields_meta meta fields + * + * @return boolean whether the result set has columns from just one table + */ + private function resultSetHasJustOneTable(array $fields_meta) + { + $just_one_table = true; + $prev_table = ''; + foreach ($fields_meta as $one_field_meta) { + if ($one_field_meta->table != '' + && $prev_table != '' + && $one_field_meta->table != $prev_table + ) { + $just_one_table = false; + } + if ($one_field_meta->table != '') { + $prev_table = $one_field_meta->table; + } + } + return $just_one_table && $prev_table != ''; + } + + /** + * Verify whether the result set contains all the columns + * of at least one unique key + * + * @param string $db database name + * @param string $table table name + * @param array $fields_meta meta fields + * + * @return boolean whether the result set contains a unique key + */ + private function resultSetContainsUniqueKey($db, $table, array $fields_meta) + { + $columns = $GLOBALS['dbi']->getColumns($db, $table); + $resultSetColumnNames = []; + foreach ($fields_meta as $oneMeta) { + $resultSetColumnNames[] = $oneMeta->name; + } + foreach (Index::getFromTable($table, $db) as $index) { + if ($index->isUnique()) { + $indexColumns = $index->getColumns(); + $numberFound = 0; + foreach ($indexColumns as $indexColumnName => $dummy) { + if (in_array($indexColumnName, $resultSetColumnNames)) { + $numberFound++; + } elseif (! in_array($indexColumnName, $columns)) { + $numberFound++; + } elseif (strpos($columns[$indexColumnName]['Extra'], 'INVISIBLE') !== false) { + $numberFound++; + } + } + if ($numberFound == count($indexColumns)) { + return true; + } + } + } + return false; + } + + /** + * Get the HTML for relational column dropdown + * During grid edit, if we have a relational field, returns the html for the + * dropdown + * + * @param string $db current database + * @param string $table current table + * @param string $column current column + * @param string $curr_value current selected value + * + * @return string html for the dropdown + */ + private function getHtmlForRelationalColumnDropdown($db, $table, $column, $curr_value) + { + $foreigners = $this->relation->getForeigners($db, $table, $column); + + $foreignData = $this->relation->getForeignData( + $foreigners, + $column, + false, + '', + '' + ); + + if ($foreignData['disp_row'] == null) { + //Handle the case when number of values + //is more than $cfg['ForeignKeyMaxLimit'] + $_url_params = [ + 'db' => $db, + 'table' => $table, + 'field' => $column, + ]; + + $dropdown = $this->template->render('sql/relational_column_dropdown', [ + 'current_value' => $_POST['curr_value'], + 'params' => $_url_params, + ]); + } else { + $dropdown = $this->relation->foreignDropdown( + $foreignData['disp_row'], + $foreignData['foreign_field'], + $foreignData['foreign_display'], + $curr_value, + $GLOBALS['cfg']['ForeignKeyMaxLimit'] + ); + $dropdown = ''; + } + + return $dropdown; + } + + /** + * Get the HTML for the profiling table and accompanying chart if profiling is set. + * Otherwise returns null + * + * @param string|null $urlQuery url query + * @param string $database current database + * @param array $profilingResults array containing the profiling info + * + * @return string html for the profiling table and chart + */ + private function getHtmlForProfilingChart($urlQuery, $database, $profilingResults): string + { + if (! empty($profilingResults)) { + $urlQuery = isset($urlQuery) ? $urlQuery : Url::getCommon(['db' => $database]); + + list( + $detailedTable, + $chartJson, + $profilingStats + ) = $this->analyzeAndGetTableHtmlForProfilingResults($profilingResults); + + return $this->template->render('sql/profiling_chart', [ + 'url_query' => $urlQuery, + 'detailed_table' => $detailedTable, + 'states' => $profilingStats['states'], + 'total_time' => $profilingStats['total_time'], + 'chart_json' => $chartJson, + ]); + } + return ''; + } + + /** + * Function to get HTML for detailed profiling results table, profiling stats, and + * $chart_json for displaying the chart. + * + * @param array $profiling_results profiling results + * + * @return mixed + */ + private function analyzeAndGetTableHtmlForProfilingResults( + $profiling_results + ) { + $profiling_stats = [ + 'total_time' => 0, + 'states' => [], + ]; + $chart_json = []; + $i = 1; + $table = ''; + foreach ($profiling_results as $one_result) { + if (! isset($profiling_stats['states'][ucwords($one_result['Status'])])) { + $profiling_stats['states'][ucwords($one_result['Status'])] = [ + 'total_time' => $one_result['Duration'], + 'calls' => 1, + ]; + } + $profiling_stats['total_time'] += $one_result['Duration']; + + $table .= $this->template->render('sql/detailed_table', [ + 'index' => $i++, + 'status' => $one_result['Status'], + 'duration' => $one_result['Duration'], + ]); + + if (isset($chart_json[ucwords($one_result['Status'])])) { + $chart_json[ucwords($one_result['Status'])] + += $one_result['Duration']; + } else { + $chart_json[ucwords($one_result['Status'])] + = $one_result['Duration']; + } + } + return [ + $table, + $chart_json, + $profiling_stats, + ]; + } + + /** + * Get the HTML for the enum column dropdown + * During grid edit, if we have a enum field, returns the html for the + * dropdown + * + * @param string $db current database + * @param string $table current table + * @param string $column current column + * @param string $curr_value currently selected value + * + * @return string html for the dropdown + */ + private function getHtmlForEnumColumnDropdown($db, $table, $column, $curr_value) + { + $values = $this->getValuesForColumn($db, $table, $column); + return $this->template->render('sql/enum_column_dropdown', [ + 'values' => $values, + 'selected_values' => [$curr_value], + ]); + } + + /** + * Get value of a column for a specific row (marked by $where_clause) + * + * @param string $db current database + * @param string $table current table + * @param string $column current column + * @param string $where_clause where clause to select a particular row + * + * @return string with value + */ + private function getFullValuesForSetColumn($db, $table, $column, $where_clause) + { + $result = $GLOBALS['dbi']->fetchSingleRow( + "SELECT `$column` FROM `$db`.`$table` WHERE $where_clause" + ); + + return $result[$column]; + } + + /** + * Get the HTML for the set column dropdown + * During grid edit, if we have a set field, returns the html for the + * dropdown + * + * @param string $db current database + * @param string $table current table + * @param string $column current column + * @param string $curr_value currently selected value + * + * @return string html for the set column + */ + private function getHtmlForSetColumn($db, $table, $column, $curr_value): string + { + $values = $this->getValuesForColumn($db, $table, $column); + + $full_values = isset($_POST['get_full_values']) ? $_POST['get_full_values'] : false; + $where_clause = isset($_POST['where_clause']) ? $_POST['where_clause'] : null; + + // If the $curr_value was truncated, we should + // fetch the correct full values from the table + if ($full_values && ! empty($where_clause)) { + $curr_value = $this->getFullValuesForSetColumn( + $db, + $table, + $column, + $where_clause + ); + } + + //converts characters of $curr_value to HTML entities + $converted_curr_value = htmlentities( + $curr_value, + ENT_COMPAT, + "UTF-8" + ); + + $selected_values = explode(',', $converted_curr_value); + $select_size = (count($values) > 10) ? 10 : count($values); + + return $this->template->render('sql/set_column', [ + 'size' => $select_size, + 'values' => $values, + 'selected_values' => $selected_values, + ]); + } + + /** + * Get all the values for a enum column or set column in a table + * + * @param string $db current database + * @param string $table current table + * @param string $column current column + * + * @return array array containing the value list for the column + */ + private function getValuesForColumn($db, $table, $column) + { + $field_info_query = $GLOBALS['dbi']->getColumnsSql($db, $table, $column); + + $field_info_result = $GLOBALS['dbi']->fetchResult( + $field_info_query, + null, + null, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + + return Util::parseEnumSetValues($field_info_result[0]['Type']); + } + + /** + * Function to get html for bookmark support if bookmarks are enabled. Else will + * return null + * + * @param array $displayParts the parts to display + * @param array $cfgBookmark configuration setting for bookmarking + * @param string $sql_query sql query + * @param string $db current database + * @param string $table current table + * @param string|null $complete_query complete query + * @param string $bkm_user bookmarking user + * + * @return string + */ + public function getHtmlForBookmark( + array $displayParts, + array $cfgBookmark, + $sql_query, + $db, + $table, + ?string $complete_query, + $bkm_user + ): string { + if ($displayParts['bkm_form'] == '1' + && (! empty($cfgBookmark) && empty($_GET['id_bookmark'])) + && ! empty($sql_query) + ) { + return $this->template->render('sql/bookmark', [ + 'db' => $db, + 'goto' => 'sql.php' . Url::getCommon([ + 'db' => $db, + 'table' => $table, + 'sql_query' => $sql_query, + 'id_bookmark' => 1, + ]), + 'user' => $bkm_user, + 'sql_query' => isset($complete_query) ? $complete_query : $sql_query, + ]); + } + return ''; + } + + /** + * Function to check whether to remember the sorting order or not + * + * @param array $analyzed_sql_results the analyzed query and other variables set + * after analyzing the query + * + * @return boolean + */ + private function isRememberSortingOrder(array $analyzed_sql_results) + { + return $GLOBALS['cfg']['RememberSorting'] + && ! ($analyzed_sql_results['is_count'] + || $analyzed_sql_results['is_export'] + || $analyzed_sql_results['is_func'] + || $analyzed_sql_results['is_analyse']) + && $analyzed_sql_results['select_from'] + && isset($analyzed_sql_results['select_expr']) + && isset($analyzed_sql_results['select_tables']) + && (empty($analyzed_sql_results['select_expr']) + || ((count($analyzed_sql_results['select_expr']) === 1) + && ($analyzed_sql_results['select_expr'][0] == '*'))) + && count($analyzed_sql_results['select_tables']) === 1; + } + + /** + * Function to check whether the LIMIT clause should be appended or not + * + * @param array $analyzed_sql_results the analyzed query and other variables set + * after analyzing the query + * + * @return boolean + */ + private function isAppendLimitClause(array $analyzed_sql_results) + { + // Assigning LIMIT clause to an syntactically-wrong query + // is not needed. Also we would want to show the true query + // and the true error message to the query executor + + return (isset($analyzed_sql_results['parser']) + && count($analyzed_sql_results['parser']->errors) === 0) + && ($_SESSION['tmpval']['max_rows'] != 'all') + && ! ($analyzed_sql_results['is_export'] + || $analyzed_sql_results['is_analyse']) + && ($analyzed_sql_results['select_from'] + || $analyzed_sql_results['is_subquery']) + && empty($analyzed_sql_results['limit']); + } + + /** + * Function to check whether this query is for just browsing + * + * @param array $analyzed_sql_results the analyzed query and other variables set + * after analyzing the query + * @param boolean|null $find_real_end whether the real end should be found + * + * @return boolean + */ + public function isJustBrowsing(array $analyzed_sql_results, ?bool $find_real_end): bool + { + return ! $analyzed_sql_results['is_group'] + && ! $analyzed_sql_results['is_func'] + && empty($analyzed_sql_results['union']) + && empty($analyzed_sql_results['distinct']) + && $analyzed_sql_results['select_from'] + && (count($analyzed_sql_results['select_tables']) === 1) + && (empty($analyzed_sql_results['statement']->where) + || (count($analyzed_sql_results['statement']->where) === 1 + && $analyzed_sql_results['statement']->where[0]->expr === '1')) + && empty($analyzed_sql_results['group']) + && ! isset($find_real_end) + && ! $analyzed_sql_results['is_subquery'] + && ! $analyzed_sql_results['join'] + && empty($analyzed_sql_results['having']); + } + + /** + * Function to check whether the related transformation information should be deleted + * + * @param array $analyzed_sql_results the analyzed query and other variables set + * after analyzing the query + * + * @return boolean + */ + private function isDeleteTransformationInfo(array $analyzed_sql_results) + { + return ! empty($analyzed_sql_results['querytype']) + && (($analyzed_sql_results['querytype'] == 'ALTER') + || ($analyzed_sql_results['querytype'] == 'DROP')); + } + + /** + * Function to check whether the user has rights to drop the database + * + * @param array $analyzed_sql_results the analyzed query and other variables set + * after analyzing the query + * @param boolean $allowUserDropDatabase whether the user is allowed to drop db + * @param boolean $is_superuser whether this user is a superuser + * + * @return boolean + */ + public function hasNoRightsToDropDatabase( + array $analyzed_sql_results, + $allowUserDropDatabase, + $is_superuser + ) { + return ! $allowUserDropDatabase + && isset($analyzed_sql_results['drop_database']) + && $analyzed_sql_results['drop_database'] + && ! $is_superuser; + } + + /** + * Function to set a column property + * + * @param Table $pmatable Table instance + * @param string $request_index col_order|col_visib + * + * @return boolean + */ + private function setColumnProperty($pmatable, $request_index) + { + $property_value = array_map('intval', explode(',', $_POST[$request_index])); + switch ($request_index) { + case 'col_order': + $property_to_set = Table::PROP_COLUMN_ORDER; + break; + case 'col_visib': + $property_to_set = Table::PROP_COLUMN_VISIB; + break; + default: + $property_to_set = ''; + } + $retval = $pmatable->setUiProp( + $property_to_set, + $property_value, + isset($_POST['table_create_time']) ? $_POST['table_create_time'] : null + ); + if (gettype($retval) != 'boolean') { + $response = Response::getInstance(); + $response->setRequestStatus(false); + $response->addJSON('message', $retval->getString()); + exit; + } + + return $retval; + } + + /** + * Function to check the request for setting the column order or visibility + * + * @param string $table the current table + * @param string $db the current database + * + * @return void + */ + public function setColumnOrderOrVisibility($table, $db) + { + $pmatable = new Table($table, $db); + $retval = false; + + // set column order + if (isset($_POST['col_order'])) { + $retval = $this->setColumnProperty($pmatable, 'col_order'); + } + + // set column visibility + if ($retval === true && isset($_POST['col_visib'])) { + $retval = $this->setColumnProperty($pmatable, 'col_visib'); + } + + $response = Response::getInstance(); + $response->setRequestStatus($retval === true); + exit; + } + + /** + * Function to add a bookmark + * + * @param string $goto goto page URL + * + * @return void + */ + public function addBookmark($goto) + { + $bookmark = Bookmark::createBookmark( + $GLOBALS['dbi'], + $GLOBALS['cfg']['Server']['user'], + $_POST['bkm_fields'], + (isset($_POST['bkm_all_users']) + && $_POST['bkm_all_users'] == 'true' ? true : false + ) + ); + $result = $bookmark->save(); + $response = Response::getInstance(); + if ($response->isAjax()) { + if ($result) { + $msg = Message::success(__('Bookmark %s has been created.')); + $msg->addParam($_POST['bkm_fields']['bkm_label']); + $response->addJSON('message', $msg); + } else { + $msg = Message::error(__('Bookmark not created!')); + $response->setRequestStatus(false); + $response->addJSON('message', $msg); + } + exit; + } else { + // go back to sql.php to redisplay query; do not use & in this case: + /** + * @todo In which scenario does this happen? + */ + Core::sendHeaderLocation( + './' . $goto + . '&label=' . $_POST['bkm_fields']['bkm_label'] + ); + } + } + + /** + * Function to find the real end of rows + * + * @param string $db the current database + * @param string $table the current table + * + * @return mixed the number of rows if "retain" param is true, otherwise true + */ + public function findRealEndOfRows($db, $table) + { + $unlim_num_rows = $GLOBALS['dbi']->getTable($db, $table)->countRecords(true); + $_SESSION['tmpval']['pos'] = $this->getStartPosToDisplayRow($unlim_num_rows); + + return $unlim_num_rows; + } + + /** + * Function to get values for the relational columns + * + * @param string $db the current database + * @param string $table the current table + * + * @return void + */ + public function getRelationalValues($db, $table) + { + $column = $_POST['column']; + if ($_SESSION['tmpval']['relational_display'] == 'D' + && isset($_POST['relation_key_or_display_column']) + && $_POST['relation_key_or_display_column'] + ) { + $curr_value = $_POST['relation_key_or_display_column']; + } else { + $curr_value = $_POST['curr_value']; + } + $dropdown = $this->getHtmlForRelationalColumnDropdown( + $db, + $table, + $column, + $curr_value + ); + $response = Response::getInstance(); + $response->addJSON('dropdown', $dropdown); + exit; + } + + /** + * Function to get values for Enum or Set Columns + * + * @param string $db the current database + * @param string $table the current table + * @param string $columnType whether enum or set + * + * @return void + */ + public function getEnumOrSetValues($db, $table, $columnType) + { + $column = $_POST['column']; + $curr_value = $_POST['curr_value']; + $response = Response::getInstance(); + if ($columnType == "enum") { + $dropdown = $this->getHtmlForEnumColumnDropdown( + $db, + $table, + $column, + $curr_value + ); + $response->addJSON('dropdown', $dropdown); + } else { + $select = $this->getHtmlForSetColumn( + $db, + $table, + $column, + $curr_value + ); + $response->addJSON('select', $select); + } + exit; + } + + /** + * Function to get the default sql query for browsing page + * + * @param string $db the current database + * @param string $table the current table + * + * @return string the default $sql_query for browse page + */ + public function getDefaultSqlQueryForBrowse($db, $table) + { + $bookmark = Bookmark::get( + $GLOBALS['dbi'], + $GLOBALS['cfg']['Server']['user'], + $db, + $table, + 'label', + false, + true + ); + + if (! empty($bookmark) && ! empty($bookmark->getQuery())) { + $GLOBALS['using_bookmark_message'] = Message::notice( + __('Using bookmark "%s" as default browse query.') + ); + $GLOBALS['using_bookmark_message']->addParam($table); + $GLOBALS['using_bookmark_message']->addHtml( + Util::showDocu('faq', 'faq6-22') + ); + $sql_query = $bookmark->getQuery(); + } else { + $defaultOrderByClause = ''; + + if (isset($GLOBALS['cfg']['TablePrimaryKeyOrder']) + && ($GLOBALS['cfg']['TablePrimaryKeyOrder'] !== 'NONE') + ) { + $primaryKey = null; + $primary = Index::getPrimary($table, $db); + + if ($primary !== false) { + $primarycols = $primary->getColumns(); + + foreach ($primarycols as $col) { + $primaryKey = $col->getName(); + break; + } + + if ($primaryKey != null) { + $defaultOrderByClause = ' ORDER BY ' + . Util::backquote($table) . '.' + . Util::backquote($primaryKey) . ' ' + . $GLOBALS['cfg']['TablePrimaryKeyOrder']; + } + } + } + + $sql_query = 'SELECT * FROM ' . Util::backquote($table) + . $defaultOrderByClause; + } + + return $sql_query; + } + + /** + * Responds an error when an error happens when executing the query + * + * @param boolean $is_gotofile whether goto file or not + * @param string $error error after executing the query + * @param string $full_sql_query full sql query + * + * @return void + */ + private function handleQueryExecuteError($is_gotofile, $error, $full_sql_query) + { + if ($is_gotofile) { + $message = Message::rawError($error); + $response = Response::getInstance(); + $response->setRequestStatus(false); + $response->addJSON('message', $message); + } else { + Util::mysqlDie($error, $full_sql_query, '', ''); + } + exit; + } + + /** + * Function to store the query as a bookmark + * + * @param string $db the current database + * @param string $bkm_user the bookmarking user + * @param string $sql_query_for_bookmark the query to be stored in bookmark + * @param string $bkm_label bookmark label + * @param boolean|null $bkm_replace whether to replace existing bookmarks + * + * @return void + */ + public function storeTheQueryAsBookmark( + $db, + $bkm_user, + $sql_query_for_bookmark, + $bkm_label, + ?bool $bkm_replace + ) { + $bfields = [ + 'bkm_database' => $db, + 'bkm_user' => $bkm_user, + 'bkm_sql_query' => $sql_query_for_bookmark, + 'bkm_label' => $bkm_label, + ]; + + // Should we replace bookmark? + if (isset($bkm_replace)) { + $bookmarks = Bookmark::getList( + $GLOBALS['dbi'], + $GLOBALS['cfg']['Server']['user'], + $db + ); + foreach ($bookmarks as $bookmark) { + if ($bookmark->getLabel() == $bkm_label) { + $bookmark->delete(); + } + } + } + + $bookmark = Bookmark::createBookmark( + $GLOBALS['dbi'], + $GLOBALS['cfg']['Server']['user'], + $bfields, + isset($_POST['bkm_all_users']) + ); + $bookmark->save(); + } + + /** + * Executes the SQL query and measures its execution time + * + * @param string $full_sql_query the full sql query + * + * @return array ($result, $querytime) + */ + private function executeQueryAndMeasureTime($full_sql_query) + { + // close session in case the query takes too long + session_write_close(); + + // Measure query time. + $querytime_before = array_sum(explode(' ', microtime())); + + $result = @$GLOBALS['dbi']->tryQuery( + $full_sql_query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + $querytime_after = array_sum(explode(' ', microtime())); + + // reopen session + session_start(); + + return [ + $result, + $querytime_after - $querytime_before, + ]; + } + + /** + * Function to get the affected or changed number of rows after executing a query + * + * @param boolean $is_affected whether the query affected a table + * @param mixed $result results of executing the query + * + * @return int number of rows affected or changed + */ + private function getNumberOfRowsAffectedOrChanged($is_affected, $result) + { + if (! $is_affected) { + $num_rows = $result ? @$GLOBALS['dbi']->numRows($result) : 0; + } else { + $num_rows = @$GLOBALS['dbi']->affectedRows(); + } + + return $num_rows; + } + + /** + * Checks if the current database has changed + * This could happen if the user sends a query like "USE `database`;" + * + * @param string $db the database in the query + * + * @return bool whether to reload the navigation(1) or not(0) + */ + private function hasCurrentDbChanged($db): bool + { + if (strlen($db) > 0) { + $current_db = $GLOBALS['dbi']->fetchValue('SELECT DATABASE()'); + // $current_db is false, except when a USE statement was sent + return ($current_db != false) && ($db !== $current_db); + } + + return false; + } + + /** + * If a table, database or column gets dropped, clean comments. + * + * @param string $db current database + * @param string $table current table + * @param string|null $column current column + * @param bool $purge whether purge set or not + * + * @return void + */ + private function cleanupRelations($db, $table, ?string $column, $purge) + { + if (! empty($purge) && strlen($db) > 0) { + if (strlen($table) > 0) { + if (isset($column) && strlen($column) > 0) { + $this->relationCleanup->column($db, $table, $column); + } else { + $this->relationCleanup->table($db, $table); + } + } else { + $this->relationCleanup->database($db); + } + } + } + + /** + * Function to count the total number of rows for the same 'SELECT' query without + * the 'LIMIT' clause that may have been programatically added + * + * @param int $num_rows number of rows affected/changed by the query + * @param bool $justBrowsing whether just browsing or not + * @param string $db the current database + * @param string $table the current table + * @param array $analyzed_sql_results the analyzed query and other variables set + * after analyzing the query + * + * @return int unlimited number of rows + */ + private function countQueryResults( + $num_rows, + $justBrowsing, + $db, + $table, + array $analyzed_sql_results + ) { + + /* Shortcut for not analyzed/empty query */ + if (empty($analyzed_sql_results)) { + return 0; + } + + if (! $this->isAppendLimitClause($analyzed_sql_results)) { + // if we did not append a limit, set this to get a correct + // "Showing rows..." message + // $_SESSION['tmpval']['max_rows'] = 'all'; + $unlim_num_rows = $num_rows; + } elseif ($this->isAppendLimitClause($analyzed_sql_results) && $_SESSION['tmpval']['max_rows'] > $num_rows) { + // When user has not defined a limit in query and total rows in + // result are less than max_rows to display, there is no need + // to count total rows for that query again + $unlim_num_rows = $_SESSION['tmpval']['pos'] + $num_rows; + } elseif ($analyzed_sql_results['querytype'] == 'SELECT' + || $analyzed_sql_results['is_subquery'] + ) { + // c o u n t q u e r y + + // If we are "just browsing", there is only one table (and no join), + // and no WHERE clause (or just 'WHERE 1 '), + // we do a quick count (which uses MaxExactCount) because + // SQL_CALC_FOUND_ROWS is not quick on large InnoDB tables + + // However, do not count again if we did it previously + // due to $find_real_end == true + if ($justBrowsing) { + // Get row count (is approximate for InnoDB) + $unlim_num_rows = $GLOBALS['dbi']->getTable($db, $table)->countRecords(); + /** + * @todo Can we know at this point that this is InnoDB, + * (in this case there would be no need for getting + * an exact count)? + */ + if ($unlim_num_rows < $GLOBALS['cfg']['MaxExactCount']) { + // Get the exact count if approximate count + // is less than MaxExactCount + /** + * @todo In countRecords(), MaxExactCount is also verified, + * so can we avoid checking it twice? + */ + $unlim_num_rows = $GLOBALS['dbi']->getTable($db, $table) + ->countRecords(true); + } + } else { + // The SQL_CALC_FOUND_ROWS option of the SELECT statement is used. + + // For UNION statements, only a SQL_CALC_FOUND_ROWS is required + // after the first SELECT. + + $count_query = Query::replaceClause( + $analyzed_sql_results['statement'], + $analyzed_sql_results['parser']->list, + 'SELECT SQL_CALC_FOUND_ROWS', + null, + true + ); + + // Another LIMIT clause is added to avoid long delays. + // A complete result will be returned anyway, but the LIMIT would + // stop the query as soon as the result that is required has been + // computed. + + if (empty($analyzed_sql_results['union'])) { + $count_query .= ' LIMIT 1'; + } + + // Running the count query. + $GLOBALS['dbi']->tryQuery($count_query); + + $unlim_num_rows = $GLOBALS['dbi']->fetchValue('SELECT FOUND_ROWS()'); + } // end else "just browsing" + } else {// not $is_select + $unlim_num_rows = 0; + } + + return $unlim_num_rows; + } + + /** + * Function to handle all aspects relating to executing the query + * + * @param array $analyzed_sql_results analyzed sql results + * @param string $full_sql_query full sql query + * @param boolean $is_gotofile whether to go to a file + * @param string $db current database + * @param string $table current table + * @param boolean|null $find_real_end whether to find the real end + * @param string $sql_query_for_bookmark sql query to be stored as bookmark + * @param array $extra_data extra data + * + * @return mixed + */ + private function executeTheQuery( + array $analyzed_sql_results, + $full_sql_query, + $is_gotofile, + $db, + $table, + ?bool $find_real_end, + $sql_query_for_bookmark, + $extra_data + ) { + $response = Response::getInstance(); + $response->getHeader()->getMenu()->setTable($table); + + // Only if we ask to see the php code + if (isset($GLOBALS['show_as_php'])) { + $result = null; + $num_rows = 0; + $unlim_num_rows = 0; + } else { // If we don't ask to see the php code + if (isset($_SESSION['profiling']) + && Util::profilingSupported() + ) { + $GLOBALS['dbi']->query('SET PROFILING=1;'); + } + + list( + $result, + $GLOBALS['querytime'] + ) = $this->executeQueryAndMeasureTime($full_sql_query); + + // Displays an error message if required and stop parsing the script + $error = $GLOBALS['dbi']->getError(); + if ($error && $GLOBALS['cfg']['IgnoreMultiSubmitErrors']) { + $extra_data['error'] = $error; + } elseif ($error) { + $this->handleQueryExecuteError($is_gotofile, $error, $full_sql_query); + } + + // If there are no errors and bookmarklabel was given, + // store the query as a bookmark + if (! empty($_POST['bkm_label']) && ! empty($sql_query_for_bookmark)) { + $cfgBookmark = Bookmark::getParams($GLOBALS['cfg']['Server']['user']); + $this->storeTheQueryAsBookmark( + $db, + $cfgBookmark['user'], + $sql_query_for_bookmark, + $_POST['bkm_label'], + isset($_POST['bkm_replace']) ? $_POST['bkm_replace'] : null + ); + } // end store bookmarks + + // Gets the number of rows affected/returned + // (This must be done immediately after the query because + // mysql_affected_rows() reports about the last query done) + $num_rows = $this->getNumberOfRowsAffectedOrChanged( + $analyzed_sql_results['is_affected'], + $result + ); + + // Grabs the profiling results + if (isset($_SESSION['profiling']) + && Util::profilingSupported() + ) { + $profiling_results = $GLOBALS['dbi']->fetchResult('SHOW PROFILE;'); + } + + $justBrowsing = $this->isJustBrowsing( + $analyzed_sql_results, + isset($find_real_end) ? $find_real_end : null + ); + + $unlim_num_rows = $this->countQueryResults( + $num_rows, + $justBrowsing, + $db, + $table, + $analyzed_sql_results + ); + + $this->cleanupRelations( + isset($db) ? $db : '', + isset($table) ? $table : '', + isset($_POST['dropped_column']) ? $_POST['dropped_column'] : null, + isset($_POST['purge']) ? $_POST['purge'] : null + ); + + if (isset($_POST['dropped_column']) + && strlen($db) > 0 + && strlen($table) > 0 + ) { + // to refresh the list of indexes (Ajax mode) + $extra_data['indexes_list'] = Index::getHtmlForIndexes( + $table, + $db + ); + } + } + + return [ + $result, + $num_rows, + $unlim_num_rows, + isset($profiling_results) ? $profiling_results : null, + $extra_data, + ]; + } + /** + * Delete related transformation information + * + * @param string $db current database + * @param string $table current table + * @param array $analyzed_sql_results analyzed sql results + * + * @return void + */ + private function deleteTransformationInfo($db, $table, array $analyzed_sql_results) + { + if (! isset($analyzed_sql_results['statement'])) { + return; + } + $statement = $analyzed_sql_results['statement']; + if ($statement instanceof AlterStatement) { + if (! empty($statement->altered[0]) + && $statement->altered[0]->options->has('DROP') + ) { + if (! empty($statement->altered[0]->field->column)) { + $this->transformations->clear( + $db, + $table, + $statement->altered[0]->field->column + ); + } + } + } elseif ($statement instanceof DropStatement) { + $this->transformations->clear($db, $table); + } + } + + /** + * Function to get the message for the no rows returned case + * + * @param string $message_to_show message to show + * @param array $analyzed_sql_results analyzed sql results + * @param int $num_rows number of rows + * + * @return Message + */ + private function getMessageForNoRowsReturned( + $message_to_show, + array $analyzed_sql_results, + $num_rows + ) { + if ($analyzed_sql_results['querytype'] == 'DELETE"') { + $message = Message::getMessageForDeletedRows($num_rows); + } elseif ($analyzed_sql_results['is_insert']) { + if ($analyzed_sql_results['querytype'] == 'REPLACE') { + // For REPLACE we get DELETED + INSERTED row count, + // so we have to call it affected + $message = Message::getMessageForAffectedRows($num_rows); + } else { + $message = Message::getMessageForInsertedRows($num_rows); + } + $insert_id = $GLOBALS['dbi']->insertId(); + if ($insert_id != 0) { + // insert_id is id of FIRST record inserted in one insert, + // so if we inserted multiple rows, we had to increment this + $message->addText('[br]'); + // need to use a temporary because the Message class + // currently supports adding parameters only to the first + // message + $_inserted = Message::notice(__('Inserted row id: %1$d')); + $_inserted->addParam($insert_id + $num_rows - 1); + $message->addMessage($_inserted); + } + } elseif ($analyzed_sql_results['is_affected']) { + $message = Message::getMessageForAffectedRows($num_rows); + + // Ok, here is an explanation for the !$is_select. + // The form generated by PhpMyAdmin\SqlQueryForm + // and db_sql.php has many submit buttons + // on the same form, and some confusion arises from the + // fact that $message_to_show is sent for every case. + // The $message_to_show containing a success message and sent with + // the form should not have priority over errors + } elseif (! empty($message_to_show) + && $analyzed_sql_results['querytype'] != 'SELECT' + ) { + $message = Message::rawSuccess(htmlspecialchars($message_to_show)); + } elseif (! empty($GLOBALS['show_as_php'])) { + $message = Message::success(__('Showing as PHP code')); + } elseif (isset($GLOBALS['show_as_php'])) { + /* User disable showing as PHP, query is only displayed */ + $message = Message::notice(__('Showing SQL query')); + } else { + $message = Message::success( + __('MySQL returned an empty result set (i.e. zero rows).') + ); + } + + if (isset($GLOBALS['querytime'])) { + $_querytime = Message::notice( + '(' . __('Query took %01.4f seconds.') . ')' + ); + $_querytime->addParam($GLOBALS['querytime']); + $message->addMessage($_querytime); + } + + // In case of ROLLBACK, notify the user. + if (isset($_POST['rollback_query'])) { + $message->addText(__('[ROLLBACK occurred.]')); + } + + return $message; + } + + /** + * Function to respond back when the query returns zero rows + * This method is called + * 1-> When browsing an empty table + * 2-> When executing a query on a non empty table which returns zero results + * 3-> When executing a query on an empty table + * 4-> When executing an INSERT, UPDATE, DELETE query from the SQL tab + * 5-> When deleting a row from BROWSE tab + * 6-> When searching using the SEARCH tab which returns zero results + * 7-> When changing the structure of the table except change operation + * + * @param array $analyzed_sql_results analyzed sql results + * @param string $db current database + * @param string $table current table + * @param string|null $message_to_show message to show + * @param int $num_rows number of rows + * @param DisplayResults $displayResultsObject DisplayResult instance + * @param array|null $extra_data extra data + * @param string $pmaThemeImage uri of the theme image + * @param array|null $profiling_results profiling results + * @param object $result executed query results + * @param string $sql_query sql query + * @param string|null $complete_query complete sql query + * + * @return string html + */ + private function getQueryResponseForNoResultsReturned( + array $analyzed_sql_results, + $db, + $table, + ?string $message_to_show, + $num_rows, + $displayResultsObject, + ?array $extra_data, + $pmaThemeImage, + ?array $profiling_results, + $result, + $sql_query, + ?string $complete_query + ) { + global $url_query; + if ($this->isDeleteTransformationInfo($analyzed_sql_results)) { + $this->deleteTransformationInfo($db, $table, $analyzed_sql_results); + } + + if (isset($extra_data['error'])) { + $message = Message::rawError($extra_data['error']); + } else { + $message = $this->getMessageForNoRowsReturned( + isset($message_to_show) ? $message_to_show : null, + $analyzed_sql_results, + $num_rows + ); + } + + $html_output = ''; + $html_message = Util::getMessage( + $message, + $GLOBALS['sql_query'], + 'success' + ); + $html_output .= $html_message; + if (! isset($GLOBALS['show_as_php'])) { + if (! empty($GLOBALS['reload'])) { + $extra_data['reload'] = 1; + $extra_data['db'] = $GLOBALS['db']; + } + + // For ajax requests add message and sql_query as JSON + if (empty($_REQUEST['ajax_page_request'])) { + $extra_data['message'] = $message; + if ($GLOBALS['cfg']['ShowSQL']) { + $extra_data['sql_query'] = $html_message; + } + } + + $response = Response::getInstance(); + $response->addJSON(isset($extra_data) ? $extra_data : []); + + if (! empty($analyzed_sql_results['is_select']) && + ! isset($extra_data['error'])) { + $url_query = isset($url_query) ? $url_query : null; + + $displayParts = [ + 'edit_lnk' => null, + 'del_lnk' => null, + 'sort_lnk' => '1', + 'nav_bar' => '0', + 'bkm_form' => '1', + 'text_btn' => '1', + 'pview_lnk' => '1', + ]; + + $html_output .= $this->getHtmlForSqlQueryResultsTable( + $displayResultsObject, + $pmaThemeImage, + $url_query, + $displayParts, + false, + 0, + $num_rows, + true, + $result, + $analyzed_sql_results, + true + ); + + if (is_array($profiling_results)) { + $header = $response->getHeader(); + $scripts = $header->getScripts(); + $scripts->addFile('sql.js'); + $html_output .= $this->getHtmlForProfilingChart( + $url_query, + $db, + $profiling_results + ); + } + + $html_output .= $displayResultsObject->getCreateViewQueryResultOp( + $analyzed_sql_results + ); + + $cfgBookmark = Bookmark::getParams($GLOBALS['cfg']['Server']['user']); + if ($cfgBookmark) { + $html_output .= $this->getHtmlForBookmark( + $displayParts, + $cfgBookmark, + $sql_query, + $db, + $table, + isset($complete_query) ? $complete_query : $sql_query, + $cfgBookmark['user'] + ); + } + } + } + + return $html_output; + } + + /** + * Function to send response for ajax grid edit + * + * @param object $result result of the executed query + * + * @return void + */ + private function sendResponseForGridEdit($result) + { + $row = $GLOBALS['dbi']->fetchRow($result); + $field_flags = $GLOBALS['dbi']->fieldFlags($result, 0); + if (false !== stripos($field_flags, DisplayResults::BINARY_FIELD)) { + $row[0] = bin2hex($row[0]); + } + $response = Response::getInstance(); + $response->addJSON('value', $row[0]); + exit; + } + + /** + * Returns a message for successful creation of a bookmark or null if a bookmark + * was not created + * + * @return string + */ + private function getBookmarkCreatedMessage(): string + { + $output = ''; + if (isset($_GET['label'])) { + $message = Message::success( + __('Bookmark %s has been created.') + ); + $message->addParam($_GET['label']); + $output = $message->getDisplay(); + } + + return $output; + } + + /** + * Function to get html for the sql query results table + * + * @param DisplayResults $displayResultsObject instance of DisplayResult + * @param string $pmaThemeImage theme image uri + * @param string $url_query url query + * @param array $displayParts the parts to display + * @param bool $editable whether the result table is + * editable or not + * @param int $unlim_num_rows unlimited number of rows + * @param int $num_rows number of rows + * @param bool $showtable whether to show table or not + * @param object|null $result result of the executed query + * @param array $analyzed_sql_results analyzed sql results + * @param bool $is_limited_display Show only limited operations or not + * + * @return string + */ + private function getHtmlForSqlQueryResultsTable( + $displayResultsObject, + $pmaThemeImage, + $url_query, + array $displayParts, + $editable, + $unlim_num_rows, + $num_rows, + $showtable, + $result, + array $analyzed_sql_results, + $is_limited_display = false + ) { + $printview = isset($_POST['printview']) && $_POST['printview'] == '1' ? '1' : null; + $table_html = ''; + $browse_dist = ! empty($_POST['is_browse_distinct']); + + if ($analyzed_sql_results['is_procedure']) { + do { + if (! isset($result)) { + $result = $GLOBALS['dbi']->storeResult(); + } + $num_rows = $GLOBALS['dbi']->numRows($result); + + if ($result !== false && $num_rows > 0) { + $fields_meta = $GLOBALS['dbi']->getFieldsMeta($result); + if (! is_array($fields_meta)) { + $fields_cnt = 0; + } else { + $fields_cnt = count($fields_meta); + } + + $displayResultsObject->setProperties( + $num_rows, + $fields_meta, + $analyzed_sql_results['is_count'], + $analyzed_sql_results['is_export'], + $analyzed_sql_results['is_func'], + $analyzed_sql_results['is_analyse'], + $num_rows, + $fields_cnt, + $GLOBALS['querytime'], + $pmaThemeImage, + $GLOBALS['text_dir'], + $analyzed_sql_results['is_maint'], + $analyzed_sql_results['is_explain'], + $analyzed_sql_results['is_show'], + $showtable, + $printview, + $url_query, + $editable, + $browse_dist + ); + + $displayParts = [ + 'edit_lnk' => $displayResultsObject::NO_EDIT_OR_DELETE, + 'del_lnk' => $displayResultsObject::NO_EDIT_OR_DELETE, + 'sort_lnk' => '1', + 'nav_bar' => '1', + 'bkm_form' => '1', + 'text_btn' => '1', + 'pview_lnk' => '1', + ]; + + $table_html .= $displayResultsObject->getTable( + $result, + $displayParts, + $analyzed_sql_results, + $is_limited_display + ); + } + + $GLOBALS['dbi']->freeResult($result); + } while ($GLOBALS['dbi']->moreResults() && $GLOBALS['dbi']->nextResult()); + } else { + $fields_meta = []; + if (isset($result) && ! is_bool($result)) { + $fields_meta = $GLOBALS['dbi']->getFieldsMeta($result); + } + $fields_cnt = count($fields_meta); + $_SESSION['is_multi_query'] = false; + $displayResultsObject->setProperties( + $unlim_num_rows, + $fields_meta, + $analyzed_sql_results['is_count'], + $analyzed_sql_results['is_export'], + $analyzed_sql_results['is_func'], + $analyzed_sql_results['is_analyse'], + $num_rows, + $fields_cnt, + $GLOBALS['querytime'], + $pmaThemeImage, + $GLOBALS['text_dir'], + $analyzed_sql_results['is_maint'], + $analyzed_sql_results['is_explain'], + $analyzed_sql_results['is_show'], + $showtable, + $printview, + $url_query, + $editable, + $browse_dist + ); + + if (! is_bool($result)) { + $table_html .= $displayResultsObject->getTable( + $result, + $displayParts, + $analyzed_sql_results, + $is_limited_display + ); + } + $GLOBALS['dbi']->freeResult($result); + } + + return $table_html; + } + + /** + * Function to get html for the previous query if there is such. If not will return + * null + * + * @param string|null $displayQuery display query + * @param bool $showSql whether to show sql + * @param array $sqlData sql data + * @param Message|string $displayMessage display message + * + * @return string + */ + private function getHtmlForPreviousUpdateQuery( + ?string $displayQuery, + bool $showSql, + $sqlData, + $displayMessage + ): string { + $output = ''; + if (isset($displayQuery) && ($showSql === true) && empty($sqlData)) { + $output = Util::getMessage( + $displayMessage, + $displayQuery, + 'success' + ); + } + + return $output; + } + + /** + * To get the message if a column index is missing. If not will return null + * + * @param string $table current table + * @param string $database current database + * @param boolean $editable whether the results table can be editable or not + * @param boolean $hasUniqueKey whether there is a unique key + * + * @return string + */ + private function getMessageIfMissingColumnIndex($table, $database, $editable, $hasUniqueKey): string + { + $output = ''; + if (! empty($table) && ($GLOBALS['dbi']->isSystemSchema($database) || ! $editable)) { + $output = Message::notice( + sprintf( + __( + 'Current selection does not contain a unique column.' + . ' Grid edit, checkbox, Edit, Copy and Delete features' + . ' are not available. %s' + ), + Util::showDocu( + 'config', + 'cfg_RowActionLinksWithoutUnique' + ) + ) + )->getDisplay(); + } elseif (! empty($table) && ! $hasUniqueKey) { + $output = Message::notice( + sprintf( + __( + 'Current selection does not contain a unique column.' + . ' Grid edit, Edit, Copy and Delete features may result in' + . ' undesired behavior. %s' + ), + Util::showDocu( + 'config', + 'cfg_RowActionLinksWithoutUnique' + ) + ) + )->getDisplay(); + } + + return $output; + } + + /** + * Function to get html to display problems in indexes + * + * @param string|null $queryType query type + * @param array|null $selectedTables array of table names selected from the + * database structure page, for an action + * like check table, optimize table, + * analyze table or repair table + * @param string $database current database + * + * @return string + */ + private function getHtmlForIndexesProblems(?string $queryType, ?array $selectedTables, string $database): string + { + // BEGIN INDEX CHECK See if indexes should be checked. + $output = ''; + if (isset($queryType) + && $queryType == 'check_tbl' + && isset($selectedTables) + && is_array($selectedTables) + ) { + foreach ($selectedTables as $table) { + $check = Index::findDuplicates($table, $database); + if (! empty($check)) { + $output .= sprintf( + __('Problems with indexes of table `%s`'), + $table + ); + $output .= $check; + } + } + } + + return $output; + } + + /** + * Function to display results when the executed query returns non empty results + * + * @param object|null $result executed query results + * @param array $analyzed_sql_results analysed sql results + * @param string $db current database + * @param string $table current table + * @param string|null $message message to show + * @param array|null $sql_data sql data + * @param DisplayResults $displayResultsObject Instance of DisplayResults + * @param string $pmaThemeImage uri of the theme image + * @param int $unlim_num_rows unlimited number of rows + * @param int $num_rows number of rows + * @param string|null $disp_query display query + * @param Message|string|null $disp_message display message + * @param array|null $profiling_results profiling results + * @param string|null $query_type query type + * @param array|null $selectedTables array of table names selected + * from the database structure page, for + * an action like check table, + * optimize table, analyze table or + * repair table + * @param string $sql_query sql query + * @param string|null $complete_query complete sql query + * + * @return string html + */ + private function getQueryResponseForResultsReturned( + $result, + array $analyzed_sql_results, + $db, + $table, + ?string $message, + ?array $sql_data, + $displayResultsObject, + $pmaThemeImage, + $unlim_num_rows, + $num_rows, + ?string $disp_query, + $disp_message, + ?array $profiling_results, + ?string $query_type, + $selectedTables, + $sql_query, + ?string $complete_query + ) { + global $showtable, $url_query; + // If we are retrieving the full value of a truncated field or the original + // value of a transformed field, show it here + if (isset($_POST['grid_edit']) && $_POST['grid_edit'] == true) { + $this->sendResponseForGridEdit($result); + // script has exited at this point + } + + // Gets the list of fields properties + if (isset($result) && $result) { + $fields_meta = $GLOBALS['dbi']->getFieldsMeta($result); + } else { + $fields_meta = []; + } + + // Should be initialized these parameters before parsing + $showtable = isset($showtable) ? $showtable : null; + $url_query = isset($url_query) ? $url_query : null; + + $response = Response::getInstance(); + $header = $response->getHeader(); + $scripts = $header->getScripts(); + + $just_one_table = $this->resultSetHasJustOneTable($fields_meta); + + // hide edit and delete links: + // - for information_schema + // - if the result set does not contain all the columns of a unique key + // (unless this is an updatable view) + // - if the SELECT query contains a join or a subquery + + $updatableView = false; + + $statement = isset($analyzed_sql_results['statement']) ? $analyzed_sql_results['statement'] : null; + if ($statement instanceof SelectStatement) { + if (! empty($statement->expr)) { + if ($statement->expr[0]->expr === '*') { + $_table = new Table($table, $db); + $updatableView = $_table->isUpdatableView(); + } + } + + if ($analyzed_sql_results['join'] + || $analyzed_sql_results['is_subquery'] + || count($analyzed_sql_results['select_tables']) !== 1 + ) { + $just_one_table = false; + } + } + + $has_unique = $this->resultSetContainsUniqueKey( + $db, + $table, + $fields_meta + ); + + $editable = ($has_unique + || $GLOBALS['cfg']['RowActionLinksWithoutUnique'] + || $updatableView) + && $just_one_table; + + $_SESSION['tmpval']['possible_as_geometry'] = $editable; + + $displayParts = [ + 'edit_lnk' => $displayResultsObject::UPDATE_ROW, + 'del_lnk' => $displayResultsObject::DELETE_ROW, + 'sort_lnk' => '1', + 'nav_bar' => '1', + 'bkm_form' => '1', + 'text_btn' => '0', + 'pview_lnk' => '1', + ]; + + if ($GLOBALS['dbi']->isSystemSchema($db) || ! $editable) { + $displayParts = [ + 'edit_lnk' => $displayResultsObject::NO_EDIT_OR_DELETE, + 'del_lnk' => $displayResultsObject::NO_EDIT_OR_DELETE, + 'sort_lnk' => '1', + 'nav_bar' => '1', + 'bkm_form' => '1', + 'text_btn' => '1', + 'pview_lnk' => '1', + ]; + } + if (isset($_POST['printview']) && $_POST['printview'] == '1') { + $displayParts = [ + 'edit_lnk' => $displayResultsObject::NO_EDIT_OR_DELETE, + 'del_lnk' => $displayResultsObject::NO_EDIT_OR_DELETE, + 'sort_lnk' => '0', + 'nav_bar' => '0', + 'bkm_form' => '0', + 'text_btn' => '0', + 'pview_lnk' => '0', + ]; + } + + $tableMaintenanceHtml = ''; + if (isset($_POST['table_maintenance'])) { + $scripts->addFile('makegrid.js'); + $scripts->addFile('sql.js'); + if (isset($message)) { + $message = Message::success($message); + $tableMaintenanceHtml = Util::getMessage( + $message, + $GLOBALS['sql_query'], + 'success' + ); + } + $tableMaintenanceHtml .= $this->getHtmlForSqlQueryResultsTable( + $displayResultsObject, + $pmaThemeImage, + $url_query, + $displayParts, + false, + $unlim_num_rows, + $num_rows, + $showtable, + $result, + $analyzed_sql_results + ); + if (empty($sql_data) || ($sql_data['valid_queries'] = 1)) { + $response->addHTML($tableMaintenanceHtml); + exit; + } + } + + if (! isset($_POST['printview']) || $_POST['printview'] != '1') { + $scripts->addFile('makegrid.js'); + $scripts->addFile('sql.js'); + unset($GLOBALS['message']); + //we don't need to buffer the output in getMessage here. + //set a global variable and check against it in the function + $GLOBALS['buffer_message'] = false; + } + + $previousUpdateQueryHtml = $this->getHtmlForPreviousUpdateQuery( + isset($disp_query) ? $disp_query : null, + (bool) $GLOBALS['cfg']['ShowSQL'], + isset($sql_data) ? $sql_data : null, + isset($disp_message) ? $disp_message : null + ); + + $profilingChartHtml = $this->getHtmlForProfilingChart( + $url_query, + $db, + isset($profiling_results) ? $profiling_results : [] + ); + + $missingUniqueColumnMessage = $this->getMessageIfMissingColumnIndex( + $table, + $db, + $editable, + $has_unique + ); + + $bookmarkCreatedMessage = $this->getBookmarkCreatedMessage(); + + $tableHtml = $this->getHtmlForSqlQueryResultsTable( + $displayResultsObject, + $pmaThemeImage, + $url_query, + $displayParts, + $editable, + $unlim_num_rows, + $num_rows, + $showtable, + $result, + $analyzed_sql_results + ); + + $indexesProblemsHtml = $this->getHtmlForIndexesProblems( + isset($query_type) ? $query_type : null, + isset($selectedTables) ? $selectedTables : null, + $db + ); + + $cfgBookmark = Bookmark::getParams($GLOBALS['cfg']['Server']['user']); + $bookmarkSupportHtml = ''; + if ($cfgBookmark) { + $bookmarkSupportHtml = $this->getHtmlForBookmark( + $displayParts, + $cfgBookmark, + $sql_query, + $db, + $table, + isset($complete_query) ? $complete_query : $sql_query, + $cfgBookmark['user'] + ); + } + + return $this->template->render('sql/sql_query_results', [ + 'table_maintenance' => $tableMaintenanceHtml, + 'previous_update_query' => $previousUpdateQueryHtml, + 'profiling_chart' => $profilingChartHtml, + 'missing_unique_column_message' => $missingUniqueColumnMessage, + 'bookmark_created_message' => $bookmarkCreatedMessage, + 'table' => $tableHtml, + 'indexes_problems' => $indexesProblemsHtml, + 'bookmark_support' => $bookmarkSupportHtml, + ]); + } + + /** + * Function to execute the query and send the response + * + * @param array $analyzed_sql_results analysed sql results + * @param bool $is_gotofile whether goto file or not + * @param string $db current database + * @param string $table current table + * @param bool|null $find_real_end whether to find real end or not + * @param string $sql_query_for_bookmark the sql query to be stored as bookmark + * @param array|null $extra_data extra data + * @param string $message_to_show message to show + * @param string $message message + * @param array|null $sql_data sql data + * @param string $goto goto page url + * @param string $pmaThemeImage uri of the PMA theme image + * @param string $disp_query display query + * @param Message|string $disp_message display message + * @param string $query_type query type + * @param string $sql_query sql query + * @param array|null $selectedTables array of table names selected from the + * database structure page, for an action + * like check table, optimize table, + * analyze table or repair table + * @param string $complete_query complete query + * + * @return void + */ + public function executeQueryAndSendQueryResponse( + $analyzed_sql_results, + $is_gotofile, + $db, + $table, + $find_real_end, + $sql_query_for_bookmark, + $extra_data, + $message_to_show, + $message, + $sql_data, + $goto, + $pmaThemeImage, + $disp_query, + $disp_message, + $query_type, + $sql_query, + $selectedTables, + $complete_query + ) { + if ($analyzed_sql_results == null) { + // Parse and analyze the query + list( + $analyzed_sql_results, + $db, + $table_from_sql + ) = ParseAnalyze::sqlQuery($sql_query, $db); + // @todo: possibly refactor + extract($analyzed_sql_results); + + if ($table != $table_from_sql && ! empty($table_from_sql)) { + $table = $table_from_sql; + } + } + + $html_output = $this->executeQueryAndGetQueryResponse( + $analyzed_sql_results, // analyzed_sql_results + $is_gotofile, // is_gotofile + $db, // db + $table, // table + $find_real_end, // find_real_end + $sql_query_for_bookmark, // sql_query_for_bookmark + $extra_data, // extra_data + $message_to_show, // message_to_show + $message, // message + $sql_data, // sql_data + $goto, // goto + $pmaThemeImage, // pmaThemeImage + $disp_query, // disp_query + $disp_message, // disp_message + $query_type, // query_type + $sql_query, // sql_query + $selectedTables, // selectedTables + $complete_query // complete_query + ); + + $response = Response::getInstance(); + $response->addHTML($html_output); + } + + /** + * Function to execute the query and send the response + * + * @param array $analyzed_sql_results analysed sql results + * @param bool $is_gotofile whether goto file or not + * @param string $db current database + * @param string $table current table + * @param bool|null $find_real_end whether to find real end or not + * @param string|null $sql_query_for_bookmark the sql query to be stored as bookmark + * @param array|null $extra_data extra data + * @param string|null $message_to_show message to show + * @param Message|string|null $message message + * @param array|null $sql_data sql data + * @param string $goto goto page url + * @param string $pmaThemeImage uri of the PMA theme image + * @param string|null $disp_query display query + * @param Message|string|null $disp_message display message + * @param string|null $query_type query type + * @param string $sql_query sql query + * @param array|null $selectedTables array of table names selected from the + * database structure page, for an action + * like check table, optimize table, + * analyze table or repair table + * @param string|null $complete_query complete query + * + * @return string html + */ + public function executeQueryAndGetQueryResponse( + array $analyzed_sql_results, + $is_gotofile, + $db, + $table, + $find_real_end, + ?string $sql_query_for_bookmark, + $extra_data, + ?string $message_to_show, + $message, + $sql_data, + $goto, + $pmaThemeImage, + ?string $disp_query, + $disp_message, + ?string $query_type, + $sql_query, + $selectedTables, + ?string $complete_query + ) { + // Handle disable/enable foreign key checks + $default_fk_check = Util::handleDisableFKCheckInit(); + + // Handle remembered sorting order, only for single table query. + // Handling is not required when it's a union query + // (the parser never sets the 'union' key to 0). + // Handling is also not required if we came from the "Sort by key" + // drop-down. + if (! empty($analyzed_sql_results) + && $this->isRememberSortingOrder($analyzed_sql_results) + && empty($analyzed_sql_results['union']) + && ! isset($_POST['sort_by_key']) + ) { + if (! isset($_SESSION['sql_from_query_box'])) { + $this->handleSortOrder($db, $table, $analyzed_sql_results, $sql_query); + } else { + unset($_SESSION['sql_from_query_box']); + } + } + + $displayResultsObject = new DisplayResults( + $GLOBALS['db'], + $GLOBALS['table'], + $GLOBALS['server'], + $goto, + $sql_query + ); + $displayResultsObject->setConfigParamsForDisplayTable(); + + // assign default full_sql_query + $full_sql_query = $sql_query; + + // Do append a "LIMIT" clause? + if ($this->isAppendLimitClause($analyzed_sql_results)) { + $full_sql_query = $this->getSqlWithLimitClause($analyzed_sql_results); + } + + $GLOBALS['reload'] = $this->hasCurrentDbChanged($db); + $GLOBALS['dbi']->selectDb($db); + + list( + $result, + $num_rows, + $unlim_num_rows, + $profiling_results, + $extra_data + ) = $this->executeTheQuery( + $analyzed_sql_results, + $full_sql_query, + $is_gotofile, + $db, + $table, + isset($find_real_end) ? $find_real_end : null, + isset($sql_query_for_bookmark) ? $sql_query_for_bookmark : null, + isset($extra_data) ? $extra_data : null + ); + + if ($GLOBALS['dbi']->moreResults()) { + $GLOBALS['dbi']->nextResult(); + } + + $warning_messages = $this->operations->getWarningMessagesArray(); + + // No rows returned -> move back to the calling page + if ((0 == $num_rows && 0 == $unlim_num_rows) + || $analyzed_sql_results['is_affected'] + ) { + $html_output = $this->getQueryResponseForNoResultsReturned( + $analyzed_sql_results, + $db, + $table, + isset($message_to_show) ? $message_to_show : null, + $num_rows, + $displayResultsObject, + $extra_data, + $pmaThemeImage, + $profiling_results, + isset($result) ? $result : null, + $sql_query, + isset($complete_query) ? $complete_query : null + ); + } else { + // At least one row is returned -> displays a table with results + $html_output = $this->getQueryResponseForResultsReturned( + isset($result) ? $result : null, + $analyzed_sql_results, + $db, + $table, + isset($message) ? $message : null, + isset($sql_data) ? $sql_data : null, + $displayResultsObject, + $pmaThemeImage, + $unlim_num_rows, + $num_rows, + isset($disp_query) ? $disp_query : null, + isset($disp_message) ? $disp_message : null, + $profiling_results, + isset($query_type) ? $query_type : null, + isset($selectedTables) ? $selectedTables : null, + $sql_query, + isset($complete_query) ? $complete_query : null + ); + } + + // Handle disable/enable foreign key checks + Util::handleDisableFKCheckCleanup($default_fk_check); + + foreach ($warning_messages as $warning) { + $message = Message::notice(Message::sanitize($warning)); + $html_output .= $message->getDisplay(); + } + + return $html_output; + } + + /** + * Function to define pos to display a row + * + * @param int $number_of_line Number of the line to display + * @param int $max_rows Number of rows by page + * + * @return int Start position to display the line + */ + private function getStartPosToDisplayRow($number_of_line, $max_rows = null) + { + if (null === $max_rows) { + $max_rows = $_SESSION['tmpval']['max_rows']; + } + + return @((ceil($number_of_line / $max_rows) - 1) * $max_rows); + } + + /** + * Function to calculate new pos if pos is higher than number of rows + * of displayed table + * + * @param string $db Database name + * @param string $table Table name + * @param int|null $pos Initial position + * + * @return int Number of pos to display last page + */ + public function calculatePosForLastPage($db, $table, $pos) + { + if (null === $pos) { + $pos = $_SESSION['tmpval']['pos']; + } + + $_table = new Table($table, $db); + $unlim_num_rows = $_table->countRecords(true); + //If position is higher than number of rows + if ($unlim_num_rows <= $pos && 0 != $pos) { + $pos = $this->getStartPosToDisplayRow($unlim_num_rows); + } + + return $pos; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/SqlQueryForm.php b/srcs/phpmyadmin/libraries/classes/SqlQueryForm.php new file mode 100644 index 0000000..15aaaf7 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/SqlQueryForm.php @@ -0,0 +1,457 @@ +' . "\n"; + + $html .= '' + . "\n" . Url::getHiddenInputs($db, $table) . "\n" + . '' . "\n" + . '' . "\n" + . '' + . "\n" . '' . "\n"; + + // display querybox + if ($display_tab === 'full' || $display_tab === 'sql') { + $html .= $this->getHtmlForInsert( + $query, + $delimiter + ); + } + + // Bookmark Support + if ($display_tab === 'full') { + $cfgBookmark = Bookmark::getParams($GLOBALS['cfg']['Server']['user']); + if ($cfgBookmark) { + $html .= $this->getHtmlForBookmark(); + } + } + + // Japanese encoding setting + if (Encoding::canConvertKanji()) { + $html .= Encoding::kanjiEncodingForm(); + } + + $html .= '
    ' . "\n"; + // print an empty div, which will be later filled with + // the sql query results by ajax + $html .= '
    '; + + return $html; + } + + /** + * Get initial values for Sql Query Form Insert + * + * @param string $query query to display in the textarea + * + * @return array ($legend, $query, $columns_list) + */ + public function init($query) + { + $columns_list = []; + if (strlen($GLOBALS['db']) === 0) { + // prepare for server related + $legend = sprintf( + __('Run SQL query/queries on server “%s”'), + htmlspecialchars( + ! empty($GLOBALS['cfg']['Servers'][$GLOBALS['server']]['verbose']) + ? $GLOBALS['cfg']['Servers'][$GLOBALS['server']]['verbose'] + : $GLOBALS['cfg']['Servers'][$GLOBALS['server']]['host'] + ) + ); + } elseif (strlen($GLOBALS['table']) === 0) { + // prepare for db related + $db = $GLOBALS['db']; + // if you want navigation: + $tmp_db_link = ''; + $legend = sprintf(__('Run SQL query/queries on database %s'), $tmp_db_link); + if (empty($query)) { + $query = Util::expandUserString( + $GLOBALS['cfg']['DefaultQueryDatabase'], + 'backquote' + ); + } + } else { + $db = $GLOBALS['db']; + $table = $GLOBALS['table']; + // Get the list and number of fields + // we do a try_query here, because we could be in the query window, + // trying to synchronize and the table has not yet been created + $columns_list = $GLOBALS['dbi']->getColumns( + $db, + $GLOBALS['table'], + null, + true + ); + + $tmp_tbl_link = ''; + $tmp_tbl_link .= htmlspecialchars($db) + . '.' . htmlspecialchars($table) . ''; + $legend = sprintf(__('Run SQL query/queries on table %s'), $tmp_tbl_link); + if (empty($query)) { + $query = Util::expandUserString( + $GLOBALS['cfg']['DefaultQueryTable'], + 'backquote' + ); + } + } + $legend .= ': ' . Util::showMySQLDocu('SELECT'); + + return [ + $legend, + $query, + $columns_list, + ]; + } + + /** + * return HTML for Sql Query Form Insert + * + * @param string $query query to display in the textarea + * @param string $delimiter default delimiter to use + * + * @return string + */ + public function getHtmlForInsert( + $query = '', + $delimiter = ';' + ) { + // enable auto select text in textarea + if ($GLOBALS['cfg']['TextareaAutoSelect']) { + $auto_sel = ' onclick="Functions.selectContent(this, sqlBoxLocked, true);"'; + } else { + $auto_sel = ''; + } + + $locking = ''; + $height = $GLOBALS['cfg']['TextareaRows'] * 2; + + list($legend, $query, $columns_list) = $this->init($query); + + if (! empty($columns_list)) { + $sqlquerycontainer_id = 'sqlquerycontainer'; + } else { + $sqlquerycontainer_id = 'sqlquerycontainerfull'; + } + + $html = '' + . '
    ' + . '
    '; + $html .= '' . $legend . ''; + $html .= '
    '; + $html .= '
    ' + . ''; + $html .= '
    '; + // Add buttons to generate query easily for + // select all, single select, insert, update and delete + if (! empty($columns_list)) { + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + $html .= ''; + if ($GLOBALS['cfg']['CodemirrorEnable']) { + $html .= ''; + } + $html .= ''; + + // parameter binding + $html .= '
    '; + $html .= ''; + $html .= ''; + $html .= Util::showDocu('faq', 'faq6-40'); + $html .= '
    '; + $html .= '
    '; + + $html .= '
    ' . "\n"; + + if (! empty($columns_list)) { + $html .= '
    ' + . '' + . '' + . '
    '; + if (Util::showIcons('ActionLinksMode')) { + $html .= ''; + } else { + $html .= ''; + } + $html .= '
    ' . "\n" + . '
    ' . "\n"; + } + + $html .= '
    ' . "\n"; + $html .= '
    ' . "\n"; + + $cfgBookmark = Bookmark::getParams($GLOBALS['cfg']['Server']['user']); + if ($cfgBookmark) { + $html .= '
    '; + $html .= '
    '; + $html .= ''; + $html .= ''; + $html .= '
    '; + $html .= '
    '; + $html .= ''; + $html .= ''; + $html .= '
    '; + $html .= '
    '; + $html .= ''; + $html .= ''; + $html .= '
    '; + $html .= '
    '; + } + + $html .= '
    ' . "\n"; + $html .= '
    ' . "\n" + . '
    ' . "\n"; + + $html .= '
    ' . "\n"; + $html .= '
    ' . "\n"; + $html .= '
    ' . "\n"; + + $html .= '
    '; + $html .= '' . "\n"; + $html .= ' ]'; + $html .= '
    '; + + $html .= '
    '; + $html .= '' + . ''; + $html .= '
    '; + + $html .= '
    '; + $html .= '' + . ''; + $html .= '
    '; + + $html .= '
    '; + $html .= '' + . ''; + $html .= '
    '; + + // Disable/Enable foreign key checks + $html .= '
    '; + $html .= Util::getFKCheckbox(); + $html .= '
    '; + + $html .= '' . "\n"; + $html .= '
    ' . "\n"; + $html .= '
    ' . "\n"; + + return $html; + } + + /** + * return HTML for sql Query Form Bookmark + * + * @return string|null + */ + public function getHtmlForBookmark() + { + $bookmark_list = Bookmark::getList( + $GLOBALS['dbi'], + $GLOBALS['cfg']['Server']['user'], + $GLOBALS['db'] + ); + if (empty($bookmark_list) || count($bookmark_list) < 1) { + return null; + } + + $html = '
    '; + $html .= ''; + $html .= __('Bookmarked SQL query') . '' . "\n"; + $html .= '
    '; + $html .= ' ' . "\n"; + $html .= '
    ' . "\n"; + $html .= '
    ' . "\n"; + $html .= '' + . '' . "\n"; + $html .= '' + . '' . "\n"; + $html .= '' + . '' . "\n"; + $html .= '
    ' . "\n"; + $html .= '
    ' . "\n"; + $html .= '
    ' . "\n"; + $html .= __('Variables'); + $html .= Util::showDocu('faq', 'faqbookmark'); + $html .= '
    '; + $html .= '
    ' . "\n"; + $html .= '
    ' . "\n"; + + $html .= '
    '; + $html .= ''; + $html .= '
    ' . "\n"; + $html .= '
    ' . "\n"; + + return $html; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/StorageEngine.php b/srcs/phpmyadmin/libraries/classes/StorageEngine.php new file mode 100644 index 0000000..cbb3172 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/StorageEngine.php @@ -0,0 +1,465 @@ +engine = $engine; + $this->title = $storage_engines[$engine]['Engine']; + $this->comment = (isset($storage_engines[$engine]['Comment']) + ? $storage_engines[$engine]['Comment'] + : ''); + switch ($storage_engines[$engine]['Support']) { + case 'DEFAULT': + $this->support = PMA_ENGINE_SUPPORT_DEFAULT; + break; + case 'YES': + $this->support = PMA_ENGINE_SUPPORT_YES; + break; + case 'DISABLED': + $this->support = PMA_ENGINE_SUPPORT_DISABLED; + break; + case 'NO': + default: + $this->support = PMA_ENGINE_SUPPORT_NO; + } + } + } + + /** + * Returns array of storage engines + * + * @static + * @staticvar array $storage_engines storage engines + * @access public + * @return array[] array of storage engines + */ + public static function getStorageEngines() + { + static $storage_engines = null; + + if (null == $storage_engines) { + $storage_engines + = $GLOBALS['dbi']->fetchResult('SHOW STORAGE ENGINES', 'Engine'); + if ($GLOBALS['dbi']->getVersion() >= 50708) { + $disabled = (string) Util::cacheGet( + 'disabled_storage_engines', + function () { + return $GLOBALS['dbi']->fetchValue( + 'SELECT @@disabled_storage_engines' + ); + } + ); + foreach (explode(",", $disabled) as $engine) { + if (isset($storage_engines[$engine])) { + $storage_engines[$engine]['Support'] = 'DISABLED'; + } + } + } + } + + return $storage_engines; + } + + /** + * Returns HTML code for storage engine select box + * + * @param string $name The name of the select form element + * @param string $id The ID of the form field + * @param string $selected The selected engine + * @param boolean $offerUnavailableEngines Should unavailable storage + * engines be offered? + * @param boolean $addEmpty Whether to provide empty option + * + * @static + * @return string html selectbox + */ + public static function getHtmlSelect( + $name = 'engine', + $id = null, + $selected = null, + $offerUnavailableEngines = false, + $addEmpty = false + ) { + $selected = mb_strtolower((string) $selected); + $output = '' . "\n"; + return $output; + } + + /** + * Loads the corresponding engine plugin, if available. + * + * @param string $engine The engine ID + * + * @return StorageEngine The engine plugin + * @static + */ + public static function getEngine($engine) + { + switch (mb_strtolower($engine)) { + case 'bdb': + return new Bdb($engine); + case 'berkeleydb': + return new Berkeleydb($engine); + case 'binlog': + return new Binlog($engine); + case 'innobase': + return new Innobase($engine); + case 'innodb': + return new Innodb($engine); + case 'memory': + return new Memory($engine); + case 'merge': + return new Merge($engine); + case 'mrg_myisam': + return new MrgMyisam($engine); + case 'myisam': + return new Myisam($engine); + case 'ndbcluster': + return new Ndbcluster($engine); + case 'pbxt': + return new Pbxt($engine); + case 'performance_schema': + return new PerformanceSchema($engine); + default: + return new StorageEngine($engine); + } + } + + /** + * Returns true if given engine name is supported/valid, otherwise false + * + * @param string $engine name of engine + * + * @static + * @return boolean whether $engine is valid or not + */ + public static function isValid($engine) + { + if ($engine == "PBMS") { + return true; + } + $storage_engines = self::getStorageEngines(); + return isset($storage_engines[$engine]); + } + + /** + * Returns as HTML table of the engine's server variables + * + * @return string The table that was generated based on the retrieved + * information + */ + public function getHtmlVariables() + { + $ret = ''; + + foreach ($this->getVariablesStatus() as $details) { + $ret .= '' . "\n" + . ' ' . "\n"; + if (! empty($details['desc'])) { + $ret .= ' ' + . Util::showHint($details['desc']) + . "\n"; + } + $ret .= ' ' . "\n" + . ' ' . htmlspecialchars($details['title']) . '' + . "\n" + . ' '; + switch ($details['type']) { + case PMA_ENGINE_DETAILS_TYPE_SIZE: + $parsed_size = $this->resolveTypeSize($details['value']); + $ret .= $parsed_size[0] . ' ' . $parsed_size[1]; + unset($parsed_size); + break; + case PMA_ENGINE_DETAILS_TYPE_NUMERIC: + $ret .= Util::formatNumber($details['value']) . ' '; + break; + default: + $ret .= htmlspecialchars($details['value']) . ' '; + } + $ret .= '' . "\n" + . '' . "\n"; + } + + if (! $ret) { + $ret = '

    ' . "\n" + . ' ' + . __( + 'There is no detailed status information available for this ' + . 'storage engine.' + ) + . "\n" + . '

    ' . "\n"; + } else { + $ret = '' . "\n" . $ret . '
    ' . "\n"; + } + + return $ret; + } + + /** + * Returns the engine specific handling for + * PMA_ENGINE_DETAILS_TYPE_SIZE type variables. + * + * This function should be overridden when + * PMA_ENGINE_DETAILS_TYPE_SIZE type needs to be + * handled differently for a particular engine. + * + * @param integer $value Value to format + * + * @return array the formatted value and its unit + */ + public function resolveTypeSize($value) + { + return Util::formatByteDown($value); + } + + /** + * Returns array with detailed info about engine specific server variables + * + * @return array array with detailed info about specific engine server variables + */ + public function getVariablesStatus() + { + $variables = $this->getVariables(); + $like = $this->getVariablesLikePattern(); + + if ($like) { + $like = " LIKE '" . $like . "' "; + } else { + $like = ''; + } + + $mysql_vars = []; + + $sql_query = 'SHOW GLOBAL VARIABLES ' . $like . ';'; + $res = $GLOBALS['dbi']->query($sql_query); + while ($row = $GLOBALS['dbi']->fetchAssoc($res)) { + if (isset($variables[$row['Variable_name']])) { + $mysql_vars[$row['Variable_name']] + = $variables[$row['Variable_name']]; + } elseif (! $like + && mb_strpos(mb_strtolower($row['Variable_name']), mb_strtolower($this->engine)) !== 0 + ) { + continue; + } + $mysql_vars[$row['Variable_name']]['value'] = $row['Value']; + + if (empty($mysql_vars[$row['Variable_name']]['title'])) { + $mysql_vars[$row['Variable_name']]['title'] = $row['Variable_name']; + } + + if (! isset($mysql_vars[$row['Variable_name']]['type'])) { + $mysql_vars[$row['Variable_name']]['type'] + = PMA_ENGINE_DETAILS_TYPE_PLAINTEXT; + } + } + $GLOBALS['dbi']->freeResult($res); + + return $mysql_vars; + } + + /** + * Reveals the engine's title + * + * @return string The title + */ + public function getTitle() + { + return $this->title; + } + + /** + * Fetches the server's comment about this engine + * + * @return string The comment + */ + public function getComment() + { + return $this->comment; + } + + /** + * Information message on whether this storage engine is supported + * + * @return string The localized message. + */ + public function getSupportInformationMessage() + { + switch ($this->support) { + case PMA_ENGINE_SUPPORT_DEFAULT: + $message = __('%s is the default storage engine on this MySQL server.'); + break; + case PMA_ENGINE_SUPPORT_YES: + $message = __('%s is available on this MySQL server.'); + break; + case PMA_ENGINE_SUPPORT_DISABLED: + $message = __('%s has been disabled for this MySQL server.'); + break; + case PMA_ENGINE_SUPPORT_NO: + default: + $message = __( + 'This MySQL server does not support the %s storage engine.' + ); + } + return sprintf($message, htmlspecialchars($this->title)); + } + + /** + * Generates a list of MySQL variables that provide information about this + * engine. This function should be overridden when extending this class + * for a particular engine. + * + * @return array The list of variables. + */ + public function getVariables() + { + return []; + } + + /** + * Returns string with filename for the MySQL helppage + * about this storage engine + * + * @return string MySQL help page filename + */ + public function getMysqlHelpPage() + { + return $this->engine . '-storage-engine'; + } + + /** + * Returns the pattern to be used in the query for SQL variables + * related to the storage engine + * + * @return string SQL query LIKE pattern + */ + public function getVariablesLikePattern() + { + return ''; + } + + /** + * Returns a list of available information pages with labels + * + * @return string[] The list + */ + public function getInfoPages() + { + return []; + } + + /** + * Generates the requested information page + * + * @param string $id page id + * + * @return string html output + */ + public function getPage($id) + { + if (! array_key_exists($id, $this->getInfoPages())) { + return ''; + } + + $id = 'getPage' . $id; + + return $this->$id(); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/SubPartition.php b/srcs/phpmyadmin/libraries/classes/SubPartition.php new file mode 100644 index 0000000..a5eac8b --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/SubPartition.php @@ -0,0 +1,182 @@ +db = $row['TABLE_SCHEMA']; + $this->table = $row['TABLE_NAME']; + $this->loadData($row); + } + + /** + * Loads data from the fetched row from information_schema.PARTITIONS + * + * @param array $row fetched row + * + * @return void + */ + protected function loadData(array $row) + { + $this->name = $row['SUBPARTITION_NAME']; + $this->ordinal = $row['SUBPARTITION_ORDINAL_POSITION']; + $this->method = $row['SUBPARTITION_METHOD']; + $this->expression = $row['SUBPARTITION_EXPRESSION']; + $this->loadCommonData($row); + } + + /** + * Loads some data that is common to both partitions and sub partitions + * + * @param array $row fetched row + * + * @return void + */ + protected function loadCommonData(array $row) + { + $this->rows = $row['TABLE_ROWS']; + $this->dataLength = $row['DATA_LENGTH']; + $this->indexLength = $row['INDEX_LENGTH']; + $this->comment = $row['PARTITION_COMMENT']; + } + + /** + * Return the partition name + * + * @return string partition name + */ + public function getName() + { + return $this->name; + } + + /** + * Return the ordinal of the partition + * + * @return int the ordinal + */ + public function getOrdinal() + { + return $this->ordinal; + } + + /** + * Returns the partition method + * + * @return string partition method + */ + public function getMethod() + { + return $this->method; + } + + /** + * Returns the partition expression + * + * @return string partition expression + */ + public function getExpression() + { + return $this->expression; + } + + /** + * Returns the number of data rows + * + * @return integer number of rows + */ + public function getRows() + { + return $this->rows; + } + + /** + * Returns the data length + * + * @return integer data length + */ + public function getDataLength() + { + return $this->dataLength; + } + + /** + * Returns the index length + * + * @return integer index length + */ + public function getIndexLength() + { + return $this->indexLength; + } + + /** + * Returns the partition comment + * + * @return string partition comment + */ + public function getComment() + { + return $this->comment; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/SysInfo.php b/srcs/phpmyadmin/libraries/classes/SysInfo.php new file mode 100644 index 0000000..8fae3a8 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/SysInfo.php @@ -0,0 +1,73 @@ +supported()) { + return $ret; + } + } + + return new SysInfoBase(); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/SysInfoBase.php b/srcs/phpmyadmin/libraries/classes/SysInfoBase.php new file mode 100644 index 0000000..147c49a --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/SysInfoBase.php @@ -0,0 +1,50 @@ + 0]; + } + + /** + * Gets information about memory usage + * + * @return array with memory usage data + */ + public function memory() + { + return []; + } + + /** + * Checks whether class is supported in this environment + * + * @return bool true on success + */ + public function supported() + { + return true; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/SysInfoLinux.php b/srcs/phpmyadmin/libraries/classes/SysInfoLinux.php new file mode 100644 index 0000000..21ac9cd --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/SysInfoLinux.php @@ -0,0 +1,103 @@ + (int) $nums[1] + (int) $nums[2] + (int) $nums[3], + 'idle' => (int) $nums[4], + ]; + } + + /** + * Checks whether class is supported in this environment + * + * @return bool true on success + */ + public function supported() + { + return @is_readable('/proc/meminfo') && @is_readable('/proc/stat'); + } + + /** + * Gets information about memory usage + * + * @return array with memory usage data + */ + public function memory() + { + preg_match_all( + SysInfo::MEMORY_REGEXP, + file_get_contents('/proc/meminfo'), + $matches + ); + + $mem = array_combine($matches[1], $matches[2]); + + $defaults = [ + 'MemTotal' => 0, + 'MemFree' => 0, + 'Cached' => 0, + 'Buffers' => 0, + 'SwapTotal' => 0, + 'SwapFree' => 0, + 'SwapCached' => 0, + ]; + + $mem = array_merge($defaults, $mem); + + foreach ($mem as $idx => $value) { + $mem[$idx] = intval($value); + } + + $mem['MemUsed'] = $mem['MemTotal'] + - $mem['MemFree'] - $mem['Cached'] - $mem['Buffers']; + + $mem['SwapUsed'] = $mem['SwapTotal'] + - $mem['SwapFree'] - $mem['SwapCached']; + + return $mem; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/SysInfoSunOS.php b/srcs/phpmyadmin/libraries/classes/SysInfoSunOS.php new file mode 100644 index 0000000..6158b62 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/SysInfoSunOS.php @@ -0,0 +1,81 @@ +_kstat('unix:0:system_misc:avenrun_1min'); + + return ['loadavg' => $load1]; + } + + /** + * Checks whether class is supported in this environment + * + * @return bool true on success + */ + public function supported() + { + return @is_readable('/proc/meminfo'); + } + + /** + * Gets information about memory usage + * + * @return array with memory usage data + */ + public function memory() + { + $pagesize = (int) $this->_kstat('unix:0:seg_cache:slab_size'); + $mem = []; + $mem['MemTotal'] = (int) $this->_kstat('unix:0:system_pages:pagestotal') * $pagesize; + $mem['MemUsed'] = (int) $this->_kstat('unix:0:system_pages:pageslocked') * $pagesize; + $mem['MemFree'] = (int) $this->_kstat('unix:0:system_pages:pagesfree') * $pagesize; + $mem['SwapTotal'] = (int) $this->_kstat('unix:0:vminfo:swap_avail') / 1024; + $mem['SwapUsed'] = (int) $this->_kstat('unix:0:vminfo:swap_alloc') / 1024; + $mem['SwapFree'] = (int) $this->_kstat('unix:0:vminfo:swap_free') / 1024; + + return $mem; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/SysInfoWINNT.php b/srcs/phpmyadmin/libraries/classes/SysInfoWINNT.php new file mode 100644 index 0000000..063c7e4 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/SysInfoWINNT.php @@ -0,0 +1,135 @@ +_wmi = null; + } else { + // initialize the wmi object + $objLocator = new COM('WbemScripting.SWbemLocator'); + $this->_wmi = $objLocator->ConnectServer(); + } + } + + /** + * Gets load information + * + * @return array with load data + */ + public function loadavg() + { + $sum = 0; + $buffer = $this->_getWMI('Win32_Processor', ['LoadPercentage']); + + foreach ($buffer as $load) { + $value = $load['LoadPercentage']; + $sum += $value; + } + + return ['loadavg' => $sum / count($buffer)]; + } + + /** + * Checks whether class is supported in this environment + * + * @return bool true on success + */ + public function supported() + { + return $this->_wmi !== null; + } + + /** + * Reads data from WMI + * + * @param string $strClass Class to read + * @param array $strValue Values to read + * + * @return array with results + */ + private function _getWMI($strClass, array $strValue = []) + { + $arrData = []; + + $objWEBM = $this->_wmi->Get($strClass); + $arrProp = $objWEBM->Properties_; + $arrWEBMCol = $objWEBM->Instances_(); + foreach ($arrWEBMCol as $objItem) { + $arrInstance = []; + foreach ($arrProp as $propItem) { + $name = $propItem->Name; + if (empty($strValue) || in_array($name, $strValue)) { + $value = $objItem->$name; + if (is_string($value)) { + $arrInstance[$name] = trim($value); + } else { + $arrInstance[$name] = $value; + } + } + } + $arrData[] = $arrInstance; + } + + return $arrData; + } + + /** + * Gets information about memory usage + * + * @return array with memory usage data + */ + public function memory() + { + $buffer = $this->_getWMI( + "Win32_OperatingSystem", + [ + 'TotalVisibleMemorySize', + 'FreePhysicalMemory', + ] + ); + $mem = []; + $mem['MemTotal'] = $buffer[0]['TotalVisibleMemorySize']; + $mem['MemFree'] = $buffer[0]['FreePhysicalMemory']; + $mem['MemUsed'] = $mem['MemTotal'] - $mem['MemFree']; + + $buffer = $this->_getWMI('Win32_PageFileUsage'); + + $mem['SwapTotal'] = 0; + $mem['SwapUsed'] = 0; + $mem['SwapPeak'] = 0; + + foreach ($buffer as $swapdevice) { + $mem['SwapTotal'] += $swapdevice['AllocatedBaseSize'] * 1024; + $mem['SwapUsed'] += $swapdevice['CurrentUsage'] * 1024; + $mem['SwapPeak'] += $swapdevice['PeakUsage'] * 1024; + } + + return $mem; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/SystemDatabase.php b/srcs/phpmyadmin/libraries/classes/SystemDatabase.php new file mode 100644 index 0000000..8dc5174 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/SystemDatabase.php @@ -0,0 +1,137 @@ +dbi = $dbi; + $this->relation = new Relation($this->dbi); + } + + /** + * Get existing data on transformations applied for + * columns in a particular table + * + * @param string $db Database name looking for + * + * @return mysqli_result Result of executed SQL query + */ + public function getExistingTransformationData($db) + { + $cfgRelation = $this->relation->getRelationsParam(); + + // Get the existing transformation details of the same database + // from pma__column_info table + $pma_transformation_sql = sprintf( + "SELECT * FROM %s.%s WHERE `db_name` = '%s'", + Util::backquote($cfgRelation['db']), + Util::backquote($cfgRelation['column_info']), + $GLOBALS['dbi']->escapeString($db) + ); + + return $this->dbi->tryQuery($pma_transformation_sql); + } + + /** + * Get SQL query for store new transformation details of a VIEW + * + * @param object $pma_transformation_data Result set of SQL execution + * @param array $column_map Details of VIEW columns + * @param string $view_name Name of the VIEW + * @param string $db Database name of the VIEW + * + * @return string SQL query for new transformations + */ + public function getNewTransformationDataSql( + $pma_transformation_data, + array $column_map, + $view_name, + $db + ) { + $cfgRelation = $this->relation->getRelationsParam(); + + // Need to store new transformation details for VIEW + $new_transformations_sql = sprintf( + "INSERT INTO %s.%s (" + . "`db_name`, `table_name`, `column_name`, " + . "`comment`, `mimetype`, `transformation`, " + . "`transformation_options`) VALUES", + Util::backquote($cfgRelation['db']), + Util::backquote($cfgRelation['column_info']) + ); + + $column_count = 0; + $add_comma = false; + + while ($data_row = $this->dbi->fetchAssoc($pma_transformation_data)) { + foreach ($column_map as $column) { + if ($data_row['table_name'] != $column['table_name'] + || $data_row['column_name'] != $column['refering_column'] + ) { + continue; + } + + $new_transformations_sql .= sprintf( + "%s ('%s', '%s', '%s', '%s', '%s', '%s', '%s')", + $add_comma ? ', ' : '', + $db, + $view_name, + isset($column['real_column']) + ? $column['real_column'] + : $column['refering_column'], + $data_row['comment'], + $data_row['mimetype'], + $data_row['transformation'], + $GLOBALS['dbi']->escapeString( + $data_row['transformation_options'] + ) + ); + + $add_comma = true; + $column_count++; + break; + } + + if ($column_count == count($column_map)) { + break; + } + } + + return ($column_count > 0) ? $new_transformations_sql : ''; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Table.php b/srcs/phpmyadmin/libraries/classes/Table.php new file mode 100644 index 0000000..df1fd12 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Table.php @@ -0,0 +1,2771 @@ +_dbi = $dbi; + $this->_name = $table_name; + $this->_db_name = $db_name; + $this->relation = new Relation($this->_dbi); + } + + /** + * returns table name + * + * @see Table::getName() + * @return string table name + */ + public function __toString() + { + return $this->getName(); + } + + /** + * Table getter + * + * @param string $table_name table name + * @param string $db_name database name + * @param DatabaseInterface|null $dbi database interface for the table + * + * @return Table + */ + public static function get($table_name, $db_name, ?DatabaseInterface $dbi = null) + { + return new Table($table_name, $db_name, $dbi); + } + + /** + * return the last error + * + * @return string the last error + */ + public function getLastError() + { + return end($this->errors); + } + + /** + * return the last message + * + * @return string the last message + */ + public function getLastMessage() + { + return end($this->messages); + } + + /** + * returns table name + * + * @param boolean $backquoted whether to quote name with backticks `` + * + * @return string table name + */ + public function getName($backquoted = false) + { + if ($backquoted) { + return Util::backquote($this->_name); + } + return $this->_name; + } + + /** + * returns database name for this table + * + * @param boolean $backquoted whether to quote name with backticks `` + * + * @return string database name for this table + */ + public function getDbName($backquoted = false) + { + if ($backquoted) { + return Util::backquote($this->_db_name); + } + return $this->_db_name; + } + + /** + * returns full name for table, including database name + * + * @param boolean $backquoted whether to quote name with backticks `` + * + * @return string + */ + public function getFullName($backquoted = false) + { + return $this->getDbName($backquoted) . '.' + . $this->getName($backquoted); + } + + + /** + * Checks the storage engine used to create table + * + * @param array|string $engine Checks the table engine against an + * array of engine strings or a single string, should be uppercase + * + * @return bool True, if $engine matches the storage engine for the table, + * False otherwise. + */ + public function isEngine($engine) + { + $tbl_storage_engine = $this->getStorageEngine(); + + if (is_array($engine)) { + foreach ($engine as $e) { + if ($e == $tbl_storage_engine) { + return true; + } + } + return false; + } else { + return $tbl_storage_engine == $engine; + } + } + + /** + * returns whether the table is actually a view + * + * @return boolean whether the given is a view + */ + public function isView() + { + $db = $this->_db_name; + $table = $this->_name; + if (empty($db) || empty($table)) { + return false; + } + + // use cached data or load information with SHOW command + if ($this->_dbi->getCachedTableContent([$db, $table]) != null + || $GLOBALS['cfg']['Server']['DisableIS'] + ) { + $type = $this->getStatusInfo('TABLE_TYPE'); + return $type == 'VIEW' || $type == 'SYSTEM VIEW'; + } + + // information_schema tables are 'SYSTEM VIEW's + if ($db == 'information_schema') { + return true; + } + + // query information_schema + $result = $this->_dbi->fetchResult( + "SELECT TABLE_NAME + FROM information_schema.VIEWS + WHERE TABLE_SCHEMA = '" . $this->_dbi->escapeString((string) $db) . "' + AND TABLE_NAME = '" . $this->_dbi->escapeString((string) $table) . "'" + ); + return $result ? true : false; + } + + /** + * Returns whether the table is actually an updatable view + * + * @return boolean whether the given is an updatable view + */ + public function isUpdatableView() + { + if (empty($this->_db_name) || empty($this->_name)) { + return false; + } + + $result = $this->_dbi->fetchResult( + "SELECT TABLE_NAME + FROM information_schema.VIEWS + WHERE TABLE_SCHEMA = '" . $this->_dbi->escapeString($this->_db_name) . "' + AND TABLE_NAME = '" . $this->_dbi->escapeString($this->_name) . "' + AND IS_UPDATABLE = 'YES'" + ); + return $result ? true : false; + } + + /** + * Checks if this is a merge table + * + * If the ENGINE of the table is MERGE or MRG_MYISAM (alias), + * this is a merge table. + * + * @return boolean true if it is a merge table + */ + public function isMerge() + { + return $this->isEngine(['MERGE', 'MRG_MYISAM']); + } + + /** + * Returns full table status info, or specific if $info provided + * this info is collected from information_schema + * + * @param string $info specific information to be fetched + * @param boolean $force_read read new rather than serving from cache + * @param boolean $disable_error if true, disables error message + * + * @todo DatabaseInterface::getTablesFull needs to be merged + * somehow into this class or at least better documented + * + * @return mixed + */ + public function getStatusInfo( + $info = null, + $force_read = false, + $disable_error = false + ) { + $db = $this->_db_name; + $table = $this->_name; + + if (! empty($_SESSION['is_multi_query'])) { + $disable_error = true; + } + + // sometimes there is only one entry (ExactRows) so + // we have to get the table's details + if ($this->_dbi->getCachedTableContent([$db, $table]) == null + || $force_read + || count($this->_dbi->getCachedTableContent([$db, $table])) === 1 + ) { + $this->_dbi->getTablesFull($db, $table); + } + + if ($this->_dbi->getCachedTableContent([$db, $table]) == null) { + // happens when we enter the table creation dialog + // or when we really did not get any status info, for example + // when $table == 'TABLE_NAMES' after the user tried SHOW TABLES + return ''; + } + + if (null === $info) { + return $this->_dbi->getCachedTableContent([$db, $table]); + } + + // array_key_exists allows for null values + if (! array_key_exists( + $info, + $this->_dbi->getCachedTableContent([$db, $table]) + ) + ) { + if (! $disable_error) { + trigger_error( + __('Unknown table status:') . ' ' . $info, + E_USER_WARNING + ); + } + return false; + } + + return $this->_dbi->getCachedTableContent([$db, $table, $info]); + } + + /** + * Returns the Table storage Engine for current table. + * + * @return string Return storage engine info if it is set for + * the selected table else return blank. + */ + public function getStorageEngine() + { + $table_storage_engine = $this->getStatusInfo('ENGINE', false, true); + if ($table_storage_engine === false) { + return ''; + } + return strtoupper((string) $table_storage_engine); + } + + /** + * Returns the comments for current table. + * + * @return string Return comment info if it is set for the selected table or return blank. + */ + public function getComment() + { + $table_comment = $this->getStatusInfo('TABLE_COMMENT', false, true); + if ($table_comment === false) { + return ''; + } + return $table_comment; + } + + /** + * Returns the collation for current table. + * + * @return string Return blank if collation is empty else return the collation info from table info. + */ + public function getCollation() + { + $table_collation = $this->getStatusInfo('TABLE_COLLATION', false, true); + if ($table_collation === false) { + return ''; + } + return $table_collation; + } + + /** + * Returns the info about no of rows for current table. + * + * @return integer Return no of rows info if it is not null for the selected table or return 0. + */ + public function getNumRows() + { + $table_num_row_info = $this->getStatusInfo('TABLE_ROWS', false, true); + if (false === $table_num_row_info) { + $table_num_row_info = $this->_dbi->getTable($this->_db_name, $GLOBALS['showtable']['Name']) + ->countRecords(true); + } + return $table_num_row_info ?: 0 ; + } + + /** + * Returns the Row format for current table. + * + * @return string Return table row format info if it is set for the selected table or return blank. + */ + public function getRowFormat() + { + $table_row_format = $this->getStatusInfo('ROW_FORMAT', false, true); + if ($table_row_format === false) { + return ''; + } + return $table_row_format; + } + + /** + * Returns the auto increment option for current table. + * + * @return integer Return auto increment info if it is set for the selected table or return blank. + */ + public function getAutoIncrement() + { + $table_auto_increment = $this->getStatusInfo('AUTO_INCREMENT', false, true); + return isset($table_auto_increment) ? $table_auto_increment : ''; + } + + /** + * Returns the array for CREATE statement for current table. + * @return array Return options array info if it is set for the selected table or return blank. + */ + public function getCreateOptions() + { + $table_options = $this->getStatusInfo('CREATE_OPTIONS', false, true); + $create_options_tmp = empty($table_options) ? [] : explode(' ', $table_options); + $create_options = []; + // export create options by its name as variables into global namespace + // f.e. pack_keys=1 becomes available as $pack_keys with value of '1' + // unset($pack_keys); + foreach ($create_options_tmp as $each_create_option) { + $each_create_option = explode('=', $each_create_option); + if (isset($each_create_option[1])) { + // ensure there is no ambiguity for PHP 5 and 7 + $create_options[$each_create_option[0]] = $each_create_option[1]; + } + } + // we need explicit DEFAULT value here (different from '0') + $create_options['pack_keys'] = (! isset($create_options['pack_keys']) || strlen($create_options['pack_keys']) == 0) + ? 'DEFAULT' + : $create_options['pack_keys']; + return $create_options; + } + + /** + * generates column specification for ALTER or CREATE TABLE syntax + * + * @param string $name name + * @param string $type type ('INT', 'VARCHAR', 'BIT', ...) + * @param string $length length ('2', '5,2', '', ...) + * @param string $attribute attribute + * @param string $collation collation + * @param bool|string $null with 'NULL' or 'NOT NULL' + * @param string $default_type whether default is CURRENT_TIMESTAMP, + * NULL, NONE, USER_DEFINED + * @param string $default_value default value for USER_DEFINED + * default type + * @param string $extra 'AUTO_INCREMENT' + * @param string $comment field comment + * @param string $virtuality virtuality of the column + * @param string $expression expression for the virtual column + * @param string $move_to new position for column + * @param array $columns_with_index Fields having PRIMARY or UNIQUE KEY indexes + * @param string $oldColumnName Old column name + * + * @todo move into class PMA_Column + * @todo on the interface, some js to clear the default value when the + * default current_timestamp is checked + * + * @return string field specification + */ + public static function generateFieldSpec( + $name, + $type, + $length = '', + $attribute = '', + $collation = '', + $null = false, + $default_type = 'USER_DEFINED', + $default_value = '', + $extra = '', + $comment = '', + $virtuality = '', + $expression = '', + $move_to = '', + $columns_with_index = null, + $oldColumnName = null + ) { + /** @var DatabaseInterface $dbi */ + $dbi = $GLOBALS['dbi']; + $is_timestamp = mb_strpos( + mb_strtoupper($type), + 'TIMESTAMP' + ) !== false; + + $query = Util::backquote($name) . ' ' . $type; + + // allow the possibility of a length for TIME, DATETIME and TIMESTAMP + // (will work on MySQL >= 5.6.4) + // + // MySQL permits a non-standard syntax for FLOAT and DOUBLE, + // see https://dev.mysql.com/doc/refman/5.5/en/floating-point-types.html + // + $pattern = '@^(DATE|TINYBLOB|TINYTEXT|BLOB|TEXT|' + . 'MEDIUMBLOB|MEDIUMTEXT|LONGBLOB|LONGTEXT|SERIAL|BOOLEAN|UUID)$@i'; + if (strlen($length) !== 0 && ! preg_match($pattern, $type)) { + // Note: The variable $length here can contain several other things + // besides length - ENUM/SET value or length of DECIMAL (eg. 12,3) + // so we can't just convert it to integer + $query .= '(' . $length . ')'; + } + if ($attribute != '') { + $query .= ' ' . $attribute; + + if ($is_timestamp + && false !== stripos($attribute, "TIMESTAMP") + && strlen($length) !== 0 + && $length !== 0 + ) { + $query .= '(' . $length . ')'; + } + } + + // if column is virtual, check if server type is Mysql as only Mysql server + // supports extra column properties + $isVirtualColMysql = $virtuality && in_array(Util::getServerType(), ['MySQL', 'Percona Server']); + // if column is virtual, check if server type is MariaDB as MariaDB server + // supports no extra virtual column properties except CHARACTER SET for text column types + $isVirtualColMariaDB = $virtuality && Util::getServerType() === 'MariaDB'; + + $matches = preg_match( + '@^(TINYTEXT|TEXT|MEDIUMTEXT|LONGTEXT|VARCHAR|CHAR|ENUM|SET)$@i', + $type + ); + if (! empty($collation) && $collation != 'NULL' && $matches) { + $query .= Util::getCharsetQueryPart( + $isVirtualColMariaDB ? preg_replace('~_.+~s', '', $collation) : $collation, + true + ); + } + + if ($virtuality) { + $query .= ' AS (' . $expression . ') ' . $virtuality; + } + + if (! $virtuality || $isVirtualColMysql) { + if ($null !== false) { + if ($null == 'YES') { + $query .= ' NULL'; + } else { + $query .= ' NOT NULL'; + } + } + + if (! $virtuality) { + switch ($default_type) { + case 'USER_DEFINED': + if ($is_timestamp && $default_value === '0') { + // a TIMESTAMP does not accept DEFAULT '0' + // but DEFAULT 0 works + $query .= ' DEFAULT 0'; + } elseif ($type == 'BIT') { + $query .= ' DEFAULT b\'' + . preg_replace('/[^01]/', '0', $default_value) + . '\''; + } elseif ($type == 'BOOLEAN') { + if (preg_match('/^1|T|TRUE|YES$/i', (string) $default_value)) { + $query .= ' DEFAULT TRUE'; + } elseif (preg_match('/^0|F|FALSE|NO$/i', $default_value)) { + $query .= ' DEFAULT FALSE'; + } else { + // Invalid BOOLEAN value + $query .= ' DEFAULT \'' + . $dbi->escapeString($default_value) . '\''; + } + } elseif ($type == 'BINARY' || $type == 'VARBINARY') { + $query .= ' DEFAULT 0x' . $default_value; + } else { + $query .= ' DEFAULT \'' + . $dbi->escapeString((string) $default_value) . '\''; + } + break; + /** @noinspection PhpMissingBreakStatementInspection */ + case 'NULL': + // If user uncheck null checkbox and not change default value null, + // default value will be ignored. + if ($null !== false && $null !== 'YES') { + break; + } + // else fall-through intended, no break here + case 'CURRENT_TIMESTAMP': + case 'current_timestamp()': + $query .= ' DEFAULT ' . $default_type; + + if (strlen($length) !== 0 + && $length !== 0 + && $is_timestamp + && $default_type !== 'NULL' // Not to be added in case of NULL + ) { + $query .= '(' . $length . ')'; + } + break; + case 'NONE': + default: + break; + } + } + + if (! empty($extra)) { + if ($virtuality) { + $extra = trim(preg_replace('~^\s*AUTO_INCREMENT\s*~is', ' ', $extra)); + } + + $query .= ' ' . $extra; + } + } + + if (! empty($comment)) { + $query .= " COMMENT '" . $dbi->escapeString($comment) . "'"; + } + + // move column + if ($move_to == '-first') { // dash can't appear as part of column name + $query .= ' FIRST'; + } elseif ($move_to != '') { + $query .= ' AFTER ' . Util::backquote($move_to); + } + if (! $virtuality && ! empty($extra)) { + if ($oldColumnName === null) { + if (is_array($columns_with_index) && ! in_array($name, $columns_with_index)) { + $query .= ', add PRIMARY KEY (' . Util::backquote($name) . ')'; + } + } else { + if (is_array($columns_with_index) && ! in_array($oldColumnName, $columns_with_index)) { + $query .= ', add PRIMARY KEY (' . Util::backquote($name) . ')'; + } + } + } + + return $query; + } // end function + + /** + * Checks if the number of records in a table is at least equal to + * $min_records + * + * @param int $min_records Number of records to check for in a table + * + * @return bool True, if at least $min_records exist, False otherwise. + */ + public function checkIfMinRecordsExist($min_records = 0) + { + $check_query = 'SELECT '; + $fieldsToSelect = ''; + + $uniqueFields = $this->getUniqueColumns(true, false); + if (count($uniqueFields) > 0) { + $fieldsToSelect = implode(', ', $uniqueFields); + } else { + $indexedCols = $this->getIndexedColumns(true, false); + if (count($indexedCols) > 0) { + $fieldsToSelect = implode(', ', $indexedCols); + } else { + $fieldsToSelect = '*'; + } + } + + $check_query .= $fieldsToSelect + . ' FROM ' . $this->getFullName(true) + . ' LIMIT ' . $min_records; + + $res = $this->_dbi->tryQuery( + $check_query + ); + + if ($res !== false) { + $num_records = $this->_dbi->numRows($res); + if ($num_records >= $min_records) { + return true; + } + } + + return false; + } + + /** + * Counts and returns (or displays) the number of records in a table + * + * @param bool $force_exact whether to force an exact count + * + * @return mixed the number of records if "retain" param is true, + * otherwise true + */ + public function countRecords($force_exact = false) + { + $is_view = $this->isView(); + $db = $this->_db_name; + $table = $this->_name; + + if ($this->_dbi->getCachedTableContent([$db, $table, 'ExactRows']) != null) { + $row_count = $this->_dbi->getCachedTableContent( + [ + $db, + $table, + 'ExactRows', + ] + ); + return $row_count; + } + $row_count = false; + + if (! $force_exact) { + if (($this->_dbi->getCachedTableContent([$db, $table, 'Rows']) == null) + && ! $is_view + ) { + $tmp_tables = $this->_dbi->getTablesFull($db, $table); + if (isset($tmp_tables[$table])) { + $this->_dbi->cacheTableContent( + [ + $db, + $table, + ], + $tmp_tables[$table] + ); + } + } + if ($this->_dbi->getCachedTableContent([$db, $table, 'Rows']) != null) { + $row_count = $this->_dbi->getCachedTableContent( + [ + $db, + $table, + 'Rows', + ] + ); + } else { + $row_count = false; + } + } + // for a VIEW, $row_count is always false at this point + if (false !== $row_count + && $row_count >= $GLOBALS['cfg']['MaxExactCount'] + ) { + return $row_count; + } + + if (! $is_view) { + $row_count = $this->_dbi->fetchValue( + 'SELECT COUNT(*) FROM ' . Util::backquote($db) . '.' + . Util::backquote($table) + ); + } else { + // For complex views, even trying to get a partial record + // count could bring down a server, so we offer an + // alternative: setting MaxExactCountViews to 0 will bypass + // completely the record counting for views + + if ($GLOBALS['cfg']['MaxExactCountViews'] == 0) { + $row_count = false; + } else { + // Counting all rows of a VIEW could be too long, + // so use a LIMIT clause. + // Use try_query because it can fail (when a VIEW is + // based on a table that no longer exists) + $result = $this->_dbi->tryQuery( + 'SELECT 1 FROM ' . Util::backquote($db) . '.' + . Util::backquote($table) . ' LIMIT ' + . $GLOBALS['cfg']['MaxExactCountViews'], + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + if (! $this->_dbi->getError()) { + $row_count = $this->_dbi->numRows($result); + $this->_dbi->freeResult($result); + } + } + } + if ($row_count) { + $this->_dbi->cacheTableContent([$db, $table, 'ExactRows'], $row_count); + } + + return $row_count; + } // end of the 'Table::countRecords()' function + + /** + * Generates column specification for ALTER syntax + * + * @param string $oldcol old column name + * @param string $newcol new column name + * @param string $type type ('INT', 'VARCHAR', 'BIT', ...) + * @param string $length length ('2', '5,2', '', ...) + * @param string $attribute attribute + * @param string $collation collation + * @param bool|string $null with 'NULL' or 'NOT NULL' + * @param string $default_type whether default is CURRENT_TIMESTAMP, + * NULL, NONE, USER_DEFINED + * @param string $default_value default value for USER_DEFINED default + * type + * @param string $extra 'AUTO_INCREMENT' + * @param string $comment field comment + * @param string $virtuality virtuality of the column + * @param string $expression expression for the virtual column + * @param string $move_to new position for column + * @param array $columns_with_index Fields having PRIMARY or UNIQUE KEY indexes + * + * @see Table::generateFieldSpec() + * + * @return string field specification + */ + public static function generateAlter( + $oldcol, + $newcol, + $type, + $length, + $attribute, + $collation, + $null, + $default_type, + $default_value, + $extra, + $comment, + $virtuality, + $expression, + $move_to, + $columns_with_index = null + ) { + return Util::backquote($oldcol) . ' ' + . self::generateFieldSpec( + $newcol, + $type, + $length, + $attribute, + $collation, + $null, + $default_type, + $default_value, + $extra, + $comment, + $virtuality, + $expression, + $move_to, + $columns_with_index, + $oldcol + ); + } // end function + + /** + * Inserts existing entries in a PMA_* table by reading a value from an old + * entry + * + * @param string $work The array index, which Relation feature to + * check ('relwork', 'commwork', ...) + * @param string $pma_table The array index, which PMA-table to update + * ('bookmark', 'relation', ...) + * @param array $get_fields Which fields will be SELECT'ed from the old entry + * @param array $where_fields Which fields will be used for the WHERE query + * (array('FIELDNAME' => 'FIELDVALUE')) + * @param array $new_fields Which fields will be used as new VALUES. + * These are the important keys which differ + * from the old entry + * (array('FIELDNAME' => 'NEW FIELDVALUE')) + * + * @global relation variable + * + * @return int|boolean + */ + public static function duplicateInfo( + $work, + $pma_table, + array $get_fields, + array $where_fields, + array $new_fields + ) { + /** @var DatabaseInterface $dbi */ + $dbi = $GLOBALS['dbi']; + $relation = new Relation($dbi); + $last_id = -1; + + if (! isset($GLOBALS['cfgRelation']) || ! $GLOBALS['cfgRelation'][$work]) { + return true; + } + + $select_parts = []; + $row_fields = []; + foreach ($get_fields as $get_field) { + $select_parts[] = Util::backquote($get_field); + $row_fields[$get_field] = 'cc'; + } + + $where_parts = []; + foreach ($where_fields as $_where => $_value) { + $where_parts[] = Util::backquote($_where) . ' = \'' + . $dbi->escapeString((string) $_value) . '\''; + } + + $new_parts = []; + $new_value_parts = []; + foreach ($new_fields as $_where => $_value) { + $new_parts[] = Util::backquote($_where); + $new_value_parts[] = $dbi->escapeString((string) $_value); + } + + $table_copy_query = ' + SELECT ' . implode(', ', $select_parts) . ' + FROM ' . Util::backquote($GLOBALS['cfgRelation']['db']) . '.' + . Util::backquote($GLOBALS['cfgRelation'][$pma_table]) . ' + WHERE ' . implode(' AND ', $where_parts); + + // must use DatabaseInterface::QUERY_STORE here, since we execute + // another query inside the loop + $table_copy_rs = $relation->queryAsControlUser( + $table_copy_query, + true, + DatabaseInterface::QUERY_STORE + ); + + while ($table_copy_row = @$dbi->fetchAssoc($table_copy_rs)) { + $value_parts = []; + foreach ($table_copy_row as $_key => $_val) { + if (isset($row_fields[$_key]) && $row_fields[$_key] == 'cc') { + $value_parts[] = $dbi->escapeString($_val); + } + } + + $new_table_query = 'INSERT IGNORE INTO ' + . Util::backquote($GLOBALS['cfgRelation']['db']) + . '.' . Util::backquote($GLOBALS['cfgRelation'][$pma_table]) + . ' (' . implode(', ', $select_parts) . ', ' + . implode(', ', $new_parts) . ') VALUES (\'' + . implode('\', \'', $value_parts) . '\', \'' + . implode('\', \'', $new_value_parts) . '\')'; + + $relation->queryAsControlUser($new_table_query); + $last_id = $dbi->insertId(); + } // end while + + $dbi->freeResult($table_copy_rs); + + return $last_id; + } // end of 'Table::duplicateInfo()' function + + /** + * Copies or renames table + * + * @param string $source_db source database + * @param string $source_table source table + * @param string|null $target_db target database + * @param string $target_table target table + * @param string $what what to be moved or copied (data, dataonly) + * @param bool $move whether to move + * @param string $mode mode + * + * @return bool true if success, false otherwise + */ + public static function moveCopy( + $source_db, + $source_table, + ?string $target_db, + $target_table, + $what, + $move, + $mode + ) { + global $err_url; + /** @var DatabaseInterface $dbi */ + $dbi = $GLOBALS['dbi']; + + $relation = new Relation($dbi); + + // Try moving the tables directly, using native `RENAME` statement. + if ($move && $what == 'data') { + $tbl = new Table($source_table, $source_db); + if ($tbl->rename($target_table, $target_db)) { + $GLOBALS['message'] = $tbl->getLastMessage(); + return true; + } + } + + // Setting required export settings. + $GLOBALS['sql_backquotes'] = 1; + $GLOBALS['asfile'] = 1; + + // Ensuring the target database is valid. + if (! $GLOBALS['dblist']->databases->exists($source_db, $target_db)) { + if (! $GLOBALS['dblist']->databases->exists($source_db)) { + $GLOBALS['message'] = Message::rawError( + sprintf( + __('Source database `%s` was not found!'), + htmlspecialchars($source_db) + ) + ); + } + if (! $GLOBALS['dblist']->databases->exists($target_db)) { + $GLOBALS['message'] = Message::rawError( + sprintf( + __('Target database `%s` was not found!'), + htmlspecialchars($target_db) + ) + ); + } + return false; + } + + /** + * The full name of source table, quoted. + * @var string $source + */ + $source = Util::backquote($source_db) + . '.' . Util::backquote($source_table); + + // If the target database is not specified, the operation is taking + // place in the same database. + if (! isset($target_db) || strlen($target_db) === 0) { + $target_db = $source_db; + } + + // Selecting the database could avoid some problems with replicated + // databases, when moving table from replicated one to not replicated one. + $dbi->selectDb($target_db); + + /** + * The full name of target table, quoted. + * @var string $target + */ + $target = Util::backquote($target_db) + . '.' . Util::backquote($target_table); + + // No table is created when this is a data-only operation. + if ($what != 'dataonly') { + /** + * Instance used for exporting the current structure of the table. + * + * @var ExportSql $export_sql_plugin + */ + $export_sql_plugin = Plugins::getPlugin( + "export", + "sql", + 'libraries/classes/Plugins/Export/', + [ + 'export_type' => 'table', + 'single_table' => false, + ] + ); + + $no_constraints_comments = true; + $GLOBALS['sql_constraints_query'] = ''; + // set the value of global sql_auto_increment variable + if (isset($_POST['sql_auto_increment'])) { + $GLOBALS['sql_auto_increment'] = $_POST['sql_auto_increment']; + } + + /** + * The old structure of the table.. + * @var string $sql_structure + */ + $sql_structure = $export_sql_plugin->getTableDef( + $source_db, + $source_table, + "\n", + $err_url, + false, + false + ); + + unset($no_constraints_comments); + + // ----------------------------------------------------------------- + // Phase 0: Preparing structures used. + + /** + * The destination where the table is moved or copied to. + * @var Expression + */ + $destination = new Expression( + $target_db, + $target_table, + '' + ); + + // Find server's SQL mode so the builder can generate correct + // queries. + // One of the options that alters the behaviour is `ANSI_QUOTES`. + Context::setMode( + $dbi->fetchValue("SELECT @@sql_mode") + ); + + // ----------------------------------------------------------------- + // Phase 1: Dropping existent element of the same name (if exists + // and required). + + if (isset($_POST['drop_if_exists']) + && $_POST['drop_if_exists'] == 'true' + ) { + + /** + * Drop statement used for building the query. + * @var DropStatement $statement + */ + $statement = new DropStatement(); + + $tbl = new Table($target_db, $target_table); + + $statement->options = new OptionsArray( + [ + $tbl->isView() ? 'VIEW' : 'TABLE', + 'IF EXISTS', + ] + ); + + $statement->fields = [$destination]; + + // Building the query. + $drop_query = $statement->build() . ';'; + + // Executing it. + $dbi->query($drop_query); + $GLOBALS['sql_query'] .= "\n" . $drop_query; + + // If an existing table gets deleted, maintain any entries for + // the PMA_* tables. + $maintain_relations = true; + } + + // ----------------------------------------------------------------- + // Phase 2: Generating the new query of this structure. + + /** + * The parser responsible for parsing the old queries. + * @var Parser $parser + */ + $parser = new Parser($sql_structure); + + if (! empty($parser->statements[0])) { + + /** + * The CREATE statement of this structure. + * @var CreateStatement $statement + */ + $statement = $parser->statements[0]; + + // Changing the destination. + $statement->name = $destination; + + // Building back the query. + $sql_structure = $statement->build() . ';'; + + // Executing it. + $dbi->query($sql_structure); + $GLOBALS['sql_query'] .= "\n" . $sql_structure; + } + + // ----------------------------------------------------------------- + // Phase 3: Adding constraints. + // All constraint names are removed because they must be unique. + + if (($move || isset($GLOBALS['add_constraints'])) + && ! empty($GLOBALS['sql_constraints_query']) + ) { + $parser = new Parser($GLOBALS['sql_constraints_query']); + + /** + * The ALTER statement that generates the constraints. + * @var AlterStatement $statement + */ + $statement = $parser->statements[0]; + + // Changing the altered table to the destination. + $statement->table = $destination; + + // Removing the name of the constraints. + foreach ($statement->altered as $idx => $altered) { + // All constraint names are removed because they must be unique. + if ($altered->options->has('CONSTRAINT')) { + $altered->field = null; + } + } + + // Building back the query. + $GLOBALS['sql_constraints_query'] = $statement->build() . ';'; + + // Executing it. + if ($mode == 'one_table') { + $dbi->query($GLOBALS['sql_constraints_query']); + } + $GLOBALS['sql_query'] .= "\n" . $GLOBALS['sql_constraints_query']; + if ($mode == 'one_table') { + unset($GLOBALS['sql_constraints_query']); + } + } + + // ----------------------------------------------------------------- + // Phase 4: Adding indexes. + // View phase 3. + + if (! empty($GLOBALS['sql_indexes'])) { + $parser = new Parser($GLOBALS['sql_indexes']); + + $GLOBALS['sql_indexes'] = ''; + /** + * The ALTER statement that generates the indexes. + * @var AlterStatement $statement + */ + foreach ($parser->statements as $statement) { + // Changing the altered table to the destination. + $statement->table = $destination; + + // Removing the name of the constraints. + foreach ($statement->altered as $idx => $altered) { + // All constraint names are removed because they must be unique. + if ($altered->options->has('CONSTRAINT')) { + $altered->field = null; + } + } + + // Building back the query. + $sql_index = $statement->build() . ';'; + + // Executing it. + if ($mode == 'one_table' || $mode == 'db_copy') { + $dbi->query($sql_index); + } + + $GLOBALS['sql_indexes'] .= $sql_index; + } + + $GLOBALS['sql_query'] .= "\n" . $GLOBALS['sql_indexes']; + if ($mode == 'one_table' || $mode == 'db_copy') { + unset($GLOBALS['sql_indexes']); + } + } + + // ----------------------------------------------------------------- + // Phase 5: Adding AUTO_INCREMENT. + + if (! empty($GLOBALS['sql_auto_increments'])) { + if ($mode == 'one_table' || $mode == 'db_copy') { + $parser = new Parser($GLOBALS['sql_auto_increments']); + + /** + * The ALTER statement that alters the AUTO_INCREMENT value. + * @var AlterStatement $statement + */ + $statement = $parser->statements[0]; + + // Changing the altered table to the destination. + $statement->table = $destination; + + // Building back the query. + $GLOBALS['sql_auto_increments'] = $statement->build() . ';'; + + // Executing it. + $dbi->query($GLOBALS['sql_auto_increments']); + $GLOBALS['sql_query'] .= "\n" . $GLOBALS['sql_auto_increments']; + unset($GLOBALS['sql_auto_increments']); + } + } + } else { + $GLOBALS['sql_query'] = ''; + } + + $_table = new Table($target_table, $target_db); + // Copy the data unless this is a VIEW + if (($what == 'data' || $what == 'dataonly') + && ! $_table->isView() + ) { + $sql_set_mode = "SET SQL_MODE='NO_AUTO_VALUE_ON_ZERO'"; + $dbi->query($sql_set_mode); + $GLOBALS['sql_query'] .= "\n\n" . $sql_set_mode . ';'; + + $_old_table = new Table($source_table, $source_db); + $nonGeneratedCols = $_old_table->getNonGeneratedColumns(true); + if (count($nonGeneratedCols) > 0) { + $sql_insert_data = 'INSERT INTO ' . $target . '(' + . implode(', ', $nonGeneratedCols) + . ') SELECT ' . implode(', ', $nonGeneratedCols) + . ' FROM ' . $source; + + $dbi->query($sql_insert_data); + $GLOBALS['sql_query'] .= "\n\n" . $sql_insert_data . ';'; + } + } + + $relation->getRelationsParam(); + + // Drops old table if the user has requested to move it + if ($move) { + // This could avoid some problems with replicated databases, when + // moving table from replicated one to not replicated one + $dbi->selectDb($source_db); + + $_source_table = new Table($source_table, $source_db); + if ($_source_table->isView()) { + $sql_drop_query = 'DROP VIEW'; + } else { + $sql_drop_query = 'DROP TABLE'; + } + $sql_drop_query .= ' ' . $source; + $dbi->query($sql_drop_query); + + // Renable table in configuration storage + $relation->renameTable( + $source_db, + $target_db, + $source_table, + $target_table + ); + + $GLOBALS['sql_query'] .= "\n\n" . $sql_drop_query . ';'; + // end if ($move) + return true; + } + + // we are copying + // Create new entries as duplicates from old PMA DBs + if ($what == 'dataonly' || isset($maintain_relations)) { + return true; + } + + if ($GLOBALS['cfgRelation']['commwork']) { + // Get all comments and MIME-Types for current table + $comments_copy_rs = $relation->queryAsControlUser( + 'SELECT column_name, comment' + . ($GLOBALS['cfgRelation']['mimework'] + ? ', mimetype, transformation, transformation_options' + : '') + . ' FROM ' + . Util::backquote($GLOBALS['cfgRelation']['db']) + . '.' + . Util::backquote($GLOBALS['cfgRelation']['column_info']) + . ' WHERE ' + . ' db_name = \'' + . $dbi->escapeString($source_db) . '\'' + . ' AND ' + . ' table_name = \'' + . $dbi->escapeString((string) $source_table) . '\'' + ); + + // Write every comment as new copied entry. [MIME] + while ($comments_copy_row + = $dbi->fetchAssoc($comments_copy_rs)) { + $new_comment_query = 'REPLACE INTO ' + . Util::backquote($GLOBALS['cfgRelation']['db']) + . '.' . Util::backquote( + $GLOBALS['cfgRelation']['column_info'] + ) + . ' (db_name, table_name, column_name, comment' + . ($GLOBALS['cfgRelation']['mimework'] + ? ', mimetype, transformation, transformation_options' + : '') + . ') VALUES(\'' . $dbi->escapeString($target_db) + . '\',\'' . $dbi->escapeString($target_table) . '\',\'' + . $dbi->escapeString($comments_copy_row['column_name']) + . '\',\'' + . $dbi->escapeString($comments_copy_row['comment']) + . '\'' + . ($GLOBALS['cfgRelation']['mimework'] + ? ',\'' . $dbi->escapeString( + $comments_copy_row['mimetype'] + ) + . '\',\'' . $dbi->escapeString( + $comments_copy_row['transformation'] + ) + . '\',\'' . $dbi->escapeString( + $comments_copy_row['transformation_options'] + ) + . '\'' + : '') + . ')'; + $relation->queryAsControlUser($new_comment_query); + } // end while + $dbi->freeResult($comments_copy_rs); + unset($comments_copy_rs); + } + + // duplicating the bookmarks must not be done here, but + // just once per db + + $get_fields = ['display_field']; + $where_fields = [ + 'db_name' => $source_db, + 'table_name' => $source_table, + ]; + $new_fields = [ + 'db_name' => $target_db, + 'table_name' => $target_table, + ]; + self::duplicateInfo( + 'displaywork', + 'table_info', + $get_fields, + $where_fields, + $new_fields + ); + + /** + * @todo revise this code when we support cross-db relations + */ + $get_fields = [ + 'master_field', + 'foreign_table', + 'foreign_field', + ]; + $where_fields = [ + 'master_db' => $source_db, + 'master_table' => $source_table, + ]; + $new_fields = [ + 'master_db' => $target_db, + 'foreign_db' => $target_db, + 'master_table' => $target_table, + ]; + self::duplicateInfo( + 'relwork', + 'relation', + $get_fields, + $where_fields, + $new_fields + ); + + $get_fields = [ + 'foreign_field', + 'master_table', + 'master_field', + ]; + $where_fields = [ + 'foreign_db' => $source_db, + 'foreign_table' => $source_table, + ]; + $new_fields = [ + 'master_db' => $target_db, + 'foreign_db' => $target_db, + 'foreign_table' => $target_table, + ]; + self::duplicateInfo( + 'relwork', + 'relation', + $get_fields, + $where_fields, + $new_fields + ); + + /** + * @todo Can't get duplicating PDFs the right way. The + * page numbers always get screwed up independently from + * duplication because the numbers do not seem to be stored on a + * per-database basis. Would the author of pdf support please + * have a look at it? + * + $get_fields = array('page_descr'); + $where_fields = array('db_name' => $source_db); + $new_fields = array('db_name' => $target_db); + $last_id = self::duplicateInfo( + 'pdfwork', + 'pdf_pages', + $get_fields, + $where_fields, + $new_fields + ); + + if (isset($last_id) && $last_id >= 0) { + $get_fields = array('x', 'y'); + $where_fields = array( + 'db_name' => $source_db, + 'table_name' => $source_table + ); + $new_fields = array( + 'db_name' => $target_db, + 'table_name' => $target_table, + 'pdf_page_number' => $last_id + ); + self::duplicateInfo( + 'pdfwork', + 'table_coords', + $get_fields, + $where_fields, + $new_fields + ); + } + */ + + return true; + } + + /** + * checks if given name is a valid table name, + * currently if not empty, trailing spaces, '.', '/' and '\' + * + * @param string $table_name name to check + * @param boolean $is_backquoted whether this name is used inside backquotes or not + * + * @todo add check for valid chars in filename on current system/os + * @see https://dev.mysql.com/doc/refman/5.0/en/legal-names.html + * + * @return boolean whether the string is valid or not + */ + public static function isValidName($table_name, $is_backquoted = false) + { + if ($table_name !== rtrim((string) $table_name)) { + // trailing spaces not allowed even in backquotes + return false; + } + + if (strlen($table_name) === 0) { + // zero length + return false; + } + + if (! $is_backquoted && $table_name !== trim($table_name)) { + // spaces at the start or in between only allowed inside backquotes + return false; + } + + if (! $is_backquoted && preg_match('/^[a-zA-Z0-9_$]+$/', $table_name)) { + // only allow the above regex in unquoted identifiers + // see : https://dev.mysql.com/doc/refman/5.7/en/identifiers.html + return true; + } elseif ($is_backquoted) { + // If backquoted, all characters should be allowed (except w/ trailing spaces) + return true; + } + + // If not backquoted and doesn't follow the above regex + return false; + } + + /** + * renames table + * + * @param string $new_name new table name + * @param string $new_db new database name + * + * @return bool success + */ + public function rename($new_name, $new_db = null) + { + if ($this->_dbi->getLowerCaseNames() === '1') { + $new_name = strtolower($new_name); + } + + if (null !== $new_db && $new_db !== $this->getDbName()) { + // Ensure the target is valid + if (! $GLOBALS['dblist']->databases->exists($new_db)) { + $this->errors[] = __('Invalid database:') . ' ' . $new_db; + return false; + } + } else { + $new_db = $this->getDbName(); + } + + $new_table = new Table($new_name, $new_db); + + if ($this->getFullName() === $new_table->getFullName()) { + return true; + } + + // Allow whitespaces (not trailing) in $new_name, + // since we are using $backquoted in getting the fullName of table + // below to be used in the query + if (! self::isValidName($new_name, true)) { + $this->errors[] = __('Invalid table name:') . ' ' + . $new_table->getFullName(); + return false; + } + + // If the table is moved to a different database drop its triggers first + $triggers = $this->_dbi->getTriggers( + $this->getDbName(), + $this->getName(), + '' + ); + $handle_triggers = $this->getDbName() != $new_db && $triggers; + if ($handle_triggers) { + foreach ($triggers as $trigger) { + $sql = 'DROP TRIGGER IF EXISTS ' + . Util::backquote($this->getDbName()) + . '.' . Util::backquote($trigger['name']) . ';'; + $this->_dbi->query($sql); + } + } + + /* + * tested also for a view, in MySQL 5.0.92, 5.1.55 and 5.5.13 + */ + $GLOBALS['sql_query'] = ' + RENAME TABLE ' . $this->getFullName(true) . ' + TO ' . $new_table->getFullName(true) . ';'; + // I don't think a specific error message for views is necessary + if (! $this->_dbi->query($GLOBALS['sql_query'])) { + // Restore triggers in the old database + if ($handle_triggers) { + $this->_dbi->selectDb($this->getDbName()); + foreach ($triggers as $trigger) { + $this->_dbi->query($trigger['create']); + } + } + $this->errors[] = sprintf( + __('Failed to rename table %1$s to %2$s!'), + $this->getFullName(), + $new_table->getFullName() + ); + return false; + } + + $old_name = $this->getName(); + $old_db = $this->getDbName(); + $this->_name = $new_name; + $this->_db_name = $new_db; + + // Renable table in configuration storage + $this->relation->renameTable( + $old_db, + $new_db, + $old_name, + $new_name + ); + + $this->messages[] = sprintf( + __('Table %1$s has been renamed to %2$s.'), + htmlspecialchars($old_name), + htmlspecialchars($new_name) + ); + return true; + } + + /** + * Get all unique columns + * + * returns an array with all columns with unique content, in fact these are + * all columns being single indexed in PRIMARY or UNIQUE + * + * e.g. + * - PRIMARY(id) // id + * - UNIQUE(name) // name + * - PRIMARY(fk_id1, fk_id2) // NONE + * - UNIQUE(x,y) // NONE + * + * @param bool $backquoted whether to quote name with backticks `` + * @param bool $fullName whether to include full name of the table as a prefix + * + * @return array + */ + public function getUniqueColumns($backquoted = true, $fullName = true) + { + $sql = $this->_dbi->getTableIndexesSql( + $this->getDbName(), + $this->getName(), + 'Non_unique = 0' + ); + $uniques = $this->_dbi->fetchResult( + $sql, + [ + 'Key_name', + null, + ], + 'Column_name' + ); + + $return = []; + foreach ($uniques as $index) { + if (count($index) > 1) { + continue; + } + if ($fullName) { + $possible_column = $this->getFullName($backquoted) . '.'; + } else { + $possible_column = ''; + } + if ($backquoted) { + $possible_column .= Util::backquote($index[0]); + } else { + $possible_column .= $index[0]; + } + // a column might have a primary and an unique index on it + if (! in_array($possible_column, $return)) { + $return[] = $possible_column; + } + } + + return $return; + } + + /** + * Formats lists of columns + * + * returns an array with all columns that make use of an index + * + * e.g. index(col1, col2) would return col1, col2 + * + * @param array $indexed column data + * @param bool $backquoted whether to quote name with backticks `` + * @param bool $fullName whether to include full name of the table as a prefix + * + * @return array + */ + private function _formatColumns(array $indexed, $backquoted, $fullName) + { + $return = []; + foreach ($indexed as $column) { + $return[] = ($fullName ? $this->getFullName($backquoted) . '.' : '') + . ($backquoted ? Util::backquote($column) : $column); + } + + return $return; + } + + /** + * Get all indexed columns + * + * returns an array with all columns that make use of an index + * + * e.g. index(col1, col2) would return col1, col2 + * + * @param bool $backquoted whether to quote name with backticks `` + * @param bool $fullName whether to include full name of the table as a prefix + * + * @return array + */ + public function getIndexedColumns($backquoted = true, $fullName = true) + { + $sql = $this->_dbi->getTableIndexesSql( + $this->getDbName(), + $this->getName(), + '' + ); + $indexed = $this->_dbi->fetchResult($sql, 'Column_name', 'Column_name'); + + return $this->_formatColumns($indexed, $backquoted, $fullName); + } + + /** + * Get all columns + * + * returns an array with all columns + * + * @param bool $backquoted whether to quote name with backticks `` + * @param bool $fullName whether to include full name of the table as a prefix + * + * @return array + */ + public function getColumns($backquoted = true, $fullName = true) + { + $sql = 'SHOW COLUMNS FROM ' . $this->getFullName(true); + $indexed = $this->_dbi->fetchResult($sql, 'Field', 'Field'); + + return $this->_formatColumns($indexed, $backquoted, $fullName); + } + + /** + * Get meta info for fields in table + * + * @return mixed + */ + public function getColumnsMeta() + { + $move_columns_sql_query = sprintf( + 'SELECT * FROM %s.%s LIMIT 1', + Util::backquote($this->_db_name), + Util::backquote($this->_name) + ); + $move_columns_sql_result = $this->_dbi->tryQuery($move_columns_sql_query); + if ($move_columns_sql_result !== false) { + return $this->_dbi->getFieldsMeta($move_columns_sql_result); + } else { + // unsure how to reproduce but it was seen on the reporting server + return []; + } + } + + /** + * Get non-generated columns in table + * + * @param bool $backquoted whether to quote name with backticks `` + * + * @return array + */ + public function getNonGeneratedColumns($backquoted = true) + { + $columns_meta_query = 'SHOW COLUMNS FROM ' . $this->getFullName(true); + $ret = []; + + $columns_meta_query_result = $this->_dbi->fetchResult( + $columns_meta_query + ); + + if ($columns_meta_query_result + && $columns_meta_query_result !== false + ) { + foreach ($columns_meta_query_result as $column) { + $value = $column['Field']; + if ($backquoted === true) { + $value = Util::backquote($value); + } + + if (( + strpos($column['Extra'], 'GENERATED') === false + && strpos($column['Extra'], 'VIRTUAL') === false + ) || $column['Extra'] === 'DEFAULT_GENERATED') { + $ret[] = $value; + } + } + } + + return $ret; + } + + /** + * Return UI preferences for this table from phpMyAdmin database. + * + * @return array + */ + protected function getUiPrefsFromDb() + { + $cfgRelation = $this->relation->getRelationsParam(); + $pma_table = Util::backquote($cfgRelation['db']) . "." + . Util::backquote($cfgRelation['table_uiprefs']); + + // Read from phpMyAdmin database + $sql_query = " SELECT `prefs` FROM " . $pma_table + . " WHERE `username` = '" . $this->_dbi->escapeString($GLOBALS['cfg']['Server']['user']) . "'" + . " AND `db_name` = '" . $this->_dbi->escapeString($this->_db_name) . "'" + . " AND `table_name` = '" . $this->_dbi->escapeString($this->_name) . "'"; + + $row = $this->_dbi->fetchArray($this->relation->queryAsControlUser($sql_query)); + if (isset($row[0])) { + return json_decode($row[0], true); + } + + return []; + } + + /** + * Save this table's UI preferences into phpMyAdmin database. + * + * @return true|Message + */ + protected function saveUiPrefsToDb() + { + $cfgRelation = $this->relation->getRelationsParam(); + $pma_table = Util::backquote($cfgRelation['db']) . "." + . Util::backquote($cfgRelation['table_uiprefs']); + + $secureDbName = $this->_dbi->escapeString($this->_db_name); + + $username = $GLOBALS['cfg']['Server']['user']; + $sql_query = " REPLACE INTO " . $pma_table + . " (username, db_name, table_name, prefs) VALUES ('" + . $this->_dbi->escapeString($username) . "', '" . $secureDbName + . "', '" . $this->_dbi->escapeString($this->_name) . "', '" + . $this->_dbi->escapeString(json_encode($this->uiprefs)) . "')"; + + $success = $this->_dbi->tryQuery($sql_query, DatabaseInterface::CONNECT_CONTROL); + + if (! $success) { + $message = Message::error( + __('Could not save table UI preferences!') + ); + $message->addMessage( + Message::rawError( + $this->_dbi->getError(DatabaseInterface::CONNECT_CONTROL) + ), + '

    ' + ); + return $message; + } + + // Remove some old rows in table_uiprefs if it exceeds the configured + // maximum rows + $sql_query = 'SELECT COUNT(*) FROM ' . $pma_table; + $rows_count = $this->_dbi->fetchValue($sql_query); + $max_rows = $GLOBALS['cfg']['Server']['MaxTableUiprefs']; + if ($rows_count > $max_rows) { + $num_rows_to_delete = $rows_count - $max_rows; + $sql_query + = ' DELETE FROM ' . $pma_table . + ' ORDER BY last_update ASC' . + ' LIMIT ' . $num_rows_to_delete; + $success = $this->_dbi->tryQuery( + $sql_query, + DatabaseInterface::CONNECT_CONTROL + ); + + if (! $success) { + $message = Message::error( + sprintf( + __( + 'Failed to cleanup table UI preferences (see ' . + '$cfg[\'Servers\'][$i][\'MaxTableUiprefs\'] %s)' + ), + Util::showDocu('config', 'cfg_Servers_MaxTableUiprefs') + ) + ); + $message->addMessage( + Message::rawError( + $this->_dbi->getError(DatabaseInterface::CONNECT_CONTROL) + ), + '

    ' + ); + return $message; + } + } + + return true; + } + + /** + * Loads the UI preferences for this table. + * If pmadb and table_uiprefs is set, it will load the UI preferences from + * phpMyAdmin database. + * + * @return void + */ + protected function loadUiPrefs() + { + $cfgRelation = $this->relation->getRelationsParam(); + $server_id = $GLOBALS['server']; + + // set session variable if it's still undefined + if (! isset($_SESSION['tmpval']['table_uiprefs'][$server_id][$this->_db_name][$this->_name])) { + // check whether we can get from pmadb + $_SESSION['tmpval']['table_uiprefs'][$server_id][$this->_db_name][$this->_name] = $cfgRelation['uiprefswork'] + ? $this->getUiPrefsFromDb() + : []; + } + $this->uiprefs =& $_SESSION['tmpval']['table_uiprefs'][$server_id][$this->_db_name][$this->_name]; + } + + /** + * Get a property from UI preferences. + * Return false if the property is not found. + * Available property: + * - PROP_SORTED_COLUMN + * - PROP_COLUMN_ORDER + * - PROP_COLUMN_VISIB + * + * @param string $property property + * + * @return mixed + */ + public function getUiProp($property) + { + if (! isset($this->uiprefs)) { + $this->loadUiPrefs(); + } + + // do checking based on property + if ($property == self::PROP_SORTED_COLUMN) { + if (! isset($this->uiprefs[$property])) { + return false; + } + + if (! isset($_POST['discard_remembered_sort'])) { + // check if the column name exists in this table + $tmp = explode(' ', $this->uiprefs[$property]); + $colname = $tmp[0]; + //remove backquoting from colname + $colname = str_replace('`', '', $colname); + //get the available column name without backquoting + $avail_columns = $this->getColumns(false); + + foreach ($avail_columns as $each_col) { + // check if $each_col ends with $colname + if (substr_compare( + $each_col, + $colname, + mb_strlen($each_col) - mb_strlen($colname) + ) === 0 + ) { + return $this->uiprefs[$property]; + } + } + } + // remove the property, since it no longer exists in database + $this->removeUiProp($property); + return false; + } + + if ($property == self::PROP_COLUMN_ORDER + || $property == self::PROP_COLUMN_VISIB + ) { + if ($this->isView() || ! isset($this->uiprefs[$property])) { + return false; + } + + // check if the table has not been modified + if ($this->getStatusInfo('Create_time') == $this->uiprefs['CREATE_TIME'] + ) { + return array_map('intval', $this->uiprefs[$property]); + } + + // remove the property, since the table has been modified + $this->removeUiProp($property); + return false; + } + + // default behaviour for other property: + return isset($this->uiprefs[$property]) ? $this->uiprefs[$property] : false; + } + + /** + * Set a property from UI preferences. + * If pmadb and table_uiprefs is set, it will save the UI preferences to + * phpMyAdmin database. + * Available property: + * - PROP_SORTED_COLUMN + * - PROP_COLUMN_ORDER + * - PROP_COLUMN_VISIB + * + * @param string $property Property + * @param mixed $value Value for the property + * @param string $table_create_time Needed for PROP_COLUMN_ORDER + * and PROP_COLUMN_VISIB + * + * @return boolean|Message + */ + public function setUiProp($property, $value, $table_create_time = null) + { + if (! isset($this->uiprefs)) { + $this->loadUiPrefs(); + } + // we want to save the create time if the property is PROP_COLUMN_ORDER + if (! $this->isView() + && ($property == self::PROP_COLUMN_ORDER + || $property == self::PROP_COLUMN_VISIB) + ) { + $curr_create_time = $this->getStatusInfo('CREATE_TIME'); + if (isset($table_create_time) + && $table_create_time == $curr_create_time + ) { + $this->uiprefs['CREATE_TIME'] = $curr_create_time; + } else { + // there is no $table_create_time, or + // supplied $table_create_time is older than current create time, + // so don't save + return Message::error( + sprintf( + __( + 'Cannot save UI property "%s". The changes made will ' . + 'not be persistent after you refresh this page. ' . + 'Please check if the table structure has been changed.' + ), + $property + ) + ); + } + } + // save the value + $this->uiprefs[$property] = $value; + + // check if pmadb is set + $cfgRelation = $this->relation->getRelationsParam(); + if ($cfgRelation['uiprefswork']) { + return $this->saveUiPrefsToDb(); + } + return true; + } + + /** + * Remove a property from UI preferences. + * + * @param string $property the property + * + * @return true|Message + */ + public function removeUiProp($property) + { + if (! isset($this->uiprefs)) { + $this->loadUiPrefs(); + } + if (isset($this->uiprefs[$property])) { + unset($this->uiprefs[$property]); + + // check if pmadb is set + $cfgRelation = $this->relation->getRelationsParam(); + if ($cfgRelation['uiprefswork']) { + return $this->saveUiPrefsToDb(); + } + } + return true; + } + + /** + * Get all column names which are MySQL reserved words + * + * @return array + * @access public + */ + public function getReservedColumnNames() + { + $columns = $this->getColumns(false); + $return = []; + foreach ($columns as $column) { + $temp = explode('.', $column); + $column_name = $temp[2]; + if (Context::isKeyword($column_name, true)) { + $return[] = $column_name; + } + } + return $return; + } + + /** + * Function to get the name and type of the columns of a table + * + * @return array + */ + public function getNameAndTypeOfTheColumns() + { + $columns = []; + foreach ($this->_dbi->getColumnsFull( + $this->_db_name, + $this->_name + ) as $row) { + if (preg_match('@^(set|enum)\((.+)\)$@i', $row['Type'], $tmp)) { + $tmp[2] = mb_substr( + preg_replace('@([^,])\'\'@', '\\1\\\'', ',' . $tmp[2]), + 1 + ); + $columns[$row['Field']] = $tmp[1] . '(' + . str_replace(',', ', ', $tmp[2]) . ')'; + } else { + $columns[$row['Field']] = $row['Type']; + } + } + return $columns; + } + + /** + * Get index with index name + * + * @param string $index Index name + * + * @return Index + */ + public function getIndex($index) + { + return Index::singleton($this->_db_name, $this->_name, $index); + } + + /** + * Function to get the sql query for index creation or edit + * + * @param Index $index current index + * @param bool $error whether error occurred or not + * + * @return string + */ + public function getSqlQueryForIndexCreateOrEdit($index, &$error) + { + // $sql_query is the one displayed in the query box + $sql_query = sprintf( + 'ALTER TABLE %s.%s', + Util::backquote($this->_db_name), + Util::backquote($this->_name) + ); + + // Drops the old index + if (! empty($_POST['old_index'])) { + if ($_POST['old_index'] == 'PRIMARY') { + $sql_query .= ' DROP PRIMARY KEY,'; + } else { + $sql_query .= sprintf( + ' DROP INDEX %s,', + Util::backquote($_POST['old_index']) + ); + } + } // end if + + // Builds the new one + switch ($index->getChoice()) { + case 'PRIMARY': + if ($index->getName() == '') { + $index->setName('PRIMARY'); + } elseif ($index->getName() != 'PRIMARY') { + $error = Message::error( + __('The name of the primary key must be "PRIMARY"!') + ); + } + $sql_query .= ' ADD PRIMARY KEY'; + break; + case 'FULLTEXT': + case 'UNIQUE': + case 'INDEX': + case 'SPATIAL': + if ($index->getName() == 'PRIMARY') { + $error = Message::error( + __('Can\'t rename index to PRIMARY!') + ); + } + $sql_query .= sprintf( + ' ADD %s ', + $index->getChoice() + ); + if ($index->getName()) { + $sql_query .= Util::backquote($index->getName()); + } + break; + } // end switch + + $index_fields = []; + foreach ($index->getColumns() as $key => $column) { + $index_fields[$key] = Util::backquote($column->getName()); + if ($column->getSubPart()) { + $index_fields[$key] .= '(' . $column->getSubPart() . ')'; + } + } // end while + + if (empty($index_fields)) { + $error = Message::error(__('No index parts defined!')); + } else { + $sql_query .= ' (' . implode(', ', $index_fields) . ')'; + } + + $keyBlockSizes = $index->getKeyBlockSize(); + if (! empty($keyBlockSizes)) { + $sql_query .= sprintf( + ' KEY_BLOCK_SIZE = %s', + $this->_dbi->escapeString($keyBlockSizes) + ); + } + + // specifying index type is allowed only for primary, unique and index only + // TokuDB is using Fractal Tree, Using Type is not useless + // Ref: https://mariadb.com/kb/en/mariadb/storage-engine-index-types/ + $type = $index->getType(); + if ($index->getChoice() != 'SPATIAL' + && $index->getChoice() != 'FULLTEXT' + && in_array($type, Index::getIndexTypes()) + && ! $this->isEngine(['TOKUDB']) + ) { + $sql_query .= ' USING ' . $type; + } + + $parser = $index->getParser(); + if ($index->getChoice() == 'FULLTEXT' && ! empty($parser)) { + $sql_query .= ' WITH PARSER ' . $this->_dbi->escapeString($parser); + } + + $comment = $index->getComment(); + if (! empty($comment)) { + $sql_query .= sprintf( + " COMMENT '%s'", + $this->_dbi->escapeString($comment) + ); + } + + $sql_query .= ';'; + + return $sql_query; + } + + /** + * Function to handle update for display field + * + * @param string $display_field display field + * @param array $cfgRelation configuration relation + * + * @return boolean True on update succeed or False on failure + */ + public function updateDisplayField($display_field, array $cfgRelation) + { + $upd_query = false; + if ($display_field == '') { + $upd_query = 'DELETE FROM ' + . Util::backquote($GLOBALS['cfgRelation']['db']) + . '.' . Util::backquote($cfgRelation['table_info']) + . ' WHERE db_name = \'' + . $this->_dbi->escapeString($this->_db_name) . '\'' + . ' AND table_name = \'' + . $this->_dbi->escapeString($this->_name) . '\''; + } else { + $upd_query = 'REPLACE INTO ' + . Util::backquote($GLOBALS['cfgRelation']['db']) + . '.' . Util::backquote($cfgRelation['table_info']) + . '(db_name, table_name, display_field) VALUES(' + . '\'' . $this->_dbi->escapeString($this->_db_name) . '\',' + . '\'' . $this->_dbi->escapeString($this->_name) . '\',' + . '\'' . $this->_dbi->escapeString($display_field) . '\')'; + } + + if ($upd_query) { + $this->_dbi->query( + $upd_query, + DatabaseInterface::CONNECT_CONTROL, + 0, + false + ); + return true; + } + return false; + } + + /** + * Function to get update query for updating internal relations + * + * @param array $multi_edit_columns_name multi edit column names + * @param array $destination_db destination tables + * @param array $destination_table destination tables + * @param array $destination_column destination columns + * @param array $cfgRelation configuration relation + * @param array|null $existrel db, table, column + * + * @return boolean + */ + public function updateInternalRelations( + array $multi_edit_columns_name, + array $destination_db, + array $destination_table, + array $destination_column, + array $cfgRelation, + $existrel + ) { + $updated = false; + foreach ($destination_db as $master_field_md5 => $foreign_db) { + $upd_query = null; + // Map the fieldname's md5 back to its real name + $master_field = $multi_edit_columns_name[$master_field_md5]; + $foreign_table = $destination_table[$master_field_md5]; + $foreign_field = $destination_column[$master_field_md5]; + if (! empty($foreign_db) + && ! empty($foreign_table) + && ! empty($foreign_field) + ) { + if (! isset($existrel[$master_field])) { + $upd_query = 'INSERT INTO ' + . Util::backquote($GLOBALS['cfgRelation']['db']) + . '.' . Util::backquote($cfgRelation['relation']) + . '(master_db, master_table, master_field, foreign_db,' + . ' foreign_table, foreign_field)' + . ' values(' + . '\'' . $this->_dbi->escapeString($this->_db_name) . '\', ' + . '\'' . $this->_dbi->escapeString($this->_name) . '\', ' + . '\'' . $this->_dbi->escapeString($master_field) . '\', ' + . '\'' . $this->_dbi->escapeString($foreign_db) . '\', ' + . '\'' . $this->_dbi->escapeString($foreign_table) . '\',' + . '\'' . $this->_dbi->escapeString($foreign_field) . '\')'; + } elseif ($existrel[$master_field]['foreign_db'] != $foreign_db + || $existrel[$master_field]['foreign_table'] != $foreign_table + || $existrel[$master_field]['foreign_field'] != $foreign_field + ) { + $upd_query = 'UPDATE ' + . Util::backquote($GLOBALS['cfgRelation']['db']) + . '.' . Util::backquote($cfgRelation['relation']) + . ' SET foreign_db = \'' + . $this->_dbi->escapeString($foreign_db) . '\', ' + . ' foreign_table = \'' + . $this->_dbi->escapeString($foreign_table) . '\', ' + . ' foreign_field = \'' + . $this->_dbi->escapeString($foreign_field) . '\' ' + . ' WHERE master_db = \'' + . $this->_dbi->escapeString($this->_db_name) . '\'' + . ' AND master_table = \'' + . $this->_dbi->escapeString($this->_name) . '\'' + . ' AND master_field = \'' + . $this->_dbi->escapeString($master_field) . '\''; + } // end if... else.... + } elseif (isset($existrel[$master_field])) { + $upd_query = 'DELETE FROM ' + . Util::backquote($GLOBALS['cfgRelation']['db']) + . '.' . Util::backquote($cfgRelation['relation']) + . ' WHERE master_db = \'' + . $this->_dbi->escapeString($this->_db_name) . '\'' + . ' AND master_table = \'' + . $this->_dbi->escapeString($this->_name) . '\'' + . ' AND master_field = \'' + . $this->_dbi->escapeString($master_field) . '\''; + } // end if... else.... + + if (isset($upd_query)) { + $this->_dbi->query( + $upd_query, + DatabaseInterface::CONNECT_CONTROL, + 0, + false + ); + $updated = true; + } + } + return $updated; + } + + /** + * Function to handle foreign key updates + * + * @param array $destination_foreign_db destination foreign database + * @param array $multi_edit_columns_name multi edit column names + * @param array $destination_foreign_table destination foreign table + * @param array $destination_foreign_column destination foreign column + * @param array $options_array options array + * @param string $table current table + * @param array $existrel_foreign db, table, column + * + * @return array + */ + public function updateForeignKeys( + array $destination_foreign_db, + array $multi_edit_columns_name, + array $destination_foreign_table, + array $destination_foreign_column, + array $options_array, + $table, + array $existrel_foreign + ) { + $html_output = ''; + $preview_sql_data = ''; + $display_query = ''; + $seen_error = false; + + foreach ($destination_foreign_db as $master_field_md5 => $foreign_db) { + $create = false; + $drop = false; + + // Map the fieldname's md5 back to its real name + $master_field = $multi_edit_columns_name[$master_field_md5]; + + $foreign_table = $destination_foreign_table[$master_field_md5]; + $foreign_field = $destination_foreign_column[$master_field_md5]; + + if (isset($existrel_foreign[$master_field_md5]['ref_db_name'])) { + $ref_db_name = $existrel_foreign[$master_field_md5]['ref_db_name']; + } else { + $ref_db_name = $GLOBALS['db']; + } + + $empty_fields = false; + foreach ($master_field as $key => $one_field) { + if ((! empty($one_field) && empty($foreign_field[$key])) + || (empty($one_field) && ! empty($foreign_field[$key])) + ) { + $empty_fields = true; + } + + if (empty($one_field) && empty($foreign_field[$key])) { + unset($master_field[$key]); + unset($foreign_field[$key]); + } + } + + if (! empty($foreign_db) + && ! empty($foreign_table) + && ! $empty_fields + ) { + if (isset($existrel_foreign[$master_field_md5])) { + $constraint_name + = $existrel_foreign[$master_field_md5]['constraint']; + $on_delete = ! empty( + $existrel_foreign[$master_field_md5]['on_delete'] + ) + ? $existrel_foreign[$master_field_md5]['on_delete'] + : 'RESTRICT'; + $on_update = ! empty( + $existrel_foreign[$master_field_md5]['on_update'] + ) + ? $existrel_foreign[$master_field_md5]['on_update'] + : 'RESTRICT'; + + if ($ref_db_name != $foreign_db + || $existrel_foreign[$master_field_md5]['ref_table_name'] != $foreign_table + || $existrel_foreign[$master_field_md5]['ref_index_list'] != $foreign_field + || $existrel_foreign[$master_field_md5]['index_list'] != $master_field + || $_POST['constraint_name'][$master_field_md5] != $constraint_name + || ($_POST['on_delete'][$master_field_md5] != $on_delete) + || ($_POST['on_update'][$master_field_md5] != $on_update) + ) { + // another foreign key is already defined for this field + // or an option has been changed for ON DELETE or ON UPDATE + $drop = true; + $create = true; + } // end if... else.... + } else { + // no key defined for this field(s) + $create = true; + } + } elseif (isset($existrel_foreign[$master_field_md5])) { + $drop = true; + } // end if... else.... + + $tmp_error_drop = false; + if ($drop) { + $drop_query = 'ALTER TABLE ' . Util::backquote($table) + . ' DROP FOREIGN KEY ' + . Util::backquote( + $existrel_foreign[$master_field_md5]['constraint'] + ) + . ';'; + + if (! isset($_POST['preview_sql'])) { + $display_query .= $drop_query . "\n"; + $this->_dbi->tryQuery($drop_query); + $tmp_error_drop = $this->_dbi->getError(); + + if (! empty($tmp_error_drop)) { + $seen_error = true; + $html_output .= Util::mysqlDie( + $tmp_error_drop, + $drop_query, + false, + '', + false + ); + continue; + } + } else { + $preview_sql_data .= $drop_query . "\n"; + } + } + $tmp_error_create = false; + if (! $create) { + continue; + } + + $create_query = $this->_getSQLToCreateForeignKey( + $table, + $master_field, + $foreign_db, + $foreign_table, + $foreign_field, + $_POST['constraint_name'][$master_field_md5], + $options_array[$_POST['on_delete'][$master_field_md5]], + $options_array[$_POST['on_update'][$master_field_md5]] + ); + + if (! isset($_POST['preview_sql'])) { + $display_query .= $create_query . "\n"; + $this->_dbi->tryQuery($create_query); + $tmp_error_create = $this->_dbi->getError(); + if (! empty($tmp_error_create)) { + $seen_error = true; + + if (substr($tmp_error_create, 1, 4) == '1005') { + $message = Message::error( + __( + 'Error creating foreign key on %1$s (check data ' . + 'types)' + ) + ); + $message->addParam(implode(', ', $master_field)); + $html_output .= $message->getDisplay(); + } else { + $html_output .= Util::mysqlDie( + $tmp_error_create, + $create_query, + false, + '', + false + ); + } + $html_output .= Util::showMySQLDocu( + 'InnoDB_foreign_key_constraints' + ) . "\n"; + } + } else { + $preview_sql_data .= $create_query . "\n"; + } + + // this is an alteration and the old constraint has been dropped + // without creation of a new one + if ($drop && $create && empty($tmp_error_drop) + && ! empty($tmp_error_create) + ) { + // a rollback may be better here + $sql_query_recreate = '# Restoring the dropped constraint...' . "\n"; + $sql_query_recreate .= $this->_getSQLToCreateForeignKey( + $table, + $master_field, + $existrel_foreign[$master_field_md5]['ref_db_name'], + $existrel_foreign[$master_field_md5]['ref_table_name'], + $existrel_foreign[$master_field_md5]['ref_index_list'], + $existrel_foreign[$master_field_md5]['constraint'], + $options_array[$existrel_foreign[$master_field_md5]['on_delete']], + $options_array[$existrel_foreign[$master_field_md5]['on_update']] + ); + if (! isset($_POST['preview_sql'])) { + $display_query .= $sql_query_recreate . "\n"; + $this->_dbi->tryQuery($sql_query_recreate); + } else { + $preview_sql_data .= $sql_query_recreate; + } + } + } // end foreach + + return [ + $html_output, + $preview_sql_data, + $display_query, + $seen_error, + ]; + } + + /** + * Returns the SQL query for foreign key constraint creation + * + * @param string $table table name + * @param array $field field names + * @param string $foreignDb foreign database name + * @param string $foreignTable foreign table name + * @param array $foreignField foreign field names + * @param string $name name of the constraint + * @param string $onDelete on delete action + * @param string $onUpdate on update action + * + * @return string SQL query for foreign key constraint creation + */ + private function _getSQLToCreateForeignKey( + $table, + array $field, + $foreignDb, + $foreignTable, + array $foreignField, + $name = null, + $onDelete = null, + $onUpdate = null + ) { + $sql_query = 'ALTER TABLE ' . Util::backquote($table) . ' ADD '; + // if user entered a constraint name + if (! empty($name)) { + $sql_query .= ' CONSTRAINT ' . Util::backquote($name); + } + + foreach ($field as $key => $one_field) { + $field[$key] = Util::backquote($one_field); + } + foreach ($foreignField as $key => $one_field) { + $foreignField[$key] = Util::backquote($one_field); + } + $sql_query .= ' FOREIGN KEY (' . implode(', ', $field) . ') REFERENCES ' + . ($this->_db_name != $foreignDb + ? Util::backquote($foreignDb) . '.' : '') + . Util::backquote($foreignTable) + . '(' . implode(', ', $foreignField) . ')'; + + if (! empty($onDelete)) { + $sql_query .= ' ON DELETE ' . $onDelete; + } + if (! empty($onUpdate)) { + $sql_query .= ' ON UPDATE ' . $onUpdate; + } + $sql_query .= ';'; + + return $sql_query; + } + + /** + * Returns the generation expression for virtual columns + * + * @param string $column name of the column + * + * @return array|boolean associative array of column name and their expressions + * or false on failure + */ + public function getColumnGenerationExpression($column = null) + { + $serverType = Util::getServerType(); + if ($serverType == 'MySQL' + && $this->_dbi->getVersion() > 50705 + && ! $GLOBALS['cfg']['Server']['DisableIS'] + ) { + $sql + = "SELECT + `COLUMN_NAME` AS `Field`, + `GENERATION_EXPRESSION` AS `Expression` + FROM + `information_schema`.`COLUMNS` + WHERE + `TABLE_SCHEMA` = '" . $this->_dbi->escapeString($this->_db_name) . "' + AND `TABLE_NAME` = '" . $this->_dbi->escapeString($this->_name) . "'"; + if ($column != null) { + $sql .= " AND `COLUMN_NAME` = '" . $this->_dbi->escapeString($column) + . "'"; + } + return $this->_dbi->fetchResult($sql, 'Field', 'Expression'); + } + + $createTable = $this->showCreate(); + if (! $createTable) { + return false; + } + + $parser = new Parser($createTable); + /** + * @var CreateStatement $stmt + */ + $stmt = $parser->statements[0]; + $fields = TableUtils::getFields($stmt); + if ($column != null) { + $expression = isset($fields[$column]['expr']) ? + substr($fields[$column]['expr'], 1, -1) : ''; + return [$column => $expression]; + } + + $ret = []; + foreach ($fields as $field => $options) { + if (isset($options['expr'])) { + $ret[$field] = substr($options['expr'], 1, -1); + } + } + return $ret; + } + + /** + * Returns the CREATE statement for this table + * + * @return mixed + */ + public function showCreate() + { + return $this->_dbi->fetchValue( + 'SHOW CREATE TABLE ' . Util::backquote($this->_db_name) . '.' + . Util::backquote($this->_name), + 0, + 1 + ); + } + + /** + * Returns the real row count for a table + * + * @return int + */ + public function getRealRowCountTable() + { + // SQL query to get row count for a table. + $result = $this->_dbi->fetchSingleRow( + sprintf( + 'SELECT COUNT(*) AS %s FROM %s.%s', + Util::backquote('row_count'), + Util::backquote($this->_db_name), + Util::backquote($this->_name) + ) + ); + return $result['row_count']; + } + + /** + * Get columns with indexes + * + * @param int $types types bitmask + * + * @return array an array of columns + */ + public function getColumnsWithIndex($types) + { + $columns_with_index = []; + foreach (Index::getFromTableByChoice( + $this->_name, + $this->_db_name, + $types + ) as $index) { + $columns = $index->getColumns(); + foreach ($columns as $column_name => $dummy) { + $columns_with_index[] = $column_name; + } + } + return $columns_with_index; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/TablePartitionDefinition.php b/srcs/phpmyadmin/libraries/classes/TablePartitionDefinition.php new file mode 100644 index 0000000..89ece4b --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/TablePartitionDefinition.php @@ -0,0 +1,200 @@ + 1 + && isset($partitionDetails['partition_by']) + && ($partitionDetails['partition_by'] === 'RANGE' + || $partitionDetails['partition_by'] === 'RANGE COLUMNS' + || $partitionDetails['partition_by'] === 'LIST' + || $partitionDetails['partition_by'] === 'LIST COLUMNS'); + + // Values are specified only for LIST and RANGE type partitions + $partitionDetails['value_enabled'] = isset($partitionDetails['partition_by']) + && ($partitionDetails['partition_by'] === 'RANGE' + || $partitionDetails['partition_by'] === 'RANGE COLUMNS' + || $partitionDetails['partition_by'] === 'LIST' + || $partitionDetails['partition_by'] === 'LIST COLUMNS'); + + return self::extractPartitions($partitionDetails); + } + + /** + * Extract some partitioning and subpartitioning parameters from the request + * + * @return array + */ + protected static function extractDetailsFromRequest(): array + { + $partitionParams = [ + 'partition_by' => null, + 'partition_expr' => null, + 'subpartition_by' => null, + 'subpartition_expr' => null, + ]; + //Initialize details with values to "null" if not in request + $details = array_merge( + $partitionParams, + //Keep $_POST values, but only for keys that are in $partitionParams + array_intersect_key($_POST, $partitionParams) + ); + + $details['partition_count'] = self::extractPartitionCount('partition_count') ?: ''; + $details['subpartition_count'] = self::extractPartitionCount('subpartition_count') ?: ''; + + return $details; + } + + /** + * @param string $paramLabel Label searched in request + * + * @return int + */ + protected static function extractPartitionCount(string $paramLabel): int + { + if (Core::isValid($_POST[$paramLabel], 'numeric')) { + // MySQL's limit is 8192, so do not allow more + $count = min((int) $_POST[$paramLabel], 8192); + } else { + $count = 0; + } + return $count; + } + + /** + * @param array $partitionDetails Details of partitions + * + * @return array + */ + protected static function extractPartitions(array $partitionDetails): array + { + $partitionCount = $partitionDetails['partition_count']; + $subpartitionCount = $partitionDetails['subpartition_count']; + + // No partitions + if ($partitionCount <= 1) { + return $partitionDetails; + } + + // Has partitions + $partitions = $_POST['partitions'] ?? []; + + // Remove details of the additional partitions + // when number of partitions have been reduced + array_splice($partitions, $partitionCount); + + for ($i = 0; $i < $partitionCount; $i++) { + if (! isset($partitions[$i])) { // Newly added partition + $partitions[$i] = [ + 'name' => 'p' . $i, + 'value_type' => '', + 'value' => '', + 'engine' => '', + 'comment' => '', + 'data_directory' => '', + 'index_directory' => '', + 'max_rows' => '', + 'min_rows' => '', + 'tablespace' => '', + 'node_group' => '', + ]; + } + + $partition =& $partitions[$i]; + $partition['prefix'] = 'partitions[' . $i . ']'; + + // Changing from HASH/KEY to RANGE/LIST + if (! isset($partition['value_type'])) { + $partition['value_type'] = ''; + $partition['value'] = ''; + } + if (! isset($partition['engine'])) { // When removing subpartitioning + $partition['engine'] = ''; + $partition['comment'] = ''; + $partition['data_directory'] = ''; + $partition['index_directory'] = ''; + $partition['max_rows'] = ''; + $partition['min_rows'] = ''; + $partition['tablespace'] = ''; + $partition['node_group'] = ''; + } + + // No subpartitions + if ($subpartitionCount <= 1 || $partitionDetails['can_have_subpartitions'] !== true) { + unset($partition['subpartitions'], $partition['subpartition_count']); + continue; + } + + // Has subpartitions + $partition['subpartition_count'] = $subpartitionCount; + + if (! isset($partition['subpartitions'])) { + $partition['subpartitions'] = []; + } + $subpartitions =& $partition['subpartitions']; + + // Remove details of the additional subpartitions + // when number of subpartitions have been reduced + array_splice($subpartitions, $subpartitionCount); + + for ($j = 0; $j < $subpartitionCount; $j++) { + if (! isset($subpartitions[$j])) { // Newly added subpartition + $subpartitions[$j] = [ + 'name' => $partition['name'] . '_s' . $j, + 'engine' => '', + 'comment' => '', + 'data_directory' => '', + 'index_directory' => '', + 'max_rows' => '', + 'min_rows' => '', + 'tablespace' => '', + 'node_group' => '', + ]; + } + + $subpartitions[$j]['prefix'] = 'partitions[' . $i . ']' + . '[subpartitions][' . $j . ']'; + } + } + $partitionDetails['partitions'] = $partitions; + return $partitionDetails; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Template.php b/srcs/phpmyadmin/libraries/classes/Template.php new file mode 100644 index 0000000..3866fd2 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Template.php @@ -0,0 +1,142 @@ +getTempDir('twig'); + /* Twig expects false when cache is not configured */ + if ($cache_dir === null) { + $cache_dir = false; + } + $twig = new Environment($loader, [ + 'auto_reload' => true, + 'cache' => $cache_dir, + 'debug' => false, + ]); + $twig->addExtension(new CoreExtension()); + $twig->addExtension(new I18nExtension()); + $twig->addExtension(new MessageExtension()); + $twig->addExtension(new PluginsExtension()); + $twig->addExtension(new RelationExtension()); + $twig->addExtension(new SanitizeExtension()); + $twig->addExtension(new ServerPrivilegesExtension()); + $twig->addExtension(new StorageEngineExtension()); + $twig->addExtension(new TableExtension()); + $twig->addExtension(new TrackerExtension()); + $twig->addExtension(new TransformationsExtension()); + $twig->addExtension(new UrlExtension()); + $twig->addExtension(new UtilExtension()); + static::$twig = $twig; + } + } + + /** + * Loads a template. + * + * @param string $templateName Template path name + * + * @return Twig_TemplateWrapper + * @throws LoaderError + * @throws RuntimeError + * @throws SyntaxError + */ + public function load(string $templateName): Twig_TemplateWrapper + { + try { + $template = static::$twig->load($templateName . '.twig'); + } catch (RuntimeException $e) { + /* Retry with disabled cache */ + static::$twig->setCache(false); + $template = static::$twig->load($templateName . '.twig'); + /* + * The trigger error is intentionally after second load + * to avoid triggering error when disabling cache does not + * solve it. + */ + trigger_error( + sprintf( + __('Error while working with template cache: %s'), + $e->getMessage() + ), + E_USER_WARNING + ); + } + + return $template; + } + + /** + * @param string $template Template path name + * @param array $data Associative array of template variables + * + * @return string + * @throws Throwable + * @throws Twig_Error_Loader + * @throws Twig_Error_Runtime + * @throws Twig_Error_Syntax + */ + public function render(string $template, array $data = []): string + { + return $this->load($template)->render($data); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Theme.php b/srcs/phpmyadmin/libraries/classes/Theme.php new file mode 100644 index 0000000..ece3a27 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Theme.php @@ -0,0 +1,387 @@ +template = new Template(); + } + + /** + * Loads theme information + * + * @return boolean whether loading them info was successful or not + * @access public + */ + public function loadInfo() + { + $infofile = $this->getPath() . '/theme.json'; + if (! @file_exists($infofile)) { + return false; + } + + if ($this->mtime_info === filemtime($infofile)) { + return true; + } + $content = @file_get_contents($infofile); + if ($content === false) { + return false; + } + $data = json_decode($content, true); + + // Did we get expected data? + if (! is_array($data)) { + return false; + } + // Check that all required data are there + $members = [ + 'name', + 'version', + 'supports', + ]; + foreach ($members as $member) { + if (! isset($data[$member])) { + return false; + } + } + + // Version check + if (! is_array($data['supports'])) { + return false; + } + if (! in_array(PMA_MAJOR_VERSION, $data['supports'])) { + return false; + } + + $this->mtime_info = filemtime($infofile); + $this->filesize_info = filesize($infofile); + + $this->setVersion($data['version']); + $this->setName($data['name']); + + return true; + } + + /** + * returns theme object loaded from given folder + * or false if theme is invalid + * + * @param string $folder path to theme + * + * @return Theme|false + * @static + * @access public + */ + public static function load($folder) + { + $theme = new Theme(); + + $theme->setPath($folder); + + if (! $theme->loadInfo()) { + return false; + } + + $theme->checkImgPath(); + + return $theme; + } + + /** + * checks image path for existence - if not found use img from fallback theme + * + * @access public + * @return bool + */ + public function checkImgPath() + { + // try current theme first + if (is_dir($this->getPath() . '/img/')) { + $this->setImgPath($this->getPath() . '/img/'); + return true; + } + + // try fallback theme + $fallback = './themes/' . ThemeManager::FALLBACK_THEME . '/img/'; + if (is_dir($fallback)) { + $this->setImgPath($fallback); + return true; + } + + // we failed + trigger_error( + sprintf( + __('No valid image path for theme %s found!'), + $this->getName() + ), + E_USER_ERROR + ); + return false; + } + + /** + * returns path to theme + * + * @access public + * @return string path to theme + */ + public function getPath() + { + return $this->path; + } + + /** + * set path to theme + * + * @param string $path path to theme + * + * @return void + * @access public + */ + public function setPath($path) + { + $this->path = trim($path); + } + + /** + * sets version + * + * @param string $version version to set + * + * @return void + * @access public + */ + public function setVersion($version) + { + $this->version = trim($version); + } + + /** + * returns version + * + * @return string version + * @access public + */ + public function getVersion() + { + return $this->version; + } + + /** + * checks theme version against $version + * returns true if theme version is equal or higher to $version + * + * @param string $version version to compare to + * + * @return boolean true if theme version is equal or higher to $version + * @access public + */ + public function checkVersion($version) + { + return version_compare($this->getVersion(), $version, 'lt'); + } + + /** + * sets name + * + * @param string $name name to set + * + * @return void + * @access public + */ + public function setName($name) + { + $this->name = trim($name); + } + + /** + * returns name + * + * @access public + * @return string name + */ + public function getName() + { + return $this->name; + } + + /** + * sets id + * + * @param string $id new id + * + * @return void + * @access public + */ + public function setId($id) + { + $this->id = trim($id); + } + + /** + * returns id + * + * @return string id + * @access public + */ + public function getId() + { + return $this->id; + } + + /** + * Sets path to images for the theme + * + * @param string $path path to images for this theme + * + * @return void + * @access public + */ + public function setImgPath($path) + { + $this->img_path = $path; + } + + /** + * Returns the path to image for the theme. + * If filename is given, it possibly fallbacks to fallback + * theme for it if image does not exist. + * + * @param string $file file name for image + * @param string $fallback fallback image + * + * @access public + * @return string image path for this theme + */ + public function getImgPath($file = null, $fallback = null) + { + if ($file === null) { + return $this->img_path; + } + + if (is_readable($this->img_path . $file)) { + return $this->img_path . $file; + } + + if ($fallback !== null) { + return $this->getImgPath($fallback); + } + + return './themes/' . ThemeManager::FALLBACK_THEME . '/img/' . $file; + } + + /** + * Renders the preview for this theme + * + * @return string + * @access public + */ + public function getPrintPreview() + { + $url_params = ['set_theme' => $this->getId()]; + $screen = null; + $path = $this->getPath() . '/screen.png'; + if (@file_exists($path)) { + $screen = $path; + } + + return $this->template->render('theme_preview', [ + 'url_params' => $url_params, + 'name' => $this->getName(), + 'version' => $this->getVersion(), + 'id' => $this->getId(), + 'screen' => $screen, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/ThemeManager.php b/srcs/phpmyadmin/libraries/classes/ThemeManager.php new file mode 100644 index 0000000..8bba657 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/ThemeManager.php @@ -0,0 +1,417 @@ +themes = []; + $this->theme_default = self::FALLBACK_THEME; + $this->active_theme = ''; + + if (! $this->setThemesPath('./themes/')) { + return; + } + + $this->setThemePerServer($GLOBALS['cfg']['ThemePerServer']); + + $this->loadThemes(); + + $this->theme = new Theme(); + + $config_theme_exists = true; + + if (! $this->checkTheme($GLOBALS['cfg']['ThemeDefault'])) { + trigger_error( + sprintf( + __('Default theme %s not found!'), + htmlspecialchars($GLOBALS['cfg']['ThemeDefault']) + ), + E_USER_ERROR + ); + $config_theme_exists = false; + } else { + $this->theme_default = $GLOBALS['cfg']['ThemeDefault']; + } + + // check if user have a theme cookie + $cookie_theme = $this->getThemeCookie(); + if (! $cookie_theme || ! $this->setActiveTheme($cookie_theme)) { + if ($config_theme_exists) { + // otherwise use default theme + $this->setActiveTheme($this->theme_default); + } else { + // or fallback theme + $this->setActiveTheme(self::FALLBACK_THEME); + } + } + } + + /** + * Returns the singleton ThemeManager object + * + * @return ThemeManager The instance + */ + public static function getInstance(): ThemeManager + { + if (empty(self::$_instance)) { + self::$_instance = new ThemeManager(); + } + return self::$_instance; + } + + /** + * sets path to folder containing the themes + * + * @param string $path path to themes folder + * + * @access public + * @return boolean success + */ + public function setThemesPath($path) + { + if (! $this->_checkThemeFolder($path)) { + return false; + } + + $this->_themes_path = trim($path); + return true; + } + + /** + * sets if there are different themes per server + * + * @param boolean $per_server Whether to enable per server flag + * + * @access public + * @return void + */ + public function setThemePerServer($per_server) + { + $this->per_server = (bool) $per_server; + } + + /** + * Sets active theme + * + * @param string $theme theme name + * + * @access public + * @return bool true on success + */ + public function setActiveTheme($theme = null) + { + if (! $this->checkTheme($theme)) { + trigger_error( + sprintf( + __('Theme %s not found!'), + htmlspecialchars($theme) + ), + E_USER_ERROR + ); + return false; + } + + $this->active_theme = $theme; + $this->theme = $this->themes[$theme]; + + // need to set later + //$this->setThemeCookie(); + + return true; + } + + /** + * Returns name for storing theme + * + * @return string cookie name + * @access public + */ + public function getThemeCookieName() + { + // Allow different theme per server + if (isset($GLOBALS['server']) && $this->per_server) { + return $this->cookie_name . '-' . $GLOBALS['server']; + } + + return $this->cookie_name; + } + + /** + * returns name of theme stored in the cookie + * + * @return string|bool theme name from cookie or false + * @access public + */ + public function getThemeCookie() + { + /** @var Config $PMA_Config */ + global $PMA_Config; + + $name = $this->getThemeCookieName(); + if ($PMA_Config->issetCookie($name)) { + return $PMA_Config->getCookie($name); + } + + return false; + } + + /** + * save theme in cookie + * + * @return bool true + * @access public + */ + public function setThemeCookie() + { + $GLOBALS['PMA_Config']->setCookie( + $this->getThemeCookieName(), + $this->theme->id, + $this->theme_default + ); + // force a change of a dummy session variable to avoid problems + // with the caching of phpmyadmin.css.php + $GLOBALS['PMA_Config']->set('theme-update', $this->theme->id); + return true; + } + + /** + * Checks whether folder is valid for storing themes + * + * @param string $folder Folder name to test + * + * @return boolean + * @access private + */ + private function _checkThemeFolder($folder) + { + if (! is_dir($folder)) { + trigger_error( + sprintf( + __('Theme path not found for theme %s!'), + htmlspecialchars($folder) + ), + E_USER_ERROR + ); + return false; + } + + return true; + } + + /** + * read all themes + * + * @return bool true + * @access public + */ + public function loadThemes() + { + $this->themes = []; + + if (false === ($handleThemes = opendir($this->_themes_path))) { + trigger_error( + 'phpMyAdmin-ERROR: cannot open themes folder: ' + . $this->_themes_path, + E_USER_WARNING + ); + return false; + } + + // check for themes directory + while (false !== ($PMA_Theme = readdir($handleThemes))) { + // Skip non dirs, . and .. + if ($PMA_Theme == '.' + || $PMA_Theme == '..' + || ! @is_dir(ROOT_PATH . $this->_themes_path . $PMA_Theme) + ) { + continue; + } + if (array_key_exists($PMA_Theme, $this->themes)) { + continue; + } + $new_theme = Theme::load( + $this->_themes_path . $PMA_Theme + ); + if ($new_theme) { + $new_theme->setId($PMA_Theme); + $this->themes[$PMA_Theme] = $new_theme; + } + } // end get themes + closedir($handleThemes); + + ksort($this->themes); + return true; + } + + /** + * checks if given theme name is a known theme + * + * @param string $theme name fo theme to check for + * + * @return bool + * @access public + */ + public function checkTheme($theme) + { + return array_key_exists($theme, $this->themes); + } + + /** + * returns HTML selectbox, with or without form enclosed + * + * @param boolean $form whether enclosed by from tags or not + * + * @return string + * @access public + */ + public function getHtmlSelectBox($form = true) + { + $select_box = ''; + + if ($form) { + $select_box .= '
    '; + $select_box .= $theme_preview_href . __('Theme:') . '' . "\n"; + + $select_box .= ''; + + if ($form) { + $select_box .= '
    '; + } + + return $select_box; + } + + /** + * Renders the previews for all themes + * + * @return string + * @access public + */ + public function getPrintPreviews() + { + $retval = ''; + foreach ($this->themes as $each_theme) { + $retval .= $each_theme->getPrintPreview(); + } // end 'open themes' + return $retval; + } + + /** + * Theme initialization + * + * @return void + * @access public + */ + public static function initializeTheme() + { + $tmanager = self::getInstance(); + + /** + * the theme object + * + * @global Theme $GLOBALS['PMA_Theme'] + */ + $GLOBALS['PMA_Theme'] = $tmanager->theme; + + // BC + /** + * the theme path + * @global string $GLOBALS['pmaThemePath'] + */ + $GLOBALS['pmaThemePath'] = $GLOBALS['PMA_Theme']->getPath(); + /** + * the theme image path + * @global string $GLOBALS['pmaThemeImage'] + */ + $GLOBALS['pmaThemeImage'] = $GLOBALS['PMA_Theme']->getImgPath(); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Tracker.php b/srcs/phpmyadmin/libraries/classes/Tracker.php new file mode 100644 index 0000000..882c09d --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Tracker.php @@ -0,0 +1,942 @@ +getRelationsParam(); + /* Restore original state */ + self::$enabled = true; + if (! $cfgRelation['trackingwork']) { + return false; + } + + $pma_table = self::_getTrackingTable(); + + return $pma_table !== null; + } + + /** + * Parses the name of a table from a SQL statement substring. + * + * @param string $string part of SQL statement + * + * @static + * + * @return string the name of table + */ + protected static function getTableName($string) + { + if (mb_strstr($string, '.')) { + $temp = explode('.', $string); + $tablename = $temp[1]; + } else { + $tablename = $string; + } + + $str = explode("\n", $tablename); + $tablename = $str[0]; + + $tablename = str_replace([';', '`'], '', $tablename); + $tablename = trim($tablename); + + return $tablename; + } + + + /** + * Gets the tracking status of a table, is it active or deactive ? + * + * @param string $dbname name of database + * @param string $tablename name of table + * + * @static + * + * @return boolean true or false + */ + public static function isTracked($dbname, $tablename) + { + if (! self::$enabled) { + return false; + } + + if (isset(self::$_tracking_cache[$dbname][$tablename])) { + return self::$_tracking_cache[$dbname][$tablename]; + } + /* We need to avoid attempt to track any queries + * from Relation::getRelationsParam + */ + self::$enabled = false; + $relation = new Relation($GLOBALS['dbi']); + $cfgRelation = $relation->getRelationsParam(); + /* Restore original state */ + self::$enabled = true; + if (! $cfgRelation['trackingwork']) { + return false; + } + + $sql_query = " SELECT tracking_active FROM " . self::_getTrackingTable() . + " WHERE db_name = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " . + " AND table_name = '" . $GLOBALS['dbi']->escapeString($tablename) . "' " . + " ORDER BY version DESC LIMIT 1"; + + $result = $GLOBALS['dbi']->fetchValue($sql_query, 0, 0, DatabaseInterface::CONNECT_CONTROL) == 1; + + self::$_tracking_cache[$dbname][$tablename] = $result; + + return $result; + } + + /** + * Returns the comment line for the log. + * + * @return string Comment, contains date and username + */ + public static function getLogComment() + { + $date = Util::date('Y-m-d H:i:s'); + $user = preg_replace('/\s+/', ' ', $GLOBALS['cfg']['Server']['user']); + + return "# log " . $date . " " . $user . "\n"; + } + + /** + * Creates tracking version of a table / view + * (in other words: create a job to track future changes on the table). + * + * @param string $dbname name of database + * @param string $tablename name of table + * @param string $version version + * @param string $tracking_set set of tracking statements + * @param bool $is_view if table is a view + * + * @static + * + * @return int result of version insertion + */ + public static function createVersion( + $dbname, + $tablename, + $version, + $tracking_set = '', + bool $is_view = false + ) { + global $sql_backquotes, $export_type; + + $relation = new Relation($GLOBALS['dbi']); + + if ($tracking_set == '') { + $tracking_set + = $GLOBALS['cfg']['Server']['tracking_default_statements']; + } + + /** + * get Export SQL instance + * @var ExportSql $export_sql_plugin + */ + $export_sql_plugin = Plugins::getPlugin( + "export", + "sql", + 'libraries/classes/Plugins/Export/', + [ + 'export_type' => $export_type, + 'single_table' => false, + ] + ); + + $sql_backquotes = true; + + $date = Util::date('Y-m-d H:i:s'); + + // Get data definition snapshot of table + + $columns = $GLOBALS['dbi']->getColumns($dbname, $tablename, null, true); + // int indices to reduce size + $columns = array_values($columns); + // remove Privileges to reduce size + for ($i = 0, $nb = count($columns); $i < $nb; $i++) { + unset($columns[$i]['Privileges']); + } + + $indexes = $GLOBALS['dbi']->getTableIndexes($dbname, $tablename); + + $snapshot = [ + 'COLUMNS' => $columns, + 'INDEXES' => $indexes, + ]; + $snapshot = serialize($snapshot); + + // Get DROP TABLE / DROP VIEW and CREATE TABLE SQL statements + $sql_backquotes = true; + + $create_sql = ""; + + if ($GLOBALS['cfg']['Server']['tracking_add_drop_table'] == true + && $is_view === false + ) { + $create_sql .= self::getLogComment() + . 'DROP TABLE IF EXISTS ' . Util::backquote($tablename) . ";\n"; + } + + if ($GLOBALS['cfg']['Server']['tracking_add_drop_view'] == true + && $is_view === true + ) { + $create_sql .= self::getLogComment() + . 'DROP VIEW IF EXISTS ' . Util::backquote($tablename) . ";\n"; + } + + $create_sql .= self::getLogComment() . + $export_sql_plugin->getTableDef($dbname, $tablename, "\n", ""); + + // Save version + + $sql_query = "/*NOTRACK*/\n" . + "INSERT INTO " . self::_getTrackingTable() . " (" . + "db_name, " . + "table_name, " . + "version, " . + "date_created, " . + "date_updated, " . + "schema_snapshot, " . + "schema_sql, " . + "data_sql, " . + "tracking " . + ") " . + "values ( + '" . $GLOBALS['dbi']->escapeString($dbname) . "', + '" . $GLOBALS['dbi']->escapeString($tablename) . "', + '" . $GLOBALS['dbi']->escapeString($version) . "', + '" . $GLOBALS['dbi']->escapeString($date) . "', + '" . $GLOBALS['dbi']->escapeString($date) . "', + '" . $GLOBALS['dbi']->escapeString($snapshot) . "', + '" . $GLOBALS['dbi']->escapeString($create_sql) . "', + '" . $GLOBALS['dbi']->escapeString("\n") . "', + '" . $GLOBALS['dbi']->escapeString($tracking_set) + . "' )"; + + $result = $relation->queryAsControlUser($sql_query); + + if ($result) { + // Deactivate previous version + self::deactivateTracking($dbname, $tablename, (int) $version - 1); + } + + return $result; + } + + + /** + * Removes all tracking data for a table or a version of a table + * + * @param string $dbname name of database + * @param string $tablename name of table + * @param string $version version + * + * @static + * + * @return int result of version insertion + */ + public static function deleteTracking($dbname, $tablename, $version = '') + { + $relation = new Relation($GLOBALS['dbi']); + + $sql_query = "/*NOTRACK*/\n" + . "DELETE FROM " . self::_getTrackingTable() + . " WHERE `db_name` = '" + . $GLOBALS['dbi']->escapeString($dbname) . "'" + . " AND `table_name` = '" + . $GLOBALS['dbi']->escapeString($tablename) . "'"; + if ($version) { + $sql_query .= " AND `version` = '" + . $GLOBALS['dbi']->escapeString($version) . "'"; + } + return $relation->queryAsControlUser($sql_query); + } + + /** + * Creates tracking version of a database + * (in other words: create a job to track future changes on the database). + * + * @param string $dbname name of database + * @param string $version version + * @param string $query query + * @param string $tracking_set set of tracking statements + * + * @static + * + * @return int result of version insertion + */ + public static function createDatabaseVersion( + $dbname, + $version, + $query, + $tracking_set = 'CREATE DATABASE,ALTER DATABASE,DROP DATABASE' + ) { + $relation = new Relation($GLOBALS['dbi']); + + $date = Util::date('Y-m-d H:i:s'); + + if ($tracking_set == '') { + $tracking_set + = $GLOBALS['cfg']['Server']['tracking_default_statements']; + } + + $create_sql = ""; + + if ($GLOBALS['cfg']['Server']['tracking_add_drop_database'] == true) { + $create_sql .= self::getLogComment() + . 'DROP DATABASE IF EXISTS ' . Util::backquote($dbname) . ";\n"; + } + + $create_sql .= self::getLogComment() . $query; + + // Save version + $sql_query = "/*NOTRACK*/\n" . + "INSERT INTO " . self::_getTrackingTable() . " (" . + "db_name, " . + "table_name, " . + "version, " . + "date_created, " . + "date_updated, " . + "schema_snapshot, " . + "schema_sql, " . + "data_sql, " . + "tracking " . + ") " . + "values ( + '" . $GLOBALS['dbi']->escapeString($dbname) . "', + '" . $GLOBALS['dbi']->escapeString('') . "', + '" . $GLOBALS['dbi']->escapeString($version) . "', + '" . $GLOBALS['dbi']->escapeString($date) . "', + '" . $GLOBALS['dbi']->escapeString($date) . "', + '" . $GLOBALS['dbi']->escapeString('') . "', + '" . $GLOBALS['dbi']->escapeString($create_sql) . "', + '" . $GLOBALS['dbi']->escapeString("\n") . "', + '" . $GLOBALS['dbi']->escapeString($tracking_set) + . "' )"; + + return $relation->queryAsControlUser($sql_query); + } + + + + /** + * Changes tracking of a table. + * + * @param string $dbname name of database + * @param string $tablename name of table + * @param string $version version + * @param integer $new_state the new state of tracking + * + * @static + * + * @return int result of SQL query + */ + private static function _changeTracking( + $dbname, + $tablename, + $version, + $new_state + ) { + $relation = new Relation($GLOBALS['dbi']); + + $sql_query = " UPDATE " . self::_getTrackingTable() . + " SET `tracking_active` = '" . $new_state . "' " . + " WHERE `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " . + " AND `table_name` = '" . $GLOBALS['dbi']->escapeString($tablename) . "' " . + " AND `version` = '" . $GLOBALS['dbi']->escapeString((string) $version) . "' "; + + return $relation->queryAsControlUser($sql_query); + } + + /** + * Changes tracking data of a table. + * + * @param string $dbname name of database + * @param string $tablename name of table + * @param string $version version + * @param string $type type of data(DDL || DML) + * @param string|array $new_data the new tracking data + * + * @static + * + * @return bool result of change + */ + public static function changeTrackingData( + $dbname, + $tablename, + $version, + $type, + $new_data + ) { + $relation = new Relation($GLOBALS['dbi']); + + if ($type == 'DDL') { + $save_to = 'schema_sql'; + } elseif ($type == 'DML') { + $save_to = 'data_sql'; + } else { + return false; + } + $date = Util::date('Y-m-d H:i:s'); + + $new_data_processed = ''; + if (is_array($new_data)) { + foreach ($new_data as $data) { + $new_data_processed .= '# log ' . $date . ' ' . $data['username'] + . $GLOBALS['dbi']->escapeString($data['statement']) . "\n"; + } + } else { + $new_data_processed = $new_data; + } + + $sql_query = " UPDATE " . self::_getTrackingTable() . + " SET `" . $save_to . "` = '" . $new_data_processed . "' " . + " WHERE `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " . + " AND `table_name` = '" . $GLOBALS['dbi']->escapeString($tablename) . "' " . + " AND `version` = '" . $GLOBALS['dbi']->escapeString($version) . "' "; + + $result = $relation->queryAsControlUser($sql_query); + + return (bool) $result; + } + + /** + * Activates tracking of a table. + * + * @param string $dbname name of database + * @param string $tablename name of table + * @param string $version version + * + * @static + * + * @return int result of SQL query + */ + public static function activateTracking($dbname, $tablename, $version) + { + return self::_changeTracking($dbname, $tablename, $version, 1); + } + + + /** + * Deactivates tracking of a table. + * + * @param string $dbname name of database + * @param string $tablename name of table + * @param string $version version + * + * @static + * + * @return int result of SQL query + */ + public static function deactivateTracking($dbname, $tablename, $version) + { + return self::_changeTracking($dbname, $tablename, $version, 0); + } + + + /** + * Gets the newest version of a tracking job + * (in other words: gets the HEAD version). + * + * @param string $dbname name of database + * @param string $tablename name of table + * @param string $statement tracked statement + * + * @static + * + * @return int (-1 if no version exists | > 0 if a version exists) + */ + public static function getVersion($dbname, $tablename, $statement = null) + { + $relation = new Relation($GLOBALS['dbi']); + + $sql_query = " SELECT MAX(version) FROM " . self::_getTrackingTable() . + " WHERE `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " . + " AND `table_name` = '" . $GLOBALS['dbi']->escapeString($tablename) . "' "; + + if ($statement != "") { + $sql_query .= " AND FIND_IN_SET('" + . $statement . "',tracking) > 0" ; + } + $row = $GLOBALS['dbi']->fetchArray($relation->queryAsControlUser($sql_query)); + return isset($row[0]) + ? $row[0] + : -1; + } + + + /** + * Gets the record of a tracking job. + * + * @param string $dbname name of database + * @param string $tablename name of table + * @param string $version version number + * + * @static + * + * @return mixed record DDM log, DDL log, structure snapshot, tracked + * statements. + */ + public static function getTrackedData($dbname, $tablename, $version) + { + $relation = new Relation($GLOBALS['dbi']); + + $sql_query = " SELECT * FROM " . self::_getTrackingTable() . + " WHERE `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' "; + if (! empty($tablename)) { + $sql_query .= " AND `table_name` = '" + . $GLOBALS['dbi']->escapeString($tablename) . "' "; + } + $sql_query .= " AND `version` = '" . $GLOBALS['dbi']->escapeString($version) + . "' ORDER BY `version` DESC LIMIT 1"; + + $mixed = $GLOBALS['dbi']->fetchAssoc($relation->queryAsControlUser($sql_query)); + + // PHP 7.4 fix for accessing array offset on null + if (! is_array($mixed)) { + $mixed = [ + 'schema_sql' => null, + 'data_sql' => null, + 'tracking' => null, + 'schema_snapshot' => null, + ]; + } + + // Parse log + $log_schema_entries = explode('# log ', (string) $mixed['schema_sql']); + $log_data_entries = explode('# log ', (string) $mixed['data_sql']); + + $ddl_date_from = $date = Util::date('Y-m-d H:i:s'); + + $ddlog = []; + $first_iteration = true; + + // Iterate tracked data definition statements + // For each log entry we want to get date, username and statement + foreach ($log_schema_entries as $log_entry) { + if (trim($log_entry) != '') { + $date = mb_substr($log_entry, 0, 19); + $username = mb_substr( + $log_entry, + 20, + mb_strpos($log_entry, "\n") - 20 + ); + if ($first_iteration) { + $ddl_date_from = $date; + $first_iteration = false; + } + $statement = rtrim(mb_strstr($log_entry, "\n")); + + $ddlog[] = [ + 'date' => $date, + 'username' => $username, + 'statement' => $statement, + ]; + } + } + + $date_from = $ddl_date_from; + $ddl_date_to = $date; + + $dml_date_from = $date_from; + + $dmlog = []; + $first_iteration = true; + + // Iterate tracked data manipulation statements + // For each log entry we want to get date, username and statement + foreach ($log_data_entries as $log_entry) { + if (trim($log_entry) != '') { + $date = mb_substr($log_entry, 0, 19); + $username = mb_substr( + $log_entry, + 20, + mb_strpos($log_entry, "\n") - 20 + ); + if ($first_iteration) { + $dml_date_from = $date; + $first_iteration = false; + } + $statement = rtrim(mb_strstr($log_entry, "\n")); + + $dmlog[] = [ + 'date' => $date, + 'username' => $username, + 'statement' => $statement, + ]; + } + } + + $dml_date_to = $date; + + // Define begin and end of date range for both logs + $data = []; + if (strtotime($ddl_date_from) <= strtotime($dml_date_from)) { + $data['date_from'] = $ddl_date_from; + } else { + $data['date_from'] = $dml_date_from; + } + if (strtotime($ddl_date_to) >= strtotime($dml_date_to)) { + $data['date_to'] = $ddl_date_to; + } else { + $data['date_to'] = $dml_date_to; + } + $data['ddlog'] = $ddlog; + $data['dmlog'] = $dmlog; + $data['tracking'] = $mixed['tracking']; + $data['schema_snapshot'] = $mixed['schema_snapshot']; + + return $data; + } + + + /** + * Parses a query. Gets + * - statement identifier (UPDATE, ALTER TABLE, ...) + * - type of statement, is it part of DDL or DML ? + * - tablename + * + * @param string $query query + * + * @static + * @todo: using PMA SQL Parser when possible + * @todo: support multi-table/view drops + * + * @return mixed Array containing identifier, type and tablename. + * + */ + public static function parseQuery($query) + { + // Usage of PMA_SQP does not work here + // + // require_once("libraries/sqlparser.lib.php"); + // $parsed_sql = PMA_SQP_parse($query); + // $sql_info = PMA_SQP_analyze($parsed_sql); + + $parser = new Parser($query); + + $tokens = $parser->list->tokens; + + // Parse USE statement, need it for SQL dump imports + if ($tokens[0]->value == 'USE') { + $GLOBALS['db'] = $tokens[2]->value; + } + + $result = []; + + if (! empty($parser->statements)) { + $statement = $parser->statements[0]; + $options = isset($statement->options) ? $statement->options->options : null; + + /* + * DDL statements + */ + $result['type'] = 'DDL'; + + // Parse CREATE statement + if ($statement instanceof CreateStatement) { + if (empty($options) || ! isset($options[6])) { + return $result; + } + + if ($options[6] == 'VIEW' || $options[6] == 'TABLE') { + $result['identifier'] = 'CREATE ' . $options[6]; + $result['tablename'] = $statement->name->table ; + } elseif ($options[6] == 'DATABASE') { + $result['identifier'] = 'CREATE DATABASE' ; + $result['tablename'] = '' ; + + // In case of CREATE DATABASE, table field of the CreateStatement is actually name of the database + $GLOBALS['db'] = $statement->name->table; + } elseif ($options[6] == 'INDEX' + || $options[6] == 'UNIQUE INDEX' + || $options[6] == 'FULLTEXT INDEX' + || $options[6] == 'SPATIAL INDEX' + ) { + $result['identifier'] = 'CREATE INDEX'; + + // In case of CREATE INDEX, we have to get the table name from body of the statement + $result['tablename'] = $statement->body[3]->value == '.' ? $statement->body[4]->value + : $statement->body[2]->value ; + } + } elseif ($statement instanceof AlterStatement) { // Parse ALTER statement + if (empty($options) || ! isset($options[3])) { + return $result; + } + + if ($options[3] == 'VIEW' || $options[3] == 'TABLE') { + $result['identifier'] = 'ALTER ' . $options[3] ; + $result['tablename'] = $statement->table->table ; + } elseif ($options[3] == 'DATABASE') { + $result['identifier'] = 'ALTER DATABASE' ; + $result['tablename'] = '' ; + + $GLOBALS['db'] = $statement->table->table ; + } + } elseif ($statement instanceof DropStatement) { // Parse DROP statement + if (empty($options) || ! isset($options[1])) { + return $result; + } + + if ($options[1] == 'VIEW' || $options[1] == 'TABLE') { + $result['identifier'] = 'DROP ' . $options[1] ; + $result['tablename'] = $statement->fields[0]->table; + } elseif ($options[1] == 'DATABASE') { + $result['identifier'] = 'DROP DATABASE' ; + $result['tablename'] = ''; + + $GLOBALS['db'] = $statement->fields[0]->table; + } elseif ($options[1] == 'INDEX') { + $result['identifier'] = 'DROP INDEX' ; + $result['tablename'] = $statement->table->table; + } + } elseif ($statement instanceof RenameStatement) { // Parse RENAME statement + $result['identifier'] = 'RENAME TABLE'; + $result['tablename'] = $statement->renames[0]->old->table; + $result['tablename_after_rename'] = $statement->renames[0]->new->table; + } + + if (isset($result['identifier'])) { + return $result ; + } + + /* + * DML statements + */ + $result['type'] = 'DML'; + + // Parse UPDATE statement + if ($statement instanceof UpdateStatement) { + $result['identifier'] = 'UPDATE'; + $result['tablename'] = $statement->tables[0]->table; + } + + // Parse INSERT INTO statement + if ($statement instanceof InsertStatement) { + $result['identifier'] = 'INSERT'; + $result['tablename'] = $statement->into->dest->table; + } + + // Parse DELETE statement + if ($statement instanceof DeleteStatement) { + $result['identifier'] = 'DELETE'; + $result['tablename'] = $statement->from[0]->table; + } + + // Parse TRUNCATE statement + if ($statement instanceof TruncateStatement) { + $result['identifier'] = 'TRUNCATE' ; + $result['tablename'] = $statement->table->table; + } + } + + return $result; + } + + + /** + * Analyzes a given SQL statement and saves tracking data. + * + * @param string $query a SQL query + * + * @static + * + * @return void + */ + public static function handleQuery($query) + { + $relation = new Relation($GLOBALS['dbi']); + + // If query is marked as untouchable, leave + if (mb_strstr($query, "/*NOTRACK*/")) { + return; + } + + if (! (substr($query, -1) == ';')) { + $query .= ";\n"; + } + // Get some information about query + $result = self::parseQuery($query); + + // Get database name + $dbname = trim(isset($GLOBALS['db']) ? $GLOBALS['db'] : '', '`'); + // $dbname can be empty, for example when coming from Synchronize + // and this is a query for the remote server + if (empty($dbname)) { + return; + } + + // If we found a valid statement + if (isset($result['identifier'])) { + if (! self::isTracked($dbname, $result['tablename'])) { + return; + } + + $version = self::getVersion( + $dbname, + $result['tablename'], + $result['identifier'] + ); + + // If version not exists and auto-creation is enabled + if ($GLOBALS['cfg']['Server']['tracking_version_auto_create'] == true + && $version == -1 + ) { + // Create the version + + switch ($result['identifier']) { + case 'CREATE TABLE': + self::createVersion($dbname, $result['tablename'], '1'); + break; + case 'CREATE VIEW': + self::createVersion( + $dbname, + $result['tablename'], + '1', + '', + true + ); + break; + case 'CREATE DATABASE': + self::createDatabaseVersion($dbname, '1', $query); + break; + } // end switch + } + + // If version exists + if ($version != -1) { + if ($result['type'] == 'DDL') { + $save_to = 'schema_sql'; + } elseif ($result['type'] == 'DML') { + $save_to = 'data_sql'; + } else { + $save_to = ''; + } + $date = Util::date('Y-m-d H:i:s'); + + // Cut off `dbname`. from query + $query = preg_replace( + '/`' . preg_quote($dbname, '/') . '`\s?\./', + '', + $query + ); + + // Add log information + $query = self::getLogComment() . $query ; + + // Mark it as untouchable + $sql_query = " /*NOTRACK*/\n" + . " UPDATE " . self::_getTrackingTable() + . " SET " . Util::backquote($save_to) + . " = CONCAT( " . Util::backquote($save_to) . ",'\n" + . $GLOBALS['dbi']->escapeString($query) . "') ," + . " `date_updated` = '" . $date . "' "; + + // If table was renamed we have to change + // the tablename attribute in pma_tracking too + if ($result['identifier'] == 'RENAME TABLE') { + $sql_query .= ', `table_name` = \'' + . $GLOBALS['dbi']->escapeString($result['tablename_after_rename']) + . '\' '; + } + + // Save the tracking information only for + // 1. the database + // 2. the table / view + // 3. the statements + // we want to track + $sql_query .= + " WHERE FIND_IN_SET('" . $result['identifier'] . "',tracking) > 0" . + " AND `db_name` = '" . $GLOBALS['dbi']->escapeString($dbname) . "' " . + " AND `table_name` = '" + . $GLOBALS['dbi']->escapeString($result['tablename']) . "' " . + " AND `version` = '" . $GLOBALS['dbi']->escapeString($version) . "' "; + + $relation->queryAsControlUser($sql_query); + } + } + } + + /** + * Returns the tracking table + * + * @return string tracking table + */ + private static function _getTrackingTable() + { + $relation = new Relation($GLOBALS['dbi']); + $cfgRelation = $relation->getRelationsParam(); + return Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['tracking']); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Tracking.php b/srcs/phpmyadmin/libraries/classes/Tracking.php new file mode 100644 index 0000000..a0a7c1d --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Tracking.php @@ -0,0 +1,1320 @@ +sqlQueryForm = $sqlQueryForm; + $this->template = $template; + $this->relation = $relation; + } + + /** + * Filters tracking entries + * + * @param array $data the entries to filter + * @param string $filter_ts_from "from" date + * @param string $filter_ts_to "to" date + * @param array $filter_users users + * + * @return array filtered entries + */ + public function filter( + array $data, + $filter_ts_from, + $filter_ts_to, + array $filter_users + ): array { + $tmp_entries = []; + $id = 0; + foreach ($data as $entry) { + $timestamp = strtotime($entry['date']); + $filtered_user = in_array($entry['username'], $filter_users); + if ($timestamp >= $filter_ts_from + && $timestamp <= $filter_ts_to + && (in_array('*', $filter_users) || $filtered_user) + ) { + $tmp_entries[] = [ + 'id' => $id, + 'timestamp' => $timestamp, + 'username' => $entry['username'], + 'statement' => $entry['statement'], + ]; + } + $id++; + } + return $tmp_entries; + } + + /** + * Function to get html for data definition and data manipulation statements + * + * @param string $urlQuery url query + * @param int $lastVersion last version + * @param string $db database + * @param array $selected selected tables + * @param string $type type of the table; table, view or both + * + * @return string HTML + */ + public function getHtmlForDataDefinitionAndManipulationStatements( + $urlQuery, + $lastVersion, + $db, + array $selected, + $type = 'both' + ) { + return $this->template->render('create_tracking_version', [ + 'url_query' => $urlQuery, + 'last_version' => $lastVersion, + 'db' => $db, + 'selected' => $selected, + 'type' => $type, + 'default_statements' => $GLOBALS['cfg']['Server']['tracking_default_statements'], + ]); + } + + /** + * Function to get html for activate/deactivate tracking + * + * @param string $action activate|deactivate + * @param string $urlQuery url query + * @param int $lastVersion last version + * + * @return string HTML + */ + public function getHtmlForActivateDeactivateTracking( + $action, + $urlQuery, + $lastVersion + ) { + return $this->template->render('table/tracking/activate_deactivate', [ + 'action' => $action, + 'url_query' => $urlQuery, + 'last_version' => $lastVersion, + 'db' => $GLOBALS['db'], + 'table' => $GLOBALS['table'], + ]); + } + + /** + * Function to get the list versions of the table + * + * @return array + */ + public function getListOfVersionsOfTable() + { + $relation = $this->relation; + $cfgRelation = $relation->getRelationsParam(); + $sql_query = " SELECT * FROM " . + Util::backquote($cfgRelation['db']) . "." . + Util::backquote($cfgRelation['tracking']) . + " WHERE db_name = '" . $GLOBALS['dbi']->escapeString($GLOBALS['db']) . + "' " . + " AND table_name = '" . + $GLOBALS['dbi']->escapeString($GLOBALS['table']) . "' " . + " ORDER BY version DESC "; + + return $relation->queryAsControlUser($sql_query); + } + + /** + * Function to get html for main page parts that do not use $_REQUEST + * + * @param string $urlQuery url query + * @param array $urlParams url parameters + * @param string $pmaThemeImage path to theme's image folder + * @param string $textDir text direction + * @param int $lastVersion last tracking version + * + * @return string + */ + public function getHtmlForMainPage( + $urlQuery, + $urlParams, + $pmaThemeImage, + $textDir, + $lastVersion = null + ) { + $selectableTablesSqlResult = $this->getSqlResultForSelectableTables(); + $selectableTablesEntries = []; + while ($entry = $GLOBALS['dbi']->fetchArray($selectableTablesSqlResult)) { + $entry['is_tracked'] = Tracker::isTracked( + $entry['db_name'], + $entry['table_name'] + ); + $selectableTablesEntries[] = $entry; + } + $selectableTablesNumRows = $GLOBALS['dbi']->numRows($selectableTablesSqlResult); + + $versionSqlResult = $this->getListOfVersionsOfTable(); + if ($lastVersion === null) { + $lastVersion = $this->getTableLastVersionNumber($versionSqlResult); + } + $GLOBALS['dbi']->dataSeek($versionSqlResult, 0); + $versions = []; + while ($version = $GLOBALS['dbi']->fetchArray($versionSqlResult)) { + $versions[] = $version; + } + + $type = $GLOBALS['dbi']->getTable($GLOBALS['db'], $GLOBALS['table']) + ->isView() ? 'view' : 'table'; + + return $this->template->render('table/tracking/main', [ + 'url_query' => $urlQuery, + 'url_params' => $urlParams, + 'db' => $GLOBALS['db'], + 'table' => $GLOBALS['table'], + 'selectable_tables_num_rows' => $selectableTablesNumRows, + 'selectable_tables_entries' => $selectableTablesEntries, + 'selected_table' => isset($_POST['table']) ? $_POST['table'] : null, + 'last_version' => $lastVersion, + 'versions' => $versions, + 'type' => $type, + 'default_statements' => $GLOBALS['cfg']['Server']['tracking_default_statements'], + 'pmaThemeImage' => $pmaThemeImage, + 'text_dir' => $textDir, + ]); + } + + /** + * Function to get the last version number of a table + * + * @param array $sql_result sql result + * + * @return int + */ + public function getTableLastVersionNumber($sql_result) + { + $maxversion = $GLOBALS['dbi']->fetchArray($sql_result); + return intval(is_array($maxversion) ? $maxversion['version'] : null); + } + + /** + * Function to get sql results for selectable tables + * + * @return array + */ + public function getSqlResultForSelectableTables() + { + $relation = $this->relation; + $cfgRelation = $relation->getRelationsParam(); + + $sql_query = " SELECT DISTINCT db_name, table_name FROM " . + Util::backquote($cfgRelation['db']) . "." . + Util::backquote($cfgRelation['tracking']) . + " WHERE db_name = '" . $GLOBALS['dbi']->escapeString($GLOBALS['db']) . + "' " . + " ORDER BY db_name, table_name"; + + return $relation->queryAsControlUser($sql_query); + } + + /** + * Function to get html for tracking report and tracking report export + * + * @param string $url_query url query + * @param array $data data + * @param array $url_params url params + * @param boolean $selection_schema selection schema + * @param boolean $selection_data selection data + * @param boolean $selection_both selection both + * @param int $filter_ts_to filter time stamp from + * @param int $filter_ts_from filter time stamp tp + * @param array $filter_users filter users + * + * @return string + */ + public function getHtmlForTrackingReport( + $url_query, + array $data, + array $url_params, + $selection_schema, + $selection_data, + $selection_both, + $filter_ts_to, + $filter_ts_from, + array $filter_users + ) { + $html = '

    ' . __('Tracking report') + . ' [' . __('Close') + . ']

    '; + + $html .= '' . __('Tracking statements') . ' ' + . htmlspecialchars($data['tracking']) . '
    '; + $html .= '
    '; + + list($str1, $str2, $str3, $str4, $str5) = $this->getHtmlForElementsOfTrackingReport( + $selection_schema, + $selection_data, + $selection_both + ); + + // Prepare delete link content here + $drop_image_or_text = ''; + if (Util::showIcons('ActionLinksMode')) { + $drop_image_or_text .= Util::getImage( + 'b_drop', + __('Delete tracking data row from report') + ); + } + if (Util::showText('ActionLinksMode')) { + $drop_image_or_text .= __('Delete'); + } + + /* + * First, list tracked data definition statements + */ + if (count($data['ddlog']) == 0 && count($data['dmlog']) === 0) { + $msg = Message::notice(__('No data')); + $msg->display(); + } + + $html .= $this->getHtmlForTrackingReportExportForm1( + $data, + $url_params, + $selection_schema, + $selection_data, + $selection_both, + $filter_ts_to, + $filter_ts_from, + $filter_users, + $str1, + $str2, + $str3, + $str4, + $str5, + $drop_image_or_text + ); + + $html .= $this->getHtmlForTrackingReportExportForm2( + $url_params, + $str1, + $str2, + $str3, + $str4, + $str5 + ); + + $html .= "



    \n"; + + return $html; + } + + /** + * Generate HTML element for report form + * + * @param boolean $selection_schema selection schema + * @param boolean $selection_data selection data + * @param boolean $selection_both selection both + * + * @return array + */ + public function getHtmlForElementsOfTrackingReport( + $selection_schema, + $selection_data, + $selection_both + ) { + $str1 = ''; + $str2 = ''; + $str3 = ''; + $str4 = ''; + $str5 = '' + . ''; + return [ + $str1, + $str2, + $str3, + $str4, + $str5, + ]; + } + + /** + * Generate HTML for export form + * + * @param array $data data + * @param array $url_params url params + * @param boolean $selection_schema selection schema + * @param boolean $selection_data selection data + * @param boolean $selection_both selection both + * @param int $filter_ts_to filter time stamp from + * @param int $filter_ts_from filter time stamp tp + * @param array $filter_users filter users + * @param string $str1 HTML for logtype select + * @param string $str2 HTML for "from date" + * @param string $str3 HTML for "to date" + * @param string $str4 HTML for user + * @param string $str5 HTML for "list report" + * @param string $drop_image_or_text HTML for image or text + * + * @return string HTML for form + */ + public function getHtmlForTrackingReportExportForm1( + array $data, + array $url_params, + $selection_schema, + $selection_data, + $selection_both, + $filter_ts_to, + $filter_ts_from, + array $filter_users, + $str1, + $str2, + $str3, + $str4, + $str5, + $drop_image_or_text + ) { + $ddlog_count = 0; + + $html = '
    '; + $html .= Url::getHiddenInputs($url_params + [ + 'report' => 'true', + 'version' => $_POST['version'], + ]); + + $html .= sprintf( + __('Show %1$s with dates from %2$s to %3$s by user %4$s %5$s'), + $str1, + $str2, + $str3, + $str4, + $str5 + ); + + if ($selection_schema || $selection_both && count($data['ddlog']) > 0) { + list($temp, $ddlog_count) = $this->getHtmlForDataDefinitionStatements( + $data, + $filter_users, + $filter_ts_from, + $filter_ts_to, + $url_params, + $drop_image_or_text + ); + $html .= $temp; + unset($temp); + } //endif + + /* + * Secondly, list tracked data manipulation statements + */ + if (($selection_data || $selection_both) && count($data['dmlog']) > 0) { + $html .= $this->getHtmlForDataManipulationStatements( + $data, + $filter_users, + $filter_ts_from, + $filter_ts_to, + $url_params, + $ddlog_count, + $drop_image_or_text + ); + } + $html .= '
    '; + return $html; + } + + /** + * Generate HTML for export form + * + * @param array $url_params Parameters + * @param string $str1 HTML for logtype select + * @param string $str2 HTML for "from date" + * @param string $str3 HTML for "to date" + * @param string $str4 HTML for user + * @param string $str5 HTML for "list report" + * + * @return string HTML for form + */ + public function getHtmlForTrackingReportExportForm2( + array $url_params, + $str1, + $str2, + $str3, + $str4, + $str5 + ) { + $html = '
    '; + $html .= Url::getHiddenInputs($url_params + [ + 'report' => 'true', + 'version' => $_POST['version'], + ]); + + $html .= sprintf( + __('Show %1$s with dates from %2$s to %3$s by user %4$s %5$s'), + $str1, + $str2, + $str3, + $str4, + $str5 + ); + $html .= '
    '; + + $html .= '
    '; + $html .= Url::getHiddenInputs($url_params + [ + 'report' => 'true', + 'version' => $_POST['version'], + 'logtype' => $_POST['logtype'], + 'date_from' => $_POST['date_from'], + 'date_to' => $_POST['date_to'], + 'users' => $_POST['users'], + 'report_export' => 'true', + ]); + + $str_export1 = ''; + + $str_export2 = ''; + + $html .= "
    " . sprintf(__('Export as %s'), $str_export1) + . $str_export2 . "
    "; + $html .= '
    '; + return $html; + } + + /** + * Function to get html for data manipulation statements + * + * @param array $data data + * @param array $filter_users filter users + * @param int $filter_ts_from filter time staml from + * @param int $filter_ts_to filter time stamp to + * @param array $url_params url parameters + * @param int $ddlog_count data definition log count + * @param string $drop_image_or_text drop image or text + * + * @return string + */ + public function getHtmlForDataManipulationStatements( + array $data, + array $filter_users, + $filter_ts_from, + $filter_ts_to, + array $url_params, + $ddlog_count, + $drop_image_or_text + ) { + // no need for the secondth returned parameter + list($html,) = $this->getHtmlForDataStatements( + $data, + $filter_users, + $filter_ts_from, + $filter_ts_to, + $url_params, + $drop_image_or_text, + 'dmlog', + __('Data manipulation statement'), + $ddlog_count, + 'dml_versions' + ); + + return $html; + } + + /** + * Function to get html for data definition statements in schema snapshot + * + * @param array $data data + * @param array $filter_users filter users + * @param int $filter_ts_from filter time stamp from + * @param int $filter_ts_to filter time stamp to + * @param array $url_params url parameters + * @param string $drop_image_or_text drop image or text + * + * @return array + */ + public function getHtmlForDataDefinitionStatements( + array $data, + array $filter_users, + $filter_ts_from, + $filter_ts_to, + array $url_params, + $drop_image_or_text + ) { + list($html, $line_number) = $this->getHtmlForDataStatements( + $data, + $filter_users, + $filter_ts_from, + $filter_ts_to, + $url_params, + $drop_image_or_text, + 'ddlog', + __('Data definition statement'), + 1, + 'ddl_versions' + ); + + return [ + $html, + $line_number, + ]; + } + + /** + * Function to get html for data statements in schema snapshot + * + * @param array $data data + * @param array $filterUsers filter users + * @param int $filterTsFrom filter time stamp from + * @param int $filterTsTo filter time stamp to + * @param array $urlParams url parameters + * @param string $dropImageOrText drop image or text + * @param string $whichLog dmlog|ddlog + * @param string $headerMessage message for this section + * @param int $lineNumber line number + * @param string $tableId id for the table element + * + * @return array [$html, $lineNumber] + */ + private function getHtmlForDataStatements( + array $data, + array $filterUsers, + $filterTsFrom, + $filterTsTo, + array $urlParams, + $dropImageOrText, + $whichLog, + $headerMessage, + $lineNumber, + $tableId + ) { + $offset = $lineNumber; + $entries = []; + foreach ($data[$whichLog] as $entry) { + $timestamp = strtotime($entry['date']); + if ($timestamp >= $filterTsFrom + && $timestamp <= $filterTsTo + && (in_array('*', $filterUsers) + || in_array($entry['username'], $filterUsers)) + ) { + $entry['formated_statement'] = Util::formatSql($entry['statement'], true); + $deleteParam = 'delete_' . $whichLog; + $entry['url_params'] = Url::getCommon($urlParams + [ + 'report' => 'true', + 'version' => $_POST['version'], + $deleteParam => $lineNumber - $offset, + ], ''); + $entry['line_number'] = $lineNumber; + $entries[] = $entry; + } + $lineNumber++; + } + + $html = $this->template->render('table/tracking/report_table', [ + 'table_id' => $tableId, + 'header_message' => $headerMessage, + 'entries' => $entries, + 'drop_image_or_text' => $dropImageOrText, + ]); + + return [ + $html, + $lineNumber, + ]; + } + + /** + * Function to get html for schema snapshot + * + * @param string $url_query url query + * + * @return string + */ + public function getHtmlForSchemaSnapshot($url_query) + { + $html = '

    ' . __('Structure snapshot') + . ' [' . __('Close') + . ']

    '; + $data = Tracker::getTrackedData( + $_POST['db'], + $_POST['table'], + $_POST['version'] + ); + + // Get first DROP TABLE/VIEW and CREATE TABLE/VIEW statements + $drop_create_statements = $data['ddlog'][0]['statement']; + + if (mb_strstr($data['ddlog'][0]['statement'], 'DROP TABLE') + || mb_strstr($data['ddlog'][0]['statement'], 'DROP VIEW') + ) { + $drop_create_statements .= $data['ddlog'][1]['statement']; + } + // Print SQL code + $html .= Util::getMessage( + sprintf( + __('Version %s snapshot (SQL code)'), + htmlspecialchars($_POST['version']) + ), + $drop_create_statements + ); + + // Unserialize snapshot + $temp = Core::safeUnserialize($data['schema_snapshot']); + if ($temp === null) { + $temp = [ + 'COLUMNS' => [], + 'INDEXES' => [], + ]; + } + $columns = $temp['COLUMNS']; + $indexes = $temp['INDEXES']; + $html .= $this->getHtmlForColumns($columns); + + if (count($indexes) > 0) { + $html .= $this->getHtmlForIndexes($indexes); + } // endif + $html .= '


    '; + + return $html; + } + + /** + * Function to get html for displaying columns in the schema snapshot + * + * @param array $columns columns + * + * @return string + */ + public function getHtmlForColumns(array $columns) + { + return $this->template->render('table/tracking/structure_snapshot_columns', [ + 'columns' => $columns, + ]); + } + + /** + * Function to get html for the indexes in schema snapshot + * + * @param array $indexes indexes + * + * @return string + */ + public function getHtmlForIndexes(array $indexes) + { + return $this->template->render('table/tracking/structure_snapshot_indexes', [ + 'indexes' => $indexes, + ]); + } + + /** + * Function to handle the tracking report + * + * @param array $data tracked data + * + * @return string HTML for the message + */ + public function deleteTrackingReportRows(array &$data) + { + $html = ''; + if (isset($_POST['delete_ddlog'])) { + // Delete ddlog row data + $html .= $this->deleteFromTrackingReportLog( + $data, + 'ddlog', + 'DDL', + __('Tracking data definition successfully deleted') + ); + } + + if (isset($_POST['delete_dmlog'])) { + // Delete dmlog row data + $html .= $this->deleteFromTrackingReportLog( + $data, + 'dmlog', + 'DML', + __('Tracking data manipulation successfully deleted') + ); + } + return $html; + } + + /** + * Function to delete from a tracking report log + * + * @param array $data tracked data + * @param string $which_log ddlog|dmlog + * @param string $type DDL|DML + * @param string $message success message + * + * @return string HTML for the message + */ + public function deleteFromTrackingReportLog(array &$data, $which_log, $type, $message) + { + $html = ''; + $delete_id = $_POST['delete_' . $which_log]; + + // Only in case of valid id + if ($delete_id == (int) $delete_id) { + unset($data[$which_log][$delete_id]); + + $successfullyDeleted = Tracker::changeTrackingData( + $GLOBALS['db'], + $GLOBALS['table'], + $_POST['version'], + $type, + $data[$which_log] + ); + if ($successfullyDeleted) { + $msg = Message::success($message); + } else { + $msg = Message::rawError(__('Query error')); + } + $html .= $msg->getDisplay(); + } + return $html; + } + + /** + * Function to export as sql dump + * + * @param array $entries entries + * + * @return string HTML SQL query form + */ + public function exportAsSqlDump(array $entries) + { + $html = ''; + $new_query = "# " + . __( + 'You can execute the dump by creating and using a temporary database. ' + . 'Please ensure that you have the privileges to do so.' + ) + . "\n" + . "# " . __('Comment out these two lines if you do not need them.') . "\n" + . "\n" + . "CREATE database IF NOT EXISTS pma_temp_db; \n" + . "USE pma_temp_db; \n" + . "\n"; + + foreach ($entries as $entry) { + $new_query .= $entry['statement']; + } + $msg = Message::success( + __('SQL statements exported. Please copy the dump or execute it.') + ); + $html .= $msg->getDisplay(); + + $db_temp = $GLOBALS['db']; + $table_temp = $GLOBALS['table']; + + $GLOBALS['db'] = $GLOBALS['table'] = ''; + + $html .= $this->sqlQueryForm->getHtml($new_query, 'sql'); + + $GLOBALS['db'] = $db_temp; + $GLOBALS['table'] = $table_temp; + + return $html; + } + + /** + * Function to export as sql execution + * + * @param array $entries entries + * + * @return array + */ + public function exportAsSqlExecution(array $entries) + { + $sql_result = []; + foreach ($entries as $entry) { + $sql_result = $GLOBALS['dbi']->query("/*NOTRACK*/\n" . $entry['statement']); + } + + return $sql_result; + } + + /** + * Function to export as entries + * + * @param array $entries entries + * + * @return void + */ + public function exportAsFileDownload(array $entries) + { + ini_set('url_rewriter.tags', ''); + + // Replace all multiple whitespaces by a single space + $table = htmlspecialchars(preg_replace('/\s+/', ' ', $_POST['table'])); + $dump = "# " . sprintf( + __('Tracking report for table `%s`'), + $table + ) + . "\n" . '# ' . date('Y-m-d H:i:s') . "\n"; + foreach ($entries as $entry) { + $dump .= $entry['statement']; + } + $filename = 'log_' . $table . '.sql'; + Response::getInstance()->disable(); + Core::downloadHeader( + $filename, + 'text/x-sql', + strlen($dump) + ); + echo $dump; + + exit; + } + + /** + * Function to activate or deactivate tracking + * + * @param string $action activate|deactivate + * + * @return string HTML for the success message + */ + public function changeTracking($action) + { + $html = ''; + if ($action == 'activate') { + $method = 'activateTracking'; + $message = __('Tracking for %1$s was activated at version %2$s.'); + } else { + $method = 'deactivateTracking'; + $message = __('Tracking for %1$s was deactivated at version %2$s.'); + } + $status = Tracker::$method( + $GLOBALS['db'], + $GLOBALS['table'], + $_POST['version'] + ); + if ($status) { + $msg = Message::success( + sprintf( + $message, + htmlspecialchars($GLOBALS['db'] . '.' . $GLOBALS['table']), + htmlspecialchars($_POST['version']) + ) + ); + $html .= $msg->getDisplay(); + } + + return $html; + } + + /** + * Function to get tracking set + * + * @return string + */ + public function getTrackingSet() + { + $tracking_set = ''; + + // a key is absent from the request if it has been removed from + // tracking_default_statements in the config + if (isset($_POST['alter_table']) && $_POST['alter_table'] == true) { + $tracking_set .= 'ALTER TABLE,'; + } + if (isset($_POST['rename_table']) && $_POST['rename_table'] == true) { + $tracking_set .= 'RENAME TABLE,'; + } + if (isset($_POST['create_table']) && $_POST['create_table'] == true) { + $tracking_set .= 'CREATE TABLE,'; + } + if (isset($_POST['drop_table']) && $_POST['drop_table'] == true) { + $tracking_set .= 'DROP TABLE,'; + } + if (isset($_POST['alter_view']) && $_POST['alter_view'] == true) { + $tracking_set .= 'ALTER VIEW,'; + } + if (isset($_POST['create_view']) && $_POST['create_view'] == true) { + $tracking_set .= 'CREATE VIEW,'; + } + if (isset($_POST['drop_view']) && $_POST['drop_view'] == true) { + $tracking_set .= 'DROP VIEW,'; + } + if (isset($_POST['create_index']) && $_POST['create_index'] == true) { + $tracking_set .= 'CREATE INDEX,'; + } + if (isset($_POST['drop_index']) && $_POST['drop_index'] == true) { + $tracking_set .= 'DROP INDEX,'; + } + if (isset($_POST['insert']) && $_POST['insert'] == true) { + $tracking_set .= 'INSERT,'; + } + if (isset($_POST['update']) && $_POST['update'] == true) { + $tracking_set .= 'UPDATE,'; + } + if (isset($_POST['delete']) && $_POST['delete'] == true) { + $tracking_set .= 'DELETE,'; + } + if (isset($_POST['truncate']) && $_POST['truncate'] == true) { + $tracking_set .= 'TRUNCATE,'; + } + $tracking_set = rtrim($tracking_set, ','); + + return $tracking_set; + } + + /** + * Deletes a tracking version + * + * @param string $version tracking version + * + * @return string HTML of the success message + */ + public function deleteTrackingVersion($version) + { + $html = ''; + $versionDeleted = Tracker::deleteTracking( + $GLOBALS['db'], + $GLOBALS['table'], + $version + ); + if ($versionDeleted) { + $msg = Message::success( + sprintf( + __('Version %1$s of %2$s was deleted.'), + htmlspecialchars($version), + htmlspecialchars($GLOBALS['db'] . '.' . $GLOBALS['table']) + ) + ); + $html .= $msg->getDisplay(); + } + + return $html; + } + + /** + * Function to create the tracking version + * + * @return string HTML of the success message + */ + public function createTrackingVersion() + { + $html = ''; + $tracking_set = $this->getTrackingSet(); + + $versionCreated = Tracker::createVersion( + $GLOBALS['db'], + $GLOBALS['table'], + $_POST['version'], + $tracking_set, + $GLOBALS['dbi']->getTable($GLOBALS['db'], $GLOBALS['table'])->isView() + ); + if ($versionCreated) { + $msg = Message::success( + sprintf( + __('Version %1$s was created, tracking for %2$s is active.'), + htmlspecialchars($_POST['version']), + htmlspecialchars($GLOBALS['db'] . '.' . $GLOBALS['table']) + ) + ); + $html .= $msg->getDisplay(); + } + + return $html; + } + + /** + * Create tracking version for multiple tables + * + * @param array $selected list of selected tables + * + * @return void + */ + public function createTrackingForMultipleTables(array $selected) + { + $tracking_set = $this->getTrackingSet(); + + foreach ($selected as $selected_table) { + Tracker::createVersion( + $GLOBALS['db'], + $selected_table, + $_POST['version'], + $tracking_set, + $GLOBALS['dbi']->getTable($GLOBALS['db'], $selected_table)->isView() + ); + } + } + + /** + * Function to get the entries + * + * @param array $data data + * @param int $filter_ts_from filter time stamp from + * @param int $filter_ts_to filter time stamp to + * @param array $filter_users filter users + * + * @return array + */ + public function getEntries(array $data, $filter_ts_from, $filter_ts_to, array $filter_users) + { + $entries = []; + // Filtering data definition statements + if ($_POST['logtype'] == 'schema' + || $_POST['logtype'] == 'schema_and_data' + ) { + $entries = array_merge( + $entries, + $this->filter( + $data['ddlog'], + $filter_ts_from, + $filter_ts_to, + $filter_users + ) + ); + } + + // Filtering data manipulation statements + if ($_POST['logtype'] == 'data' + || $_POST['logtype'] == 'schema_and_data' + ) { + $entries = array_merge( + $entries, + $this->filter( + $data['dmlog'], + $filter_ts_from, + $filter_ts_to, + $filter_users + ) + ); + } + + // Sort it + $ids = $timestamps = $usernames = $statements = []; + foreach ($entries as $key => $row) { + $ids[$key] = $row['id']; + $timestamps[$key] = $row['timestamp']; + $usernames[$key] = $row['username']; + $statements[$key] = $row['statement']; + } + + array_multisort( + $timestamps, + SORT_ASC, + $ids, + SORT_ASC, + $usernames, + SORT_ASC, + $statements, + SORT_ASC, + $entries + ); + + return $entries; + } + + /** + * Function to get version status + * + * @param array $version version info + * + * @return string The status message + */ + public function getVersionStatus(array $version) + { + if ($version['tracking_active'] == 1) { + return __('active'); + } + + return __('not active'); + } + + /** + * Get HTML for tracked and untracked tables + * + * @param string $db current database + * @param string $urlQuery url query string + * @param string $pmaThemeImage path to theme's image folder + * @param string $textDir text direction + * + * @return string HTML + */ + public function getHtmlForDbTrackingTables( + string $db, + string $urlQuery, + string $pmaThemeImage, + string $textDir + ) { + $relation = $this->relation; + $cfgRelation = $relation->getRelationsParam(); + + // Prepare statement to get HEAD version + $allTablesQuery = ' SELECT table_name, MAX(version) as version FROM ' . + Util::backquote($cfgRelation['db']) . '.' . + Util::backquote($cfgRelation['tracking']) . + ' WHERE db_name = \'' . $GLOBALS['dbi']->escapeString($db) . + '\' ' . + ' GROUP BY table_name' . + ' ORDER BY table_name ASC'; + + $allTablesResult = $relation->queryAsControlUser($allTablesQuery); + $untrackedTables = $this->getUntrackedTables($db); + + // If a HEAD version exists + $versions = []; + $headVersionExists = is_object($allTablesResult) + && $GLOBALS['dbi']->numRows($allTablesResult) > 0; + if ($headVersionExists) { + while ($oneResult = $GLOBALS['dbi']->fetchArray($allTablesResult)) { + list($tableName, $versionNumber) = $oneResult; + $tableQuery = ' SELECT * FROM ' . + Util::backquote($cfgRelation['db']) . '.' . + Util::backquote($cfgRelation['tracking']) . + ' WHERE `db_name` = \'' + . $GLOBALS['dbi']->escapeString($db) + . '\' AND `table_name` = \'' + . $GLOBALS['dbi']->escapeString($tableName) + . '\' AND `version` = \'' . $versionNumber . '\''; + + $tableResult = $relation->queryAsControlUser($tableQuery); + $versionData = $GLOBALS['dbi']->fetchArray($tableResult); + $versionData['status_button'] = $this->getStatusButton( + $versionData, + $urlQuery + ); + $versions[] = $versionData; + } + } + + $html = $this->template->render('database/tracking/tables', [ + 'db' => $db, + 'head_version_exists' => $headVersionExists, + 'untracked_tables_exists' => count($untrackedTables) > 0, + 'versions' => $versions, + 'url_query' => $urlQuery, + 'text_dir' => $textDir, + 'untracked_tables' => $untrackedTables, + 'pma_theme_image' => $pmaThemeImage, + ]); + + return $html; + } + + /** + * Helper function: Recursive function for getting table names from $table_list + * + * @param array $table_list Table list + * @param string $db Current database + * @param boolean $testing Testing + * + * @return array + */ + public function extractTableNames(array $table_list, $db, $testing = false) + { + $untracked_tables = []; + $sep = $GLOBALS['cfg']['NavigationTreeTableSeparator']; + + foreach ($table_list as $key => $value) { + if (is_array($value) && array_key_exists('is' . $sep . 'group', $value) + && $value['is' . $sep . 'group'] + ) { + $untracked_tables = array_merge($this->extractTableNames($value, $db), $untracked_tables); //Recursion step + } else { + if (is_array($value) && ($testing || Tracker::getVersion($db, $value['Name']) == -1)) { + $untracked_tables[] = $value['Name']; + } + } + } + return $untracked_tables; + } + + + /** + * Get untracked tables + * + * @param string $db current database + * + * @return array + */ + public function getUntrackedTables($db) + { + $table_list = Util::getTableList($db); + //Use helper function to get table list recursively. + return $this->extractTableNames($table_list, $db); + } + + /** + * Get tracking status button + * + * @param array $versionData data about tracking versions + * @param string $urlQuery url query string + * + * @return string HTML + */ + private function getStatusButton(array $versionData, $urlQuery) + { + $state = $this->getVersionStatus($versionData); + $options = [ + 0 => [ + 'label' => __('not active'), + 'value' => 'deactivate_now', + 'selected' => $state != 'active', + ], + 1 => [ + 'label' => __('active'), + 'value' => 'activate_now', + 'selected' => $state == 'active', + ], + ]; + $link = 'tbl_tracking.php' . $urlQuery . '&table=' + . htmlspecialchars($versionData['table_name']) + . '&version=' . $versionData['version']; + + return Util::toggleButton( + $link, + 'toggle_activation', + $options, + null + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Transformations.php b/srcs/phpmyadmin/libraries/classes/Transformations.php new file mode 100644 index 0000000..adc2265 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Transformations.php @@ -0,0 +1,485 @@ + + * getOptions("'option ,, quoted',abd,'2,3',"); + * // array { + * // 'option ,, quoted', + * // 'abc', + * // '2,3', + * // '', + * // } + * + * + * @param string $option_string comma separated options + * + * @return array options + */ + public function getOptions($option_string) + { + $result = []; + + if (strlen($option_string) === 0 + || ! $transform_options = explode(",", $option_string) + ) { + return $result; + } + + while (($option = array_shift($transform_options)) !== null) { + $trimmed = trim($option); + if (strlen($trimmed) > 1 + && $trimmed[0] == "'" + && $trimmed[strlen($trimmed) - 1] == "'" + ) { + // '...' + $option = mb_substr($trimmed, 1, -1); + } elseif (isset($trimmed[0]) && $trimmed[0] == "'") { + // '..., + $trimmed = ltrim($option); + $rtrimmed = null; + while (($option = array_shift($transform_options)) !== null) { + // ..., + $trimmed .= ',' . $option; + $rtrimmed = rtrim($trimmed); + if ($rtrimmed[strlen($rtrimmed) - 1] == "'") { + // ,...' + break; + } + } + $option = mb_substr($rtrimmed, 1, -1); + } + $result[] = stripslashes($option); + } + + return $result; + } + + /** + * Gets all available MIME-types + * + * @access public + * @staticvar array mimetypes + * @return array array[mimetype], array[transformation] + */ + public function getAvailableMimeTypes() + { + static $stack = null; + + if (null !== $stack) { + return $stack; + } + + $stack = []; + $sub_dirs = [ + 'Input/' => 'input_', + 'Output/' => '', + '' => '', + ]; + + foreach ($sub_dirs as $sd => $prefix) { + $handle = opendir('libraries/classes/Plugins/Transformations/' . $sd); + + if (! $handle) { + $stack[$prefix . 'transformation'] = []; + $stack[$prefix . 'transformation_file'] = []; + continue; + } + + $filestack = []; + while ($file = readdir($handle)) { + // Ignore hidden files + if ($file[0] == '.') { + continue; + } + // Ignore old plugins (.class in filename) + if (strpos($file, '.class') !== false) { + continue; + } + $filestack[] = $file; + } + + closedir($handle); + sort($filestack); + + foreach ($filestack as $file) { + if (preg_match('|^[^.].*_.*_.*\.php$|', $file)) { + // File contains transformation functions. + $parts = explode('_', str_replace('.php', '', $file)); + $mimetype = $parts[0] . "/" . $parts[1]; + $stack['mimetype'][$mimetype] = $mimetype; + + $stack[$prefix . 'transformation'][] = $mimetype . ': ' . $parts[2]; + $stack[$prefix . 'transformation_file'][] = $sd . $file; + if ($sd === '') { + $stack['input_transformation'][] = $mimetype . ': ' . $parts[2]; + $stack['input_transformation_file'][] = $sd . $file; + } + } elseif (preg_match('|^[^.].*\.php$|', $file)) { + // File is a plain mimetype, no functions. + $base = str_replace('.php', '', $file); + + if ($base != 'global') { + $mimetype = str_replace('_', '/', $base); + $stack['mimetype'][$mimetype] = $mimetype; + $stack['empty_mimetype'][$mimetype] = $mimetype; + } + } + } + } + return $stack; + } + + /** + * Returns the class name of the transformation + * + * @param string $filename transformation file name + * + * @return string the class name of transformation + */ + public function getClassName($filename) + { + // get the transformation class name + $class_name = explode(".php", $filename); + $class_name = 'PhpMyAdmin\\' . str_replace('/', '\\', mb_substr($class_name[0], 18)); + + return $class_name; + } + + /** + * Returns the description of the transformation + * + * @param string $file transformation file + * + * @return string the description of the transformation + */ + public function getDescription($file) + { + $include_file = 'libraries/classes/Plugins/Transformations/' . $file; + /** @var TransformationsInterface $class_name */ + $class_name = $this->getClassName($include_file); + if (class_exists($class_name)) { + return $class_name::getInfo(); + } + return ''; + } + + /** + * Returns the name of the transformation + * + * @param string $file transformation file + * + * @return string the name of the transformation + */ + public function getName($file) + { + $include_file = 'libraries/classes/Plugins/Transformations/' . $file; + /** @var TransformationsInterface $class_name */ + $class_name = $this->getClassName($include_file); + if (class_exists($class_name)) { + return $class_name::getName(); + } + return ''; + } + + /** + * Fixups old MIME or transformation name to new one + * + * - applies some hardcoded fixups + * - adds spaces after _ and numbers + * - capitalizes words + * - removes back spaces + * + * @param string $value Value to fixup + * + * @return string + */ + public function fixUpMime($value) + { + $value = str_replace( + [ + "jpeg", + "png", + ], + [ + "JPEG", + "PNG", + ], + $value + ); + return str_replace( + ' ', + '', + ucwords( + preg_replace('/([0-9_]+)/', '$1 ', $value) + ) + ); + } + + /** + * Gets the mimetypes for all columns of a table + * + * @param string $db the name of the db to check for + * @param string $table the name of the table to check for + * @param boolean $strict whether to include only results having a mimetype set + * @param boolean $fullName whether to use full column names as the key + * + * @access public + * + * @return array|bool [field_name][field_key] = field_value + */ + public function getMime($db, $table, $strict = false, $fullName = false) + { + $relation = new Relation($GLOBALS['dbi']); + $cfgRelation = $relation->getRelationsParam(); + + if (! $cfgRelation['mimework']) { + return false; + } + + $com_qry = ''; + if ($fullName) { + $com_qry .= "SELECT CONCAT(" + . "`db_name`, '.', `table_name`, '.', `column_name`" + . ") AS column_name, "; + } else { + $com_qry = "SELECT `column_name`, "; + } + $com_qry .= '`mimetype`, + `transformation`, + `transformation_options`, + `input_transformation`, + `input_transformation_options` + FROM ' . Util::backquote($cfgRelation['db']) . '.' + . Util::backquote($cfgRelation['column_info']) . ' + WHERE `db_name` = \'' . $GLOBALS['dbi']->escapeString($db) . '\' + AND `table_name` = \'' . $GLOBALS['dbi']->escapeString($table) . '\' + AND ( `mimetype` != \'\'' . (! $strict ? ' + OR `transformation` != \'\' + OR `transformation_options` != \'\' + OR `input_transformation` != \'\' + OR `input_transformation_options` != \'\'' : '') . ')'; + $result = $GLOBALS['dbi']->fetchResult( + $com_qry, + 'column_name', + null, + DatabaseInterface::CONNECT_CONTROL + ); + + foreach ($result as $column => $values) { + // convert mimetype to new format (f.e. Text_Plain, etc) + $values['mimetype'] = $this->fixUpMime($values['mimetype']); + + // For transformation of form + // output/image_jpeg__inline.inc.php + // extract dir part. + $dir = explode('/', $values['transformation']); + $subdir = ''; + if (count($dir) === 2) { + $subdir = ucfirst($dir[0]) . '/'; + $values['transformation'] = $dir[1]; + } + + $values['transformation'] = $this->fixUpMime($values['transformation']); + $values['transformation'] = $subdir . $values['transformation']; + $result[$column] = $values; + } + + return $result; + } + + /** + * Set a single mimetype to a certain value. + * + * @param string $db the name of the db + * @param string $table the name of the table + * @param string $key the name of the column + * @param string $mimetype the mimetype of the column + * @param string $transformation the transformation of the column + * @param string $transformationOpts the transformation options of the column + * @param string $inputTransform the input transformation of the column + * @param string $inputTransformOpts the input transformation options of the column + * @param boolean $forcedelete force delete, will erase any existing + * comments for this column + * + * @access public + * + * @return boolean true, if comment-query was made. + */ + public function setMime( + $db, + $table, + $key, + $mimetype, + $transformation, + $transformationOpts, + $inputTransform, + $inputTransformOpts, + $forcedelete = false + ) { + $relation = new Relation($GLOBALS['dbi']); + $cfgRelation = $relation->getRelationsParam(); + + if (! $cfgRelation['mimework']) { + return false; + } + + // lowercase mimetype & transformation + $mimetype = mb_strtolower($mimetype); + $transformation = mb_strtolower($transformation); + + // Do we have any parameter to set? + $has_value = ( + strlen($mimetype) > 0 || + strlen($transformation) > 0 || + strlen($transformationOpts) > 0 || + strlen($inputTransform) > 0 || + strlen($inputTransformOpts) > 0 + ); + + $test_qry = ' + SELECT `mimetype`, + `comment` + FROM ' . Util::backquote($cfgRelation['db']) . '.' + . Util::backquote($cfgRelation['column_info']) . ' + WHERE `db_name` = \'' . $GLOBALS['dbi']->escapeString($db) . '\' + AND `table_name` = \'' . $GLOBALS['dbi']->escapeString($table) . '\' + AND `column_name` = \'' . $GLOBALS['dbi']->escapeString($key) . '\''; + + $test_rs = $relation->queryAsControlUser( + $test_qry, + true, + DatabaseInterface::QUERY_STORE + ); + + if ($test_rs && $GLOBALS['dbi']->numRows($test_rs) > 0) { + $row = @$GLOBALS['dbi']->fetchAssoc($test_rs); + $GLOBALS['dbi']->freeResult($test_rs); + + if (! $forcedelete && ($has_value || strlen($row['comment']) > 0)) { + $upd_query = 'UPDATE ' + . Util::backquote($cfgRelation['db']) . '.' + . Util::backquote($cfgRelation['column_info']) + . ' SET ' + . '`mimetype` = \'' + . $GLOBALS['dbi']->escapeString($mimetype) . '\', ' + . '`transformation` = \'' + . $GLOBALS['dbi']->escapeString($transformation) . '\', ' + . '`transformation_options` = \'' + . $GLOBALS['dbi']->escapeString($transformationOpts) . '\', ' + . '`input_transformation` = \'' + . $GLOBALS['dbi']->escapeString($inputTransform) . '\', ' + . '`input_transformation_options` = \'' + . $GLOBALS['dbi']->escapeString($inputTransformOpts) . '\''; + } else { + $upd_query = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['column_info']); + } + $upd_query .= ' + WHERE `db_name` = \'' . $GLOBALS['dbi']->escapeString($db) . '\' + AND `table_name` = \'' . $GLOBALS['dbi']->escapeString($table) + . '\' + AND `column_name` = \'' . $GLOBALS['dbi']->escapeString($key) + . '\''; + } elseif ($has_value) { + $upd_query = 'INSERT INTO ' + . Util::backquote($cfgRelation['db']) + . '.' . Util::backquote($cfgRelation['column_info']) + . ' (db_name, table_name, column_name, mimetype, ' + . 'transformation, transformation_options, ' + . 'input_transformation, input_transformation_options) ' + . ' VALUES(' + . '\'' . $GLOBALS['dbi']->escapeString($db) . '\',' + . '\'' . $GLOBALS['dbi']->escapeString($table) . '\',' + . '\'' . $GLOBALS['dbi']->escapeString($key) . '\',' + . '\'' . $GLOBALS['dbi']->escapeString($mimetype) . '\',' + . '\'' . $GLOBALS['dbi']->escapeString($transformation) . '\',' + . '\'' . $GLOBALS['dbi']->escapeString($transformationOpts) . '\',' + . '\'' . $GLOBALS['dbi']->escapeString($inputTransform) . '\',' + . '\'' . $GLOBALS['dbi']->escapeString($inputTransformOpts) . '\')'; + } + + if (isset($upd_query)) { + return $relation->queryAsControlUser($upd_query); + } + + return false; + } + + + /** + * GLOBAL Plugin functions + */ + + /** + * Delete related transformation details + * after deleting database. table or column + * + * @param string $db Database name + * @param string $table Table name + * @param string $column Column name + * + * @return boolean State of the query execution + */ + public function clear($db, $table = '', $column = '') + { + $relation = new Relation($GLOBALS['dbi']); + $cfgRelation = $relation->getRelationsParam(); + + if (! isset($cfgRelation['column_info'])) { + return false; + } + + $delete_sql = 'DELETE FROM ' + . Util::backquote($cfgRelation['db']) . '.' + . Util::backquote($cfgRelation['column_info']) + . ' WHERE '; + + if (($column != '') && ($table != '')) { + $delete_sql .= '`db_name` = \'' . $db . '\' AND ' + . '`table_name` = \'' . $table . '\' AND ' + . '`column_name` = \'' . $column . '\' '; + } elseif ($table != '') { + $delete_sql .= '`db_name` = \'' . $db . '\' AND ' + . '`table_name` = \'' . $table . '\' '; + } else { + $delete_sql .= '`db_name` = \'' . $db . '\' '; + } + + return $GLOBALS['dbi']->tryQuery($delete_sql); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Twig/CoreExtension.php b/srcs/phpmyadmin/libraries/classes/Twig/CoreExtension.php new file mode 100644 index 0000000..e6e2142 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Twig/CoreExtension.php @@ -0,0 +1,41 @@ + ['html']] + ), + new TwigFilter( + 'link', + 'PhpMyAdmin\Core::linkURL' + ), + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Twig/I18n/NodeTrans.php b/srcs/phpmyadmin/libraries/classes/Twig/I18n/NodeTrans.php new file mode 100644 index 0000000..6fae7aa --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Twig/I18n/NodeTrans.php @@ -0,0 +1,171 @@ +node). + * The attributes are automatically made available as array items ($this['name']). + * + * @param Node $body Body of node trans + * @param Node $plural Node plural + * @param AbstractExpression $count Node count + * @param Node $context Node context + * @param Node $notes Node notes + * @param int $lineno The line number + * @param string $tag The tag name associated with the Node + */ + public function __construct( + Node $body, + Node $plural = null, + AbstractExpression $count = null, + Node $context = null, + Node $notes = null, + $lineno, + $tag = null + ) { + $nodes = ['body' => $body]; + if (null !== $count) { + $nodes['count'] = $count; + } + if (null !== $plural) { + $nodes['plural'] = $plural; + } + if (null !== $context) { + $nodes['context'] = $context; + } + if (null !== $notes) { + $nodes['notes'] = $notes; + } + + Node::__construct($nodes, [], $lineno, $tag); + } + + /** + * Compiles the node to PHP. + * + * @param Compiler $compiler Node compiler + * + * @return void + */ + public function compile(Compiler $compiler) + { + $compiler->addDebugInfo($this); + + list($msg, $vars) = $this->compileString($this->getNode('body')); + + if ($this->hasNode('plural')) { + list($msg1, $vars1) = $this->compileString($this->getNode('plural')); + + $vars = array_merge($vars, $vars1); + } + + $function = $this->getTransFunction( + $this->hasNode('plural'), + $this->hasNode('context') + ); + + if ($this->hasNode('notes')) { + $message = trim($this->getNode('notes')->getAttribute('data')); + + // line breaks are not allowed cause we want a single line comment + $message = str_replace(["\n", "\r"], ' ', $message); + $compiler->write("// l10n: {$message}\n"); + } + + if ($vars) { + $compiler + ->write('echo strtr(' . $function . '(') + ->subcompile($msg) + ; + + if ($this->hasNode('plural')) { + $compiler + ->raw(', ') + ->subcompile($msg1) + ->raw(', abs(') + ->subcompile($this->hasNode('count') ? $this->getNode('count') : null) + ->raw(')') + ; + } + + $compiler->raw('), array('); + + foreach ($vars as $var) { + if ('count' === $var->getAttribute('name')) { + $compiler + ->string('%count%') + ->raw(' => abs(') + ->subcompile($this->hasNode('count') ? $this->getNode('count') : null) + ->raw('), ') + ; + } else { + $compiler + ->string('%' . $var->getAttribute('name') . '%') + ->raw(' => ') + ->subcompile($var) + ->raw(', ') + ; + } + } + + $compiler->raw("));\n"); + } else { + $compiler->write('echo ' . $function . '('); + + if ($this->hasNode('context')) { + $context = trim($this->getNode('context')->getAttribute('data')); + $compiler->write('"' . $context . '", '); + } + + $compiler->subcompile($msg); + + if ($this->hasNode('plural')) { + $compiler + ->raw(', ') + ->subcompile($msg1) + ->raw(', abs(') + ->subcompile($this->hasNode('count') ? $this->getNode('count') : null) + ->raw(')') + ; + } + + $compiler->raw(");\n"); + } + } + + /** + * @param bool $plural Return plural or singular function to use + * @param bool $hasMsgContext It has message context? + * + * @return string + */ + protected function getTransFunction($plural, $hasMsgContext = false) + { + if ($hasMsgContext) { + return $plural ? '_ngettext' : '_pgettext'; + } + + return $plural ? '_ngettext' : '_gettext'; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Twig/I18n/TokenParserTrans.php b/srcs/phpmyadmin/libraries/classes/Twig/I18n/TokenParserTrans.php new file mode 100644 index 0000000..25aade0 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Twig/I18n/TokenParserTrans.php @@ -0,0 +1,85 @@ +getLine(); + $stream = $this->parser->getStream(); + $count = null; + $plural = null; + $notes = null; + $context = null; + + if (! $stream->test(Token::BLOCK_END_TYPE)) { + $body = $this->parser->getExpressionParser()->parseExpression(); + } else { + $stream->expect(Token::BLOCK_END_TYPE); + $body = $this->parser->subparse([$this, 'decideForFork']); + $next = $stream->next()->getValue(); + + if ('plural' === $next) { + $count = $this->parser->getExpressionParser()->parseExpression(); + $stream->expect(Token::BLOCK_END_TYPE); + $plural = $this->parser->subparse([$this, 'decideForFork']); + + if ('notes' === $stream->next()->getValue()) { + $stream->expect(Token::BLOCK_END_TYPE); + $notes = $this->parser->subparse([$this, 'decideForEnd'], true); + } + } elseif ('context' === $next) { + $stream->expect(Token::BLOCK_END_TYPE); + $context = $this->parser->subparse([$this, 'decideForEnd'], true); + } elseif ('notes' === $next) { + $stream->expect(Token::BLOCK_END_TYPE); + $notes = $this->parser->subparse([$this, 'decideForEnd'], true); + } + } + + $stream->expect(Token::BLOCK_END_TYPE); + + $this->checkTransString($body, $lineno); + + return new NodeTrans($body, $plural, $count, $context, $notes, $lineno, $this->getTag()); + } + + /** + * Tests the current token for a type. + * + * @param Token $token Twig token to test + * + * @return bool + */ + public function decideForFork(Token $token) + { + return $token->test(['plural', 'context', 'notes', 'endtrans']); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Twig/I18nExtension.php b/srcs/phpmyadmin/libraries/classes/Twig/I18nExtension.php new file mode 100644 index 0000000..4d0c14f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Twig/I18nExtension.php @@ -0,0 +1,45 @@ +getDisplay(); + }, + ['is_safe' => ['html']] + ), + new TwigFilter( + 'error', + function (string $string) { + return Message::error($string)->getDisplay(); + }, + ['is_safe' => ['html']] + ), + new TwigFilter( + 'raw_success', + function (string $string) { + return Message::rawSuccess($string)->getDisplay(); + }, + ['is_safe' => ['html']] + ), + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Twig/PluginsExtension.php b/srcs/phpmyadmin/libraries/classes/Twig/PluginsExtension.php new file mode 100644 index 0000000..1706a62 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Twig/PluginsExtension.php @@ -0,0 +1,52 @@ + ['html']] + ), + new TwigFunction( + 'get_choice', + 'PhpMyAdmin\Plugins::getChoice', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_default_plugin', + 'PhpMyAdmin\Plugins::getDefault', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_options', + 'PhpMyAdmin\Plugins::getOptions', + ['is_safe' => ['html']] + ), + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Twig/RelationExtension.php b/srcs/phpmyadmin/libraries/classes/Twig/RelationExtension.php new file mode 100644 index 0000000..6aca5d3 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Twig/RelationExtension.php @@ -0,0 +1,71 @@ + ['html']] + ), + new TwigFunction( + 'get_display_field', + [ + $relation, + 'getDisplayField', + ], + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_foreign_data', + [ + $relation, + 'getForeignData', + ] + ), + new TwigFunction( + 'get_tables', + [ + $relation, + 'getTables', + ] + ), + new TwigFunction( + 'search_column_in_foreigners', + [ + $relation, + 'searchColumnInForeigners', + ] + ), + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Twig/SanitizeExtension.php b/srcs/phpmyadmin/libraries/classes/Twig/SanitizeExtension.php new file mode 100644 index 0000000..0256109 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Twig/SanitizeExtension.php @@ -0,0 +1,64 @@ + ['html']] + ), + new TwigFilter( + 'js_format', + 'PhpMyAdmin\Sanitize::jsFormat', + ['is_safe' => ['html']] + ), + new TwigFilter( + 'sanitize', + 'PhpMyAdmin\Sanitize::sanitizeMessage', + ['is_safe' => ['html']] + ), + ]; + } + + /** + * Returns a list of functions to add to the existing list. + * + * @return TwigFunction[] + */ + public function getFunctions() + { + return [ + new TwigFunction( + 'get_js_value', + 'PhpMyAdmin\Sanitize::getJsValue', + ['is_safe' => ['html']] + ), + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Twig/ServerPrivilegesExtension.php b/srcs/phpmyadmin/libraries/classes/Twig/ServerPrivilegesExtension.php new file mode 100644 index 0000000..0a1363c --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Twig/ServerPrivilegesExtension.php @@ -0,0 +1,51 @@ + ['html']] + ), + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Twig/StorageEngineExtension.php b/srcs/phpmyadmin/libraries/classes/Twig/StorageEngineExtension.php new file mode 100644 index 0000000..34b87c1 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Twig/StorageEngineExtension.php @@ -0,0 +1,37 @@ + ['html']] + ), + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Twig/TableExtension.php b/srcs/phpmyadmin/libraries/classes/Twig/TableExtension.php new file mode 100644 index 0000000..1dc4ef1 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Twig/TableExtension.php @@ -0,0 +1,36 @@ + ['html']] + ), + new TwigFunction( + 'get_hidden_fields', + 'PhpMyAdmin\Url::getHiddenFields', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_common', + 'PhpMyAdmin\Url::getCommon', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_common_raw', + 'PhpMyAdmin\Url::getCommonRaw', + ['is_safe' => ['html']] + ), + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Twig/UtilExtension.php b/srcs/phpmyadmin/libraries/classes/Twig/UtilExtension.php new file mode 100644 index 0000000..809b07e --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Twig/UtilExtension.php @@ -0,0 +1,212 @@ + ['html']] + ), + new TwigFunction( + 'extract_column_spec', + 'PhpMyAdmin\Util::extractColumnSpec' + ), + new TwigFunction( + 'format_byte_down', + 'PhpMyAdmin\Util::formatByteDown' + ), + new TwigFunction( + 'format_number', + 'PhpMyAdmin\Util::formatNumber' + ), + new TwigFunction( + 'format_sql', + 'PhpMyAdmin\Util::formatSql', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_button_or_image', + 'PhpMyAdmin\Util::getButtonOrImage', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_docu_link', + 'PhpMyAdmin\Util::getDocuLink', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_list_navigator', + 'PhpMyAdmin\Util::getListNavigator', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'show_docu', + 'PhpMyAdmin\Util::showDocu', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_dropdown', + 'PhpMyAdmin\Util::getDropdown', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_fk_checkbox', + 'PhpMyAdmin\Util::getFKCheckbox', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_gis_datatypes', + 'PhpMyAdmin\Util::getGISDatatypes' + ), + new TwigFunction( + 'get_gis_functions', + 'PhpMyAdmin\Util::getGISFunctions' + ), + new TwigFunction( + 'get_html_tab', + 'PhpMyAdmin\Util::getHtmlTab', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_icon', + 'PhpMyAdmin\Util::getIcon', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_image', + 'PhpMyAdmin\Util::getImage', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_radio_fields', + 'PhpMyAdmin\Util::getRadioFields', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_select_upload_file_block', + 'PhpMyAdmin\Util::getSelectUploadFileBlock', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_script_name_for_option', + 'PhpMyAdmin\Util::getScriptNameForOption', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_start_and_number_of_rows_panel', + 'PhpMyAdmin\Util::getStartAndNumberOfRowsPanel', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_supported_datatypes', + 'PhpMyAdmin\Util::getSupportedDatatypes', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'is_foreign_key_supported', + 'PhpMyAdmin\Util::isForeignKeySupported' + ), + new TwigFunction( + 'link_or_button', + 'PhpMyAdmin\Util::linkOrButton', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'link_to_var_documentation', + 'PhpMyAdmin\Util::linkToVarDocumentation', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'localised_date', + 'PhpMyAdmin\Util::localisedDate' + ), + new TwigFunction( + 'show_hint', + 'PhpMyAdmin\Util::showHint', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'show_icons', + 'PhpMyAdmin\Util::showIcons' + ), + new TwigFunction( + 'show_mysql_docu', + 'PhpMyAdmin\Util::showMySQLDocu', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'get_mysql_docu_url', + 'PhpMyAdmin\Util::getMySQLDocuURL', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'show_php_docu', + 'PhpMyAdmin\Util::showPHPDocu', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'sortable_table_header', + 'PhpMyAdmin\Util::sortableTableHeader', + ['is_safe' => ['html']] + ), + new TwigFunction( + 'timespan_format', + 'PhpMyAdmin\Util::timespanFormat' + ), + new TwigFunction( + 'generate_hidden_max_file_size', + 'PhpMyAdmin\Util::generateHiddenMaxFileSize', + ['is_safe' => ['html']] + ), + ]; + } + + /** + * Returns a list of filters to add to the existing list. + * + * @return TwigFilter[] + */ + public function getFilters() + { + return [ + new TwigFilter( + 'convert_bit_default_value', + 'PhpMyAdmin\Util::convertBitDefaultValue' + ), + new TwigFilter( + 'escape_mysql_wildcards', + 'PhpMyAdmin\Util::convertBitDefaultValue' + ), + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/TwoFactor.php b/srcs/phpmyadmin/libraries/classes/TwoFactor.php new file mode 100644 index 0000000..cc26528 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/TwoFactor.php @@ -0,0 +1,303 @@ +initRelationParamsCache(); + + $this->userPreferences = new UserPreferences(); + $this->user = $user; + $this->_available = $this->getAvailable(); + $this->config = $this->readConfig(); + $this->_writable = ($this->config['type'] == 'db'); + $this->_backend = $this->getBackend(); + } + + /** + * Reads the configuration + * + * @return array + */ + public function readConfig() + { + $result = []; + $config = $this->userPreferences->load(); + if (isset($config['config_data']['2fa'])) { + $result = $config['config_data']['2fa']; + } + $result['type'] = $config['type']; + if (! isset($result['backend'])) { + $result['backend'] = ''; + } + if (! isset($result['settings'])) { + $result['settings'] = []; + } + return $result; + } + + /** + * Get any property of this class + * + * @param string $property name of the property + * + * @return mixed|void if property exist, value of the relevant property + */ + public function __get($property) + { + switch ($property) { + case 'backend': + return $this->_backend; + case 'available': + return $this->_available; + case 'writable': + return $this->_writable; + case 'showSubmit': + $backend = $this->_backend; + return $backend::$showSubmit; + } + } + + /** + * Returns list of available backends + * + * @return array + */ + public function getAvailable() + { + $result = []; + if ($GLOBALS['cfg']['DBG']['simple2fa']) { + $result[] = 'simple'; + } + if (class_exists(Google2FA::class)) { + $result[] = 'application'; + } + if (class_exists(U2FServer::class)) { + $result[] = 'key'; + } + return $result; + } + + /** + * Returns list of missing dependencies + * + * @return array + */ + public function getMissingDeps() + { + $result = []; + if (! class_exists(Google2FA::class)) { + $result[] = [ + 'class' => Application::getName(), + 'dep' => 'pragmarx/google2fa-qrcode', + ]; + } + if (! class_exists('BaconQrCode\Renderer\Image\Png')) { + $result[] = [ + 'class' => Application::getName(), + 'dep' => 'bacon/bacon-qr-code', + ]; + } + if (! class_exists(U2FServer::class)) { + $result[] = [ + 'class' => Key::getName(), + 'dep' => 'samyoul/u2f-php-server', + ]; + } + return $result; + } + + /** + * Returns class name for given name + * + * @param string $name Backend name + * + * @return string + */ + public function getBackendClass($name) + { + $result = TwoFactorPlugin::class; + if (in_array($name, $this->_available)) { + $result = 'PhpMyAdmin\\Plugins\\TwoFactor\\' . ucfirst($name); + } elseif (! empty($name)) { + $result = Invalid::class; + } + return $result; + } + + /** + * Returns backend for current user + * + * @return TwoFactorPlugin + */ + public function getBackend() + { + $name = $this->getBackendClass($this->config['backend']); + return new $name($this); + } + + /** + * Checks authentication, returns true on success + * + * @param boolean $skip_session Skip session cache + * + * @return boolean + */ + public function check($skip_session = false) + { + if ($skip_session) { + return $this->_backend->check(); + } + if (empty($_SESSION['two_factor_check'])) { + $_SESSION['two_factor_check'] = $this->_backend->check(); + } + return $_SESSION['two_factor_check']; + } + + /** + * Renders user interface to enter two-factor authentication + * + * @return string HTML code + */ + public function render() + { + return $this->_backend->getError() . $this->_backend->render(); + } + + /** + * Renders user interface to configure two-factor authentication + * + * @return string HTML code + */ + public function setup() + { + return $this->_backend->getError() . $this->_backend->setup(); + } + + /** + * Saves current configuration. + * + * @return true|Message + */ + public function save() + { + return $this->userPreferences->persistOption('2fa', $this->config, null); + } + + /** + * Changes two-factor authentication settings + * + * The object might stay in partialy changed setup + * if configuration fails. + * + * @param string $name Backend name + * + * @return boolean + */ + public function configure($name) + { + $this->config = [ + 'backend' => $name, + ]; + if ($name === '') { + $cls = $this->getBackendClass($name); + $this->config['settings'] = []; + $this->_backend = new $cls($this); + } else { + if (! in_array($name, $this->_available)) { + return false; + } + $cls = $this->getBackendClass($name); + $this->config['settings'] = []; + $this->_backend = new $cls($this); + if (! $this->_backend->configure()) { + return false; + } + } + $result = $this->save(); + if ($result !== true) { + $result->display(); + } + return true; + } + + /** + * Returns array with all available backends + * + * @return array + */ + public function getAllBackends() + { + $all = array_merge([''], $this->available); + $backends = []; + foreach ($all as $name) { + $cls = $this->getBackendClass($name); + $backends[] = [ + 'id' => $cls::$id, + 'name' => $cls::getName(), + 'description' => $cls::getDescription(), + ]; + } + return $backends; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Types.php b/srcs/phpmyadmin/libraries/classes/Types.php new file mode 100644 index 0000000..40bc15f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Types.php @@ -0,0 +1,875 @@ +_dbi = $dbi; + } + + /** + * Returns list of unary operators. + * + * @return string[] + */ + public function getUnaryOperators() + { + return [ + 'IS NULL', + 'IS NOT NULL', + "= ''", + "!= ''", + ]; + } + + /** + * Check whether operator is unary. + * + * @param string $op operator name + * + * @return boolean + */ + public function isUnaryOperator($op) + { + return in_array($op, $this->getUnaryOperators()); + } + + /** + * Returns list of operators checking for NULL. + * + * @return string[] + */ + public function getNullOperators() + { + return [ + 'IS NULL', + 'IS NOT NULL', + ]; + } + + /** + * ENUM search operators + * + * @return string[] + */ + public function getEnumOperators() + { + return [ + '=', + '!=', + ]; + } + + /** + * TEXT search operators + * + * @return string[] + */ + public function getTextOperators() + { + return [ + 'LIKE', + 'LIKE %...%', + 'NOT LIKE', + '=', + '!=', + 'REGEXP', + 'REGEXP ^...$', + 'NOT REGEXP', + "= ''", + "!= ''", + 'IN (...)', + 'NOT IN (...)', + 'BETWEEN', + 'NOT BETWEEN', + ]; + } + + /** + * Number search operators + * + * @return string[] + */ + public function getNumberOperators() + { + return [ + '=', + '>', + '>=', + '<', + '<=', + '!=', + 'LIKE', + 'LIKE %...%', + 'NOT LIKE', + 'IN (...)', + 'NOT IN (...)', + 'BETWEEN', + 'NOT BETWEEN', + ]; + } + + /** + * Returns operators for given type + * + * @param string $type Type of field + * @param boolean $null Whether field can be NULL + * + * @return string[] + */ + public function getTypeOperators($type, $null) + { + $ret = []; + $class = $this->getTypeClass($type); + + if (strncasecmp($type, 'enum', 4) == 0) { + $ret = array_merge($ret, $this->getEnumOperators()); + } elseif ($class == 'CHAR') { + $ret = array_merge($ret, $this->getTextOperators()); + } else { + $ret = array_merge($ret, $this->getNumberOperators()); + } + + if ($null) { + $ret = array_merge($ret, $this->getNullOperators()); + } + + return $ret; + } + + /** + * Returns operators for given type as html options + * + * @param string $type Type of field + * @param boolean $null Whether field can be NULL + * @param string $selectedOperator Option to be selected + * + * @return string Generated Html + */ + public function getTypeOperatorsHtml($type, $null, $selectedOperator = null) + { + $html = ''; + + foreach ($this->getTypeOperators($type, $null) as $fc) { + if (isset($selectedOperator) && $selectedOperator == $fc) { + $selected = ' selected="selected"'; + } else { + $selected = ''; + } + $html .= ''; + } + + return $html; + } + + /** + * Returns the data type description. + * + * @param string $type The data type to get a description. + * + * @return string + * + */ + public function getTypeDescription($type) + { + $type = mb_strtoupper($type); + switch ($type) { + case 'TINYINT': + return __( + 'A 1-byte integer, signed range is -128 to 127, unsigned range is ' . + '0 to 255' + ); + case 'SMALLINT': + return __( + 'A 2-byte integer, signed range is -32,768 to 32,767, unsigned ' . + 'range is 0 to 65,535' + ); + case 'MEDIUMINT': + return __( + 'A 3-byte integer, signed range is -8,388,608 to 8,388,607, ' . + 'unsigned range is 0 to 16,777,215' + ); + case 'INT': + return __( + 'A 4-byte integer, signed range is ' . + '-2,147,483,648 to 2,147,483,647, unsigned range is 0 to ' . + '4,294,967,295' + ); + case 'BIGINT': + return __( + 'An 8-byte integer, signed range is -9,223,372,036,854,775,808 ' . + 'to 9,223,372,036,854,775,807, unsigned range is 0 to ' . + '18,446,744,073,709,551,615' + ); + case 'DECIMAL': + return __( + 'A fixed-point number (M, D) - the maximum number of digits (M) ' . + 'is 65 (default 10), the maximum number of decimals (D) is 30 ' . + '(default 0)' + ); + case 'FLOAT': + return __( + 'A small floating-point number, allowable values are ' . + '-3.402823466E+38 to -1.175494351E-38, 0, and 1.175494351E-38 to ' . + '3.402823466E+38' + ); + case 'DOUBLE': + return __( + 'A double-precision floating-point number, allowable values are ' . + '-1.7976931348623157E+308 to -2.2250738585072014E-308, 0, and ' . + '2.2250738585072014E-308 to 1.7976931348623157E+308' + ); + case 'REAL': + return __( + 'Synonym for DOUBLE (exception: in REAL_AS_FLOAT SQL mode it is ' . + 'a synonym for FLOAT)' + ); + case 'BIT': + return __( + 'A bit-field type (M), storing M of bits per value (default is 1, ' . + 'maximum is 64)' + ); + case 'BOOLEAN': + return __( + 'A synonym for TINYINT(1), a value of zero is considered false, ' . + 'nonzero values are considered true' + ); + case 'SERIAL': + return __('An alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE'); + case 'DATE': + return sprintf( + __('A date, supported range is %1$s to %2$s'), + '1000-01-01', + '9999-12-31' + ); + case 'DATETIME': + return sprintf( + __('A date and time combination, supported range is %1$s to %2$s'), + '1000-01-01 00:00:00', + '9999-12-31 23:59:59' + ); + case 'TIMESTAMP': + return __( + 'A timestamp, range is 1970-01-01 00:00:01 UTC to 2038-01-09 ' . + '03:14:07 UTC, stored as the number of seconds since the epoch ' . + '(1970-01-01 00:00:00 UTC)' + ); + case 'TIME': + return sprintf( + __('A time, range is %1$s to %2$s'), + '-838:59:59', + '838:59:59' + ); + case 'YEAR': + return __( + "A year in four-digit (4, default) or two-digit (2) format, the " . + "allowable values are 70 (1970) to 69 (2069) or 1901 to 2155 and " . + "0000" + ); + case 'CHAR': + return __( + 'A fixed-length (0-255, default 1) string that is always ' . + 'right-padded with spaces to the specified length when stored' + ); + case 'VARCHAR': + return sprintf( + __( + 'A variable-length (%s) string, the effective maximum length ' . + 'is subject to the maximum row size' + ), + '0-65,535' + ); + case 'TINYTEXT': + return __( + 'A TEXT column with a maximum length of 255 (2^8 - 1) characters, ' . + 'stored with a one-byte prefix indicating the length of the value ' . + 'in bytes' + ); + case 'TEXT': + return __( + 'A TEXT column with a maximum length of 65,535 (2^16 - 1) ' . + 'characters, stored with a two-byte prefix indicating the length ' . + 'of the value in bytes' + ); + case 'MEDIUMTEXT': + return __( + 'A TEXT column with a maximum length of 16,777,215 (2^24 - 1) ' . + 'characters, stored with a three-byte prefix indicating the ' . + 'length of the value in bytes' + ); + case 'LONGTEXT': + return __( + 'A TEXT column with a maximum length of 4,294,967,295 or 4GiB ' . + '(2^32 - 1) characters, stored with a four-byte prefix indicating ' . + 'the length of the value in bytes' + ); + case 'BINARY': + return __( + 'Similar to the CHAR type, but stores binary byte strings rather ' . + 'than non-binary character strings' + ); + case 'VARBINARY': + return __( + 'Similar to the VARCHAR type, but stores binary byte strings ' . + 'rather than non-binary character strings' + ); + case 'TINYBLOB': + return __( + 'A BLOB column with a maximum length of 255 (2^8 - 1) bytes, ' . + 'stored with a one-byte prefix indicating the length of the value' + ); + case 'MEDIUMBLOB': + return __( + 'A BLOB column with a maximum length of 16,777,215 (2^24 - 1) ' . + 'bytes, stored with a three-byte prefix indicating the length of ' . + 'the value' + ); + case 'BLOB': + return __( + 'A BLOB column with a maximum length of 65,535 (2^16 - 1) bytes, ' . + 'stored with a two-byte prefix indicating the length of the value' + ); + case 'LONGBLOB': + return __( + 'A BLOB column with a maximum length of 4,294,967,295 or 4GiB ' . + '(2^32 - 1) bytes, stored with a four-byte prefix indicating the ' . + 'length of the value' + ); + case 'ENUM': + return __( + "An enumeration, chosen from the list of up to 65,535 values or " . + "the special '' error value" + ); + case 'SET': + return __("A single value chosen from a set of up to 64 members"); + case 'GEOMETRY': + return __('A type that can store a geometry of any type'); + case 'POINT': + return __('A point in 2-dimensional space'); + case 'LINESTRING': + return __('A curve with linear interpolation between points'); + case 'POLYGON': + return __('A polygon'); + case 'MULTIPOINT': + return __('A collection of points'); + case 'MULTILINESTRING': + return __( + 'A collection of curves with linear interpolation between points' + ); + case 'MULTIPOLYGON': + return __('A collection of polygons'); + case 'GEOMETRYCOLLECTION': + return __('A collection of geometry objects of any type'); + case 'JSON': + return __( + 'Stores and enables efficient access to data in JSON' + . ' (JavaScript Object Notation) documents' + ); + } + return ''; + } + + /** + * Returns class of a type, used for functions available for type + * or default values. + * + * @param string $type The data type to get a class. + * + * @return string + * + */ + public function getTypeClass($type) + { + $type = mb_strtoupper((string) $type); + switch ($type) { + case 'TINYINT': + case 'SMALLINT': + case 'MEDIUMINT': + case 'INT': + case 'BIGINT': + case 'DECIMAL': + case 'FLOAT': + case 'DOUBLE': + case 'REAL': + case 'BIT': + case 'BOOLEAN': + case 'SERIAL': + return 'NUMBER'; + + case 'DATE': + case 'DATETIME': + case 'TIMESTAMP': + case 'TIME': + case 'YEAR': + return 'DATE'; + + case 'CHAR': + case 'VARCHAR': + case 'TINYTEXT': + case 'TEXT': + case 'MEDIUMTEXT': + case 'LONGTEXT': + case 'BINARY': + case 'VARBINARY': + case 'TINYBLOB': + case 'MEDIUMBLOB': + case 'BLOB': + case 'LONGBLOB': + case 'ENUM': + case 'SET': + return 'CHAR'; + + case 'GEOMETRY': + case 'POINT': + case 'LINESTRING': + case 'POLYGON': + case 'MULTIPOINT': + case 'MULTILINESTRING': + case 'MULTIPOLYGON': + case 'GEOMETRYCOLLECTION': + return 'SPATIAL'; + + case 'JSON': + return 'JSON'; + } + + return ''; + } + + /** + * Returns array of functions available for a class. + * + * @param string $class The class to get function list. + * + * @return string[] + * + */ + public function getFunctionsClass($class) + { + $isMariaDB = $this->_dbi->isMariaDB(); + $serverVersion = $this->_dbi->getVersion(); + + switch ($class) { + case 'CHAR': + $ret = [ + 'AES_DECRYPT', + 'AES_ENCRYPT', + 'BIN', + 'CHAR', + 'COMPRESS', + 'CURRENT_USER', + 'DATABASE', + 'DAYNAME', + 'DES_DECRYPT', + 'DES_ENCRYPT', + 'ENCRYPT', + 'HEX', + 'INET6_NTOA', + 'INET_NTOA', + 'LOAD_FILE', + 'LOWER', + 'LTRIM', + 'MD5', + 'MONTHNAME', + 'OLD_PASSWORD', + 'PASSWORD', + 'QUOTE', + 'REVERSE', + 'RTRIM', + 'SHA1', + 'SOUNDEX', + 'SPACE', + 'TRIM', + 'UNCOMPRESS', + 'UNHEX', + 'UPPER', + 'USER', + 'UUID', + 'VERSION', + ]; + + if (($isMariaDB && $serverVersion < 100012) + || $serverVersion < 50603 + ) { + $ret = array_diff($ret, ['INET6_NTOA']); + } + return $ret; + + case 'DATE': + return [ + 'CURRENT_DATE', + 'CURRENT_TIME', + 'DATE', + 'FROM_DAYS', + 'FROM_UNIXTIME', + 'LAST_DAY', + 'NOW', + 'SEC_TO_TIME', + 'SYSDATE', + 'TIME', + 'TIMESTAMP', + 'UTC_DATE', + 'UTC_TIME', + 'UTC_TIMESTAMP', + 'YEAR', + ]; + + case 'NUMBER': + $ret = [ + 'ABS', + 'ACOS', + 'ASCII', + 'ASIN', + 'ATAN', + 'BIT_LENGTH', + 'BIT_COUNT', + 'CEILING', + 'CHAR_LENGTH', + 'CONNECTION_ID', + 'COS', + 'COT', + 'CRC32', + 'DAYOFMONTH', + 'DAYOFWEEK', + 'DAYOFYEAR', + 'DEGREES', + 'EXP', + 'FLOOR', + 'HOUR', + 'INET6_ATON', + 'INET_ATON', + 'LENGTH', + 'LN', + 'LOG', + 'LOG2', + 'LOG10', + 'MICROSECOND', + 'MINUTE', + 'MONTH', + 'OCT', + 'ORD', + 'PI', + 'QUARTER', + 'RADIANS', + 'RAND', + 'ROUND', + 'SECOND', + 'SIGN', + 'SIN', + 'SQRT', + 'TAN', + 'TO_DAYS', + 'TO_SECONDS', + 'TIME_TO_SEC', + 'UNCOMPRESSED_LENGTH', + 'UNIX_TIMESTAMP', + 'UUID_SHORT', + 'WEEK', + 'WEEKDAY', + 'WEEKOFYEAR', + 'YEARWEEK', + ]; + if (($isMariaDB && $serverVersion < 100012) + || $serverVersion < 50603 + ) { + $ret = array_diff($ret, ['INET6_ATON']); + } + return $ret; + + case 'SPATIAL': + if ($serverVersion >= 50600) { + return [ + 'ST_GeomFromText', + 'ST_GeomFromWKB', + + 'ST_GeomCollFromText', + 'ST_LineFromText', + 'ST_MLineFromText', + 'ST_PointFromText', + 'ST_MPointFromText', + 'ST_PolyFromText', + 'ST_MPolyFromText', + + 'ST_GeomCollFromWKB', + 'ST_LineFromWKB', + 'ST_MLineFromWKB', + 'ST_PointFromWKB', + 'ST_MPointFromWKB', + 'ST_PolyFromWKB', + 'ST_MPolyFromWKB', + ]; + } else { + return [ + 'GeomFromText', + 'GeomFromWKB', + + 'GeomCollFromText', + 'LineFromText', + 'MLineFromText', + 'PointFromText', + 'MPointFromText', + 'PolyFromText', + 'MPolyFromText', + + 'GeomCollFromWKB', + 'LineFromWKB', + 'MLineFromWKB', + 'PointFromWKB', + 'MPointFromWKB', + 'PolyFromWKB', + 'MPolyFromWKB', + ]; + } + } + return []; + } + + /** + * Returns array of functions available for a type. + * + * @param string $type The data type to get function list. + * + * @return string[] + * + */ + public function getFunctions($type) + { + $class = $this->getTypeClass($type); + return $this->getFunctionsClass($class); + } + + /** + * Returns array of all functions available. + * + * @return string[] + * + */ + public function getAllFunctions() + { + $ret = array_merge( + $this->getFunctionsClass('CHAR'), + $this->getFunctionsClass('NUMBER'), + $this->getFunctionsClass('DATE'), + $this->getFunctionsClass('UUID') + ); + sort($ret); + return $ret; + } + + /** + * Returns array of all attributes available. + * + * @return string[] + * + */ + public function getAttributes() + { + return [ + '', + 'BINARY', + 'UNSIGNED', + 'UNSIGNED ZEROFILL', + 'on update CURRENT_TIMESTAMP', + ]; + } + + /** + * Returns array of all column types available. + * + * VARCHAR, TINYINT, TEXT and DATE are listed first, based on + * estimated popularity. + * + * @return string[] + * + */ + public function getColumns() + { + $isMariaDB = $this->_dbi->isMariaDB(); + $serverVersion = $this->_dbi->getVersion(); + + // most used types + $ret = [ + 'INT', + 'VARCHAR', + 'TEXT', + 'DATE', + ]; + // numeric + $ret[_pgettext('numeric types', 'Numeric')] = [ + 'TINYINT', + 'SMALLINT', + 'MEDIUMINT', + 'INT', + 'BIGINT', + '-', + 'DECIMAL', + 'FLOAT', + 'DOUBLE', + 'REAL', + '-', + 'BIT', + 'BOOLEAN', + 'SERIAL', + ]; + + // Date/Time + $ret[_pgettext('date and time types', 'Date and time')] = [ + 'DATE', + 'DATETIME', + 'TIMESTAMP', + 'TIME', + 'YEAR', + ]; + + // Text + $ret[_pgettext('string types', 'String')] = [ + 'CHAR', + 'VARCHAR', + '-', + 'TINYTEXT', + 'TEXT', + 'MEDIUMTEXT', + 'LONGTEXT', + '-', + 'BINARY', + 'VARBINARY', + '-', + 'TINYBLOB', + 'BLOB', + 'MEDIUMBLOB', + 'LONGBLOB', + '-', + 'ENUM', + 'SET', + ]; + + $ret[_pgettext('spatial types', 'Spatial')] = [ + 'GEOMETRY', + 'POINT', + 'LINESTRING', + 'POLYGON', + 'MULTIPOINT', + 'MULTILINESTRING', + 'MULTIPOLYGON', + 'GEOMETRYCOLLECTION', + ]; + + if (($isMariaDB && $serverVersion > 100207) + || (! $isMariaDB && $serverVersion >= 50708)) { + $ret['JSON'] = [ + 'JSON', + ]; + } + + return $ret; + } + + /** + * Returns an array of integer types + * + * @return string[] integer types + */ + public function getIntegerTypes() + { + return [ + 'tinyint', + 'smallint', + 'mediumint', + 'int', + 'bigint', + ]; + } + + /** + * Returns the min and max values of a given integer type + * + * @param string $type integer type + * @param boolean $signed whether signed + * + * @return string[] min and max values + */ + public function getIntegerRange($type, $signed = true) + { + static $min_max_data = [ + 'unsigned' => [ + 'tinyint' => [ + '0', + '255', + ], + 'smallint' => [ + '0', + '65535', + ], + 'mediumint' => [ + '0', + '16777215', + ], + 'int' => [ + '0', + '4294967295', + ], + 'bigint' => [ + '0', + '18446744073709551615', + ], + ], + 'signed' => [ + 'tinyint' => [ + '-128', + '127', + ], + 'smallint' => [ + '-32768', + '32767', + ], + 'mediumint' => [ + '-8388608', + '8388607', + ], + 'int' => [ + '-2147483648', + '2147483647', + ], + 'bigint' => [ + '-9223372036854775808', + '9223372036854775807', + ], + ], + ]; + $relevantArray = $signed + ? $min_max_data['signed'] + : $min_max_data['unsigned']; + return isset($relevantArray[$type]) ? $relevantArray[$type] : [ + '', + '', + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Url.php b/srcs/phpmyadmin/libraries/classes/Url.php new file mode 100644 index 0000000..aeae50a --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Url.php @@ -0,0 +1,274 @@ + 0) { + $params['db'] = $db; + } + if (strlen((string) $table) > 0) { + $params['table'] = $table; + } + } + + if (! empty($GLOBALS['server']) + && $GLOBALS['server'] != $GLOBALS['cfg']['ServerDefault'] + ) { + $params['server'] = $GLOBALS['server']; + } + if (empty($PMA_Config->getCookie('pma_lang')) && ! empty($GLOBALS['lang'])) { + $params['lang'] = $GLOBALS['lang']; + } + + if (! is_array($skip)) { + if (isset($params[$skip])) { + unset($params[$skip]); + } + } else { + foreach ($skip as $skipping) { + if (isset($params[$skipping])) { + unset($params[$skipping]); + } + } + } + + return Url::getHiddenFields($params); + } + + /** + * create hidden form fields from array with name => value + * + * + * $values = array( + * 'aaa' => aaa, + * 'bbb' => array( + * 'bbb_0', + * 'bbb_1', + * ), + * 'ccc' => array( + * 'a' => 'ccc_a', + * 'b' => 'ccc_b', + * ), + * ); + * echo Url::getHiddenFields($values); + * + * // produces: + * + * + * + * + * + * + * + * @param array $values hidden values + * @param string $pre prefix + * @param bool $is_token if token already added in hidden input field + * + * @return string form fields of type hidden + */ + public static function getHiddenFields(array $values, $pre = '', $is_token = false) + { + $fields = ''; + + /* Always include token in plain forms */ + if ($is_token === false) { + $values['token'] = $_SESSION[' PMA_token ']; + } + + foreach ($values as $name => $value) { + if (! empty($pre)) { + $name = $pre . '[' . $name . ']'; + } + + if (is_array($value)) { + $fields .= Url::getHiddenFields($value, $name, true); + } else { + // do not generate an ending "\n" because + // Url::getHiddenInputs() is sometimes called + // from a JS document.write() + $fields .= ''; + } + } + + return $fields; + } + + /** + * Generates text with URL parameters. + * + * + * $params['myparam'] = 'myvalue'; + * $params['db'] = 'mysql'; + * $params['table'] = 'rights'; + * // note the missing ? + * echo 'script.php' . Url::getCommon($params); + * // produces with cookies enabled: + * // script.php?myparam=myvalue&db=mysql&table=rights + * // with cookies disabled: + * // script.php?server=1&lang=en&myparam=myvalue&db=mysql + * // &table=rights + * + * // note the missing ? + * echo 'script.php' . Url::getCommon(); + * // produces with cookies enabled: + * // script.php + * // with cookies disabled: + * // script.php?server=1&lang=en + * + * + * @param mixed $params optional, Contains an associative array with url params + * @param string $divider optional character to use instead of '?' + * + * @return string string with URL parameters + * @access public + */ + public static function getCommon($params = [], $divider = '?') + { + return htmlspecialchars( + Url::getCommonRaw($params, $divider) + ); + } + + /** + * Generates text with URL parameters. + * + * + * $params['myparam'] = 'myvalue'; + * $params['db'] = 'mysql'; + * $params['table'] = 'rights'; + * // note the missing ? + * echo 'script.php' . Url::getCommon($params); + * // produces with cookies enabled: + * // script.php?myparam=myvalue&db=mysql&table=rights + * // with cookies disabled: + * // script.php?server=1&lang=en&myparam=myvalue&db=mysql + * // &table=rights + * + * // note the missing ? + * echo 'script.php' . Url::getCommon(); + * // produces with cookies enabled: + * // script.php + * // with cookies disabled: + * // script.php?server=1&lang=en + * + * + * @param mixed $params optional, Contains an associative array with url params + * @param string $divider optional character to use instead of '?' + * + * @return string string with URL parameters + * @access public + */ + public static function getCommonRaw($params = [], $divider = '?') + { + /** @var Config $PMA_Config */ + global $PMA_Config; + $separator = Url::getArgSeparator(); + + // avoid overwriting when creating navi panel links to servers + if (isset($GLOBALS['server']) + && $GLOBALS['server'] != $GLOBALS['cfg']['ServerDefault'] + && ! isset($params['server']) + && ! $PMA_Config->get('is_setup') + ) { + $params['server'] = $GLOBALS['server']; + } + + if (empty($PMA_Config->getCookie('pma_lang')) && ! empty($GLOBALS['lang'])) { + $params['lang'] = $GLOBALS['lang']; + } + + $query = http_build_query($params, '', $separator); + + if ($divider != '?' || strlen($query) > 0) { + return $divider . $query; + } + + return ''; + } + + /** + * Returns url separator + * + * extracted from arg_separator.input as set in php.ini + * we do not use arg_separator.output to avoid problems with & and & + * + * @param string $encode whether to encode separator or not, + * currently 'none' or 'html' + * + * @return string character used for separating url parts usually ; or & + * @access public + */ + public static function getArgSeparator($encode = 'none') + { + static $separator = null; + static $html_separator = null; + + if (null === $separator) { + // use separators defined by php, but prefer ';' + // as recommended by W3C + // (see https://www.w3.org/TR/1999/REC-html401-19991224/appendix + // /notes.html#h-B.2.2) + $arg_separator = ini_get('arg_separator.input'); + if (mb_strpos($arg_separator, ';') !== false) { + $separator = ';'; + } elseif (strlen($arg_separator) > 0) { + $separator = $arg_separator[0]; + } else { + $separator = '&'; + } + $html_separator = htmlentities($separator); + } + + switch ($encode) { + case 'html': + return $html_separator; + case 'text': + case 'none': + default: + return $separator; + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/UserPassword.php b/srcs/phpmyadmin/libraries/classes/UserPassword.php new file mode 100644 index 0000000..f9ee4fb --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/UserPassword.php @@ -0,0 +1,286 @@ +serverPrivileges = $serverPrivileges; + } + + /** + * Send the message as an ajax request + * + * @param array $change_password_message Message to display + * @param string $sql_query SQL query executed + * + * @return void + */ + public function getChangePassMessage(array $change_password_message, $sql_query = '') + { + $response = Response::getInstance(); + if ($response->isAjax()) { + /** + * If in an Ajax request, we don't need to show the rest of the page + */ + if ($change_password_message['error']) { + $response->addJSON('message', $change_password_message['msg']); + $response->setRequestStatus(false); + } else { + $sql_query = Util::getMessage( + $change_password_message['msg'], + $sql_query, + 'success' + ); + $response->addJSON('message', $sql_query); + } + exit; + } + } + + /** + * Generate the message + * + * @return array error value and message + */ + public function setChangePasswordMsg() + { + $error = false; + $message = Message::success(__('The profile has been updated.')); + + if ($_POST['nopass'] != '1') { + if (strlen($_POST['pma_pw']) === 0 || strlen($_POST['pma_pw2']) === 0) { + $message = Message::error(__('The password is empty!')); + $error = true; + } elseif ($_POST['pma_pw'] !== $_POST['pma_pw2']) { + $message = Message::error( + __('The passwords aren\'t the same!') + ); + $error = true; + } elseif (strlen($_POST['pma_pw']) > 256) { + $message = Message::error(__('Password is too long!')); + $error = true; + } + } + return [ + 'error' => $error, + 'msg' => $message, + ]; + } + + /** + * Change the password + * + * @param string $password New password + * @param string $message Message + * @param array $change_password_message Message to show + * + * @return void + */ + public function changePassword($password, $message, array $change_password_message) + { + global $auth_plugin; + + $hashing_function = $this->changePassHashingFunction(); + + list($username, $hostname) = $GLOBALS['dbi']->getCurrentUserAndHost(); + + $serverType = Util::getServerType(); + $serverVersion = $GLOBALS['dbi']->getVersion(); + + if (isset($_POST['authentication_plugin']) + && ! empty($_POST['authentication_plugin']) + ) { + $orig_auth_plugin = $_POST['authentication_plugin']; + } else { + $orig_auth_plugin = $this->serverPrivileges->getCurrentAuthenticationPlugin( + 'change', + $username, + $hostname + ); + } + + $sql_query = 'SET password = ' + . (($password == '') ? '\'\'' : $hashing_function . '(\'***\')'); + + if ($serverType == 'MySQL' + && $serverVersion >= 50706 + ) { + $sql_query = 'ALTER USER \'' . $username . '\'@\'' . $hostname + . '\' IDENTIFIED WITH ' . $orig_auth_plugin . ' BY ' + . (($password == '') ? '\'\'' : '\'***\''); + } elseif (($serverType == 'MySQL' + && $serverVersion >= 50507) + || ($serverType == 'MariaDB' + && $serverVersion >= 50200) + ) { + // For MySQL versions 5.5.7+ and MariaDB versions 5.2+, + // explicitly set value of `old_passwords` so that + // it does not give an error while using + // the PASSWORD() function + if ($orig_auth_plugin == 'sha256_password') { + $value = 2; + } else { + $value = 0; + } + $GLOBALS['dbi']->tryQuery('SET `old_passwords` = ' . $value . ';'); + } + + $this->changePassUrlParamsAndSubmitQuery( + $username, + $hostname, + $password, + $sql_query, + $hashing_function, + $orig_auth_plugin + ); + + $auth_plugin->handlePasswordChange($password); + $this->getChangePassMessage($change_password_message, $sql_query); + $this->changePassDisplayPage($message, $sql_query); + } + + /** + * Generate the hashing function + * + * @return string + */ + private function changePassHashingFunction() + { + if (Core::isValid( + $_POST['authentication_plugin'], + 'identical', + 'mysql_old_password' + )) { + $hashing_function = 'OLD_PASSWORD'; + } else { + $hashing_function = 'PASSWORD'; + } + return $hashing_function; + } + + /** + * Changes password for a user + * + * @param string $username Username + * @param string $hostname Hostname + * @param string $password Password + * @param string $sql_query SQL query + * @param string $hashing_function Hashing function + * @param string $orig_auth_plugin Original Authentication Plugin + * + * @return void + */ + private function changePassUrlParamsAndSubmitQuery( + $username, + $hostname, + $password, + $sql_query, + $hashing_function, + $orig_auth_plugin + ) { + $err_url = 'user_password.php' . Url::getCommon(); + + $serverType = Util::getServerType(); + $serverVersion = $GLOBALS['dbi']->getVersion(); + + if ($serverType == 'MySQL' && $serverVersion >= 50706) { + $local_query = 'ALTER USER \'' . $username . '\'@\'' . $hostname . '\'' + . ' IDENTIFIED with ' . $orig_auth_plugin . ' BY ' + . (($password == '') + ? '\'\'' + : '\'' . $GLOBALS['dbi']->escapeString($password) . '\''); + } elseif ($serverType == 'MariaDB' + && $serverVersion >= 50200 + && $serverVersion < 100100 + && $orig_auth_plugin !== '' + ) { + if ($orig_auth_plugin == 'mysql_native_password') { + // Set the hashing method used by PASSWORD() + // to be 'mysql_native_password' type + $GLOBALS['dbi']->tryQuery('SET old_passwords = 0;'); + } elseif ($orig_auth_plugin == 'sha256_password') { + // Set the hashing method used by PASSWORD() + // to be 'sha256_password' type + $GLOBALS['dbi']->tryQuery('SET `old_passwords` = 2;'); + } + + $hashedPassword = $this->serverPrivileges->getHashedPassword($_POST['pma_pw']); + + $local_query = "UPDATE `mysql`.`user` SET" + . " `authentication_string` = '" . $hashedPassword + . "', `Password` = '', " + . " `plugin` = '" . $orig_auth_plugin . "'" + . " WHERE `User` = '" . $username . "' AND Host = '" + . $hostname . "';"; + } else { + $local_query = 'SET password = ' . (($password == '') + ? '\'\'' + : $hashing_function . '(\'' + . $GLOBALS['dbi']->escapeString($password) . '\')'); + } + if (! @$GLOBALS['dbi']->tryQuery($local_query)) { + Util::mysqlDie( + $GLOBALS['dbi']->getError(), + $sql_query, + false, + $err_url + ); + } + + // Flush privileges after successful password change + $GLOBALS['dbi']->tryQuery("FLUSH PRIVILEGES;"); + } + + /** + * Display the page + * + * @param string $message Message + * @param string $sql_query SQL query + * + * @return void + */ + private function changePassDisplayPage($message, $sql_query) + { + echo '

    ' , __('Change password') , '

    ' , "\n\n"; + echo Util::getMessage( + $message, + $sql_query, + 'success' + ); + echo '' , "\n" + , '' , __('Back') , ''; + exit; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/UserPreferences.php b/srcs/phpmyadmin/libraries/classes/UserPreferences.php new file mode 100644 index 0000000..3f737ac --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/UserPreferences.php @@ -0,0 +1,287 @@ +relation = new Relation($GLOBALS['dbi']); + $this->template = new Template(); + } + + /** + * Common initialization for user preferences modification pages + * + * @param ConfigFile $cf Config file instance + * + * @return void + */ + public function pageInit(ConfigFile $cf) + { + $forms_all_keys = UserFormList::getFields(); + $cf->resetConfigData(); // start with a clean instance + $cf->setAllowedKeys($forms_all_keys); + $cf->setCfgUpdateReadMapping( + [ + 'Server/hide_db' => 'Servers/1/hide_db', + 'Server/only_db' => 'Servers/1/only_db', + ] + ); + $cf->updateWithGlobalConfig($GLOBALS['cfg']); + } + + /** + * Loads user preferences + * + * Returns an array: + * * config_data - path => value pairs + * * mtime - last modification time + * * type - 'db' (config read from pmadb) or 'session' (read from user session) + * + * @return array + */ + public function load() + { + $cfgRelation = $this->relation->getRelationsParam(); + if (! $cfgRelation['userconfigwork']) { + // no pmadb table, use session storage + if (! isset($_SESSION['userconfig'])) { + $_SESSION['userconfig'] = [ + 'db' => [], + 'ts' => time(), + ]; + } + return [ + 'config_data' => $_SESSION['userconfig']['db'], + 'mtime' => $_SESSION['userconfig']['ts'], + 'type' => 'session', + ]; + } + // load configuration from pmadb + $query_table = Util::backquote($cfgRelation['db']) . '.' + . Util::backquote($cfgRelation['userconfig']); + $query = 'SELECT `config_data`, UNIX_TIMESTAMP(`timevalue`) ts' + . ' FROM ' . $query_table + . ' WHERE `username` = \'' + . $GLOBALS['dbi']->escapeString($cfgRelation['user']) + . '\''; + $row = $GLOBALS['dbi']->fetchSingleRow($query, 'ASSOC', DatabaseInterface::CONNECT_CONTROL); + + return [ + 'config_data' => $row ? json_decode($row['config_data'], true) : [], + 'mtime' => $row ? $row['ts'] : time(), + 'type' => 'db', + ]; + } + + /** + * Saves user preferences + * + * @param array $config_array configuration array + * + * @return true|Message + */ + public function save(array $config_array) + { + $cfgRelation = $this->relation->getRelationsParam(); + $server = isset($GLOBALS['server']) + ? $GLOBALS['server'] + : $GLOBALS['cfg']['ServerDefault']; + $cache_key = 'server_' . $server; + if (! $cfgRelation['userconfigwork']) { + // no pmadb table, use session storage + $_SESSION['userconfig'] = [ + 'db' => $config_array, + 'ts' => time(), + ]; + if (isset($_SESSION['cache'][$cache_key]['userprefs'])) { + unset($_SESSION['cache'][$cache_key]['userprefs']); + } + return true; + } + + // save configuration to pmadb + $query_table = Util::backquote($cfgRelation['db']) . '.' + . Util::backquote($cfgRelation['userconfig']); + $query = 'SELECT `username` FROM ' . $query_table + . ' WHERE `username` = \'' + . $GLOBALS['dbi']->escapeString($cfgRelation['user']) + . '\''; + + $has_config = $GLOBALS['dbi']->fetchValue( + $query, + 0, + 0, + DatabaseInterface::CONNECT_CONTROL + ); + $config_data = json_encode($config_array); + if ($has_config) { + $query = 'UPDATE ' . $query_table + . ' SET `timevalue` = NOW(), `config_data` = \'' + . $GLOBALS['dbi']->escapeString($config_data) + . '\'' + . ' WHERE `username` = \'' + . $GLOBALS['dbi']->escapeString($cfgRelation['user']) + . '\''; + } else { + $query = 'INSERT INTO ' . $query_table + . ' (`username`, `timevalue`,`config_data`) ' + . 'VALUES (\'' + . $GLOBALS['dbi']->escapeString($cfgRelation['user']) . '\', NOW(), ' + . '\'' . $GLOBALS['dbi']->escapeString($config_data) . '\')'; + } + if (isset($_SESSION['cache'][$cache_key]['userprefs'])) { + unset($_SESSION['cache'][$cache_key]['userprefs']); + } + if (! $GLOBALS['dbi']->tryQuery($query, DatabaseInterface::CONNECT_CONTROL)) { + $message = Message::error(__('Could not save configuration')); + $message->addMessage( + Message::rawError( + $GLOBALS['dbi']->getError(DatabaseInterface::CONNECT_CONTROL) + ), + '

    ' + ); + return $message; + } + return true; + } + + /** + * Returns a user preferences array filtered by $cfg['UserprefsDisallow'] + * (blacklist) and keys from user preferences form (whitelist) + * + * @param array $config_data path => value pairs + * + * @return array + */ + public function apply(array $config_data) + { + $cfg = []; + $blacklist = array_flip($GLOBALS['cfg']['UserprefsDisallow']); + $whitelist = array_flip(UserFormList::getFields()); + // whitelist some additional fields which are custom handled + $whitelist['ThemeDefault'] = true; + $whitelist['lang'] = true; + $whitelist['Server/hide_db'] = true; + $whitelist['Server/only_db'] = true; + $whitelist['2fa'] = true; + foreach ($config_data as $path => $value) { + if (! isset($whitelist[$path]) || isset($blacklist[$path])) { + continue; + } + Core::arrayWrite($path, $cfg, $value); + } + return $cfg; + } + + /** + * Updates one user preferences option (loads and saves to database). + * + * No validation is done! + * + * @param string $path configuration + * @param mixed $value value + * @param mixed $default_value default value + * + * @return true|Message + */ + public function persistOption($path, $value, $default_value) + { + $prefs = $this->load(); + if ($value === $default_value) { + if (isset($prefs['config_data'][$path])) { + unset($prefs['config_data'][$path]); + } else { + return true; + } + } else { + $prefs['config_data'][$path] = $value; + } + return $this->save($prefs['config_data']); + } + + /** + * Redirects after saving new user preferences + * + * @param string $file_name Filename + * @param array|null $params URL parameters + * @param string $hash Hash value + * + * @return void + */ + public function redirect( + $file_name, + $params = null, + $hash = null + ) { + // redirect + $url_params = ['saved' => 1]; + if (is_array($params)) { + $url_params = array_merge($params, $url_params); + } + if ($hash) { + $hash = '#' . urlencode($hash); + } + Core::sendHeaderLocation('./' . $file_name + . Url::getCommonRaw($url_params) . $hash); + } + + /** + * Shows form which allows to quickly load + * settings stored in browser's local storage + * + * @return string + */ + public function autoloadGetHeader() + { + if (isset($_REQUEST['prefs_autoload']) + && $_REQUEST['prefs_autoload'] == 'hide' + ) { + $_SESSION['userprefs_autoload'] = true; + return ''; + } + + $script_name = basename(basename($GLOBALS['PMA_PHP_SELF'])); + $return_url = $script_name . '?' . http_build_query($_GET, '', '&'); + + return $this->template->render('preferences/autoload', [ + 'hidden_inputs' => Url::getHiddenInputs(), + 'return_url' => $return_url, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/UserPreferencesHeader.php b/srcs/phpmyadmin/libraries/classes/UserPreferencesHeader.php new file mode 100644 index 0000000..cecbb05 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/UserPreferencesHeader.php @@ -0,0 +1,148 @@ + 'prefs_manage.php', + 'text' => __('Manage your settings'), + ] + ) . "\n"; + /* Second authentication factor */ + $content .= Util::getHtmlTab( + [ + 'link' => 'prefs_twofactor.php', + 'text' => __('Two-factor authentication'), + ] + ) . "\n"; + + $content .= self::displayTabsWithIcon(); + + return $template->render( + 'list/unordered', + [ + 'id' => 'topmenu2', + 'class' => 'user_prefs_tabs', + 'content' => $content, + ] + ) . '
    '; + } + + /** + * @return string + */ + protected static function displayTabsWithIcon(): string + { + $form_param = $_GET['form'] ?? null; + $tabs_icons = [ + 'Features' => 'b_tblops', + 'Sql' => 'b_sql', + 'Navi' => 'b_select', + 'Main' => 'b_props', + 'Import' => 'b_import', + 'Export' => 'b_export', + ]; + $script_name = basename($GLOBALS['PMA_PHP_SELF']); + $content = null; + foreach (UserFormList::getAll() as $formset) { + $formset_class = UserFormList::get($formset); + $tab = [ + 'link' => 'prefs_forms.php', + 'text' => $formset_class::getName(), + 'icon' => $tabs_icons[$formset], + 'active' => 'prefs_forms.php' === $script_name && $formset === $form_param, + ]; + $content .= Util::getHtmlTab($tab, ['form' => $formset]) . "\n"; + } + return $content; + } + + /** + * @return string|null + */ + protected static function displayConfigurationSavedMessage(): ?string + { + // show "configuration saved" message and reload navigation panel if needed + if (! empty($_GET['saved'])) { + return Message::rawSuccess(__('Configuration has been saved.')) + ->getDisplay(); + } + + return null; + } + + /** + * @param Relation $relation Relation instance + * + * @return string|null + */ + protected static function sessionStorageWarning(Relation $relation): ?string + { + // warn about using session storage for settings + $cfgRelation = $relation->getRelationsParam(); + if (! $cfgRelation['userconfigwork']) { + $msg = __( + 'Your preferences will be saved for current session only. Storing them ' + . 'permanently requires %sphpMyAdmin configuration storage%s.' + ); + $msg = Sanitize::sanitizeMessage( + sprintf($msg, '[doc@linked-tables]', '[/doc]') + ); + return Message::notice($msg) + ->getDisplay(); + } + + return null; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Util.php b/srcs/phpmyadmin/libraries/classes/Util.php new file mode 100644 index 0000000..ad7bcdf --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Util.php @@ -0,0 +1,4975 @@ +'; + if ($include_icon) { + $button .= self::getImage($icon, $alternate); + } + if ($include_icon && $include_text) { + $button .= ' '; + } + if ($include_text) { + $button .= $alternate; + } + $button .= $menu_icon ? '' : ''; + + return $button; + } + + /** + * Returns an HTML IMG tag for a particular image from a theme + * + * The image name should match CSS class defined in icons.css.php + * + * @param string $image The name of the file to get + * @param string $alternate Used to set 'alt' and 'title' attributes + * of the image + * @param array $attributes An associative array of other attributes + * + * @return string an html IMG tag + */ + public static function getImage($image, $alternate = '', array $attributes = []) + { + $alternate = htmlspecialchars($alternate); + + if (isset($attributes['class'])) { + $attributes['class'] = "icon ic_$image " . $attributes['class']; + } else { + $attributes['class'] = "icon ic_$image"; + } + + // set all other attributes + $attr_str = ''; + foreach ($attributes as $key => $value) { + if (! in_array($key, ['alt', 'title'])) { + $attr_str .= " $key=\"$value\""; + } + } + + // override the alt attribute + if (isset($attributes['alt'])) { + $alt = $attributes['alt']; + } else { + $alt = $alternate; + } + + // override the title attribute + if (isset($attributes['title'])) { + $title = $attributes['title']; + } else { + $title = $alternate; + } + + // generate the IMG tag + $template = '%s'; + return sprintf($template, $title, $alt, $attr_str); + } + + /** + * Returns the formatted maximum size for an upload + * + * @param integer $max_upload_size the size + * + * @return string the message + * + * @access public + */ + public static function getFormattedMaximumUploadSize($max_upload_size) + { + // I have to reduce the second parameter (sensitiveness) from 6 to 4 + // to avoid weird results like 512 kKib + list($max_size, $max_unit) = self::formatByteDown($max_upload_size, 4); + return '(' . sprintf(__('Max: %s%s'), $max_size, $max_unit) . ')'; + } + + /** + * Generates a hidden field which should indicate to the browser + * the maximum size for upload + * + * @param integer $max_size the size + * + * @return string the INPUT field + * + * @access public + */ + public static function generateHiddenMaxFileSize($max_size) + { + return ''; + } + + /** + * Add slashes before "_" and "%" characters for using them in MySQL + * database, table and field names. + * Note: This function does not escape backslashes! + * + * @param string $name the string to escape + * + * @return string the escaped string + * + * @access public + */ + public static function escapeMysqlWildcards($name) + { + return strtr($name, ['_' => '\\_', '%' => '\\%']); + } // end of the 'escapeMysqlWildcards()' function + + /** + * removes slashes before "_" and "%" characters + * Note: This function does not unescape backslashes! + * + * @param string $name the string to escape + * + * @return string the escaped string + * + * @access public + */ + public static function unescapeMysqlWildcards($name) + { + return strtr($name, ['\\_' => '_', '\\%' => '%']); + } // end of the 'unescapeMysqlWildcards()' function + + /** + * removes quotes (',",`) from a quoted string + * + * checks if the string is quoted and removes this quotes + * + * @param string $quoted_string string to remove quotes from + * @param string $quote type of quote to remove + * + * @return string unqoted string + */ + public static function unQuote($quoted_string, $quote = null) + { + $quotes = []; + + if ($quote === null) { + $quotes[] = '`'; + $quotes[] = '"'; + $quotes[] = "'"; + } else { + $quotes[] = $quote; + } + + foreach ($quotes as $quote) { + if (mb_substr($quoted_string, 0, 1) === $quote + && mb_substr($quoted_string, -1, 1) === $quote + ) { + $unquoted_string = mb_substr($quoted_string, 1, -1); + // replace escaped quotes + $unquoted_string = str_replace( + $quote . $quote, + $quote, + $unquoted_string + ); + return $unquoted_string; + } + } + + return $quoted_string; + } + + /** + * format sql strings + * + * @param string $sqlQuery raw SQL string + * @param boolean $truncate truncate the query if it is too long + * + * @return string the formatted sql + * + * @global array $cfg the configuration array + * + * @access public + * @todo move into PMA_Sql + */ + public static function formatSql($sqlQuery, $truncate = false) + { + global $cfg; + + if ($truncate + && mb_strlen($sqlQuery) > $cfg['MaxCharactersInDisplayedSQL'] + ) { + $sqlQuery = mb_substr( + $sqlQuery, + 0, + $cfg['MaxCharactersInDisplayedSQL'] + ) . '[...]'; + } + return '
    ' . "\n"
    +            . htmlspecialchars($sqlQuery) . "\n"
    +            . '
    '; + } // end of the "formatSql()" function + + /** + * Displays a button to copy content to clipboard + * + * @param string $text Text to copy to clipboard + * + * @return string the html link + * + * @access public + */ + public static function showCopyToClipboard($text) + { + $open_link = ' ' . __('Copy') . ''; + return $open_link; + } // end of the 'showCopyToClipboard()' function + + /** + * Displays a link to the documentation as an icon + * + * @param string $link documentation link + * @param string $target optional link target + * @param boolean $bbcode optional flag indicating whether to output bbcode + * + * @return string the html link + * + * @access public + */ + public static function showDocLink($link, $target = 'documentation', $bbcode = false) + { + if ($bbcode) { + return "[a@$link@$target][dochelpicon][/a]"; + } + + return '' + . self::getImage('b_help', __('Documentation')) + . ''; + } // end of the 'showDocLink()' function + + /** + * Get a URL link to the official MySQL documentation + * + * @param string $link contains name of page/anchor that is being linked + * @param string $anchor anchor to page part + * + * @return string the URL link + * + * @access public + */ + public static function getMySQLDocuURL($link, $anchor = '') + { + // Fixup for newly used names: + $link = str_replace('_', '-', mb_strtolower($link)); + + if (empty($link)) { + $link = 'index'; + } + $mysql = '5.5'; + $lang = 'en'; + if (isset($GLOBALS['dbi'])) { + $serverVersion = $GLOBALS['dbi']->getVersion(); + if ($serverVersion >= 50700) { + $mysql = '5.7'; + } elseif ($serverVersion >= 50600) { + $mysql = '5.6'; + } elseif ($serverVersion >= 50500) { + $mysql = '5.5'; + } + } + $url = 'https://dev.mysql.com/doc/refman/' + . $mysql . '/' . $lang . '/' . $link . '.html'; + if (! empty($anchor)) { + $url .= '#' . $anchor; + } + + return Core::linkURL($url); + } + + /** + * Get a link to variable documentation + * + * @param string $name The variable name + * @param boolean $useMariaDB Use only MariaDB documentation + * @param string $text (optional) The text for the link + * @return string link or empty string + */ + public static function linkToVarDocumentation( + string $name, + bool $useMariaDB = false, + string $text = null + ): string { + $html = ''; + try { + $type = KBSearch::MYSQL; + if ($useMariaDB) { + $type = KBSearch::MARIADB; + } + $docLink = KBSearch::getByName($name, $type); + $html = Util::showMySQLDocu( + $name, + false, + $docLink, + $text + ); + } catch (KBException $e) { + unset($e);// phpstan workaround + } + return $html; + } + + /** + * Displays a link to the official MySQL documentation + * + * @param string $link contains name of page/anchor that is being linked + * @param bool $bigIcon whether to use big icon (like in left frame) + * @param string|null $url href attribute + * @param string|null $text text of link + * @param string $anchor anchor to page part + * + * @return string the html link + * + * @access public + */ + public static function showMySQLDocu( + $link, + bool $bigIcon = false, + $url = null, + $text = null, + $anchor = '' + ): string { + if ($url === null) { + $url = self::getMySQLDocuURL($link, $anchor); + } + $openLink = ''; + $closeLink = ''; + $html = ''; + + if ($bigIcon) { + $html = $openLink . + self::getImage('b_sqlhelp', __('Documentation')) + . $closeLink; + } elseif ($text !== null) { + $html = $openLink . $text . $closeLink; + } else { + $html = self::showDocLink($url, 'mysql_doc'); + } + + return $html; + } // end of the 'showMySQLDocu()' function + + /** + * Returns link to documentation. + * + * @param string $page Page in documentation + * @param string $anchor Optional anchor in page + * + * @return string URL + */ + public static function getDocuLink($page, $anchor = '') + { + /* Construct base URL */ + $url = $page . '.html'; + if (! empty($anchor)) { + $url .= '#' . $anchor; + } + + /* Check if we have built local documentation, however + * provide consistent URL for testsuite + */ + if (! defined('TESTSUITE') && @file_exists(ROOT_PATH . 'doc/html/index.html')) { + return 'doc/html/' . $url; + } + + return Core::linkURL('https://docs.phpmyadmin.net/en/latest/' . $url); + } + + /** + * Displays a link to the phpMyAdmin documentation + * + * @param string $page Page in documentation + * @param string $anchor Optional anchor in page + * @param boolean $bbcode Optional flag indicating whether to output bbcode + * + * @return string the html link + * + * @access public + */ + public static function showDocu($page, $anchor = '', $bbcode = false) + { + return self::showDocLink(self::getDocuLink($page, $anchor), 'documentation', $bbcode); + } // end of the 'showDocu()' function + + /** + * Displays a link to the PHP documentation + * + * @param string $target anchor in documentation + * + * @return string the html link + * + * @access public + */ + public static function showPHPDocu($target) + { + $url = Core::getPHPDocLink($target); + + return self::showDocLink($url); + } // end of the 'showPHPDocu()' function + + /** + * Returns HTML code for a tooltip + * + * @param string $message the message for the tooltip + * + * @return string + * + * @access public + */ + public static function showHint($message) + { + if ($GLOBALS['cfg']['ShowHint']) { + $classClause = ' class="pma_hint"'; + } else { + $classClause = ''; + } + return '' + . self::getImage('b_help') + . '' . $message . '' + . ''; + } + + /** + * Displays a MySQL error message in the main panel when $exit is true. + * Returns the error message otherwise. + * + * @param string|bool $server_msg Server's error message. + * @param string $sql_query The SQL query that failed. + * @param bool $is_modify_link Whether to show a "modify" link or not. + * @param string $back_url URL for the "back" link (full path is + * not required). + * @param bool $exit Whether execution should be stopped or + * the error message should be returned. + * + * @return string + * + * @global string $table The current table. + * @global string $db The current database. + * + * @access public + */ + public static function mysqlDie( + $server_msg = '', + $sql_query = '', + $is_modify_link = true, + $back_url = '', + $exit = true + ) { + global $table, $db; + + /** + * Error message to be built. + * @var string $error_msg + */ + $error_msg = ''; + + // Checking for any server errors. + if (empty($server_msg)) { + $server_msg = $GLOBALS['dbi']->getError(); + } + + // Finding the query that failed, if not specified. + if (empty($sql_query) && ! empty($GLOBALS['sql_query'])) { + $sql_query = $GLOBALS['sql_query']; + } + $sql_query = trim($sql_query); + + /** + * The lexer used for analysis. + * @var Lexer $lexer + */ + $lexer = new Lexer($sql_query); + + /** + * The parser used for analysis. + * @var Parser $parser + */ + $parser = new Parser($lexer->list); + + /** + * The errors found by the lexer and the parser. + * @var array $errors + */ + $errors = ParserError::get([$lexer, $parser]); + + if (empty($sql_query)) { + $formatted_sql = ''; + } elseif (count($errors)) { + $formatted_sql = htmlspecialchars($sql_query); + } else { + $formatted_sql = self::formatSql($sql_query, true); + } + + $error_msg .= '

    ' . __('Error') . '

    '; + + // For security reasons, if the MySQL refuses the connection, the query + // is hidden so no details are revealed. + if (! empty($sql_query) && ! mb_strstr($sql_query, 'connect')) { + // Static analysis errors. + if (! empty($errors)) { + $error_msg .= '

    ' . __('Static analysis:') + . '

    '; + $error_msg .= '

    ' . sprintf( + __('%d errors were found during analysis.'), + count($errors) + ) . '

    '; + $error_msg .= '

      '; + $error_msg .= implode( + ParserError::format( + $errors, + '
    1. %2$s (near "%4$s" at position %5$d)
    2. ' + ) + ); + $error_msg .= '

    '; + } + + // Display the SQL query and link to MySQL documentation. + $error_msg .= '

    ' . __('SQL query:') . '' . self::showCopyToClipboard($sql_query) . "\n"; + $formattedSqlToLower = mb_strtolower($formatted_sql); + + // TODO: Show documentation for all statement types. + if (mb_strstr($formattedSqlToLower, 'select')) { + // please show me help to the error on select + $error_msg .= self::showMySQLDocu('SELECT'); + } + + if ($is_modify_link) { + $_url_params = [ + 'sql_query' => $sql_query, + 'show_query' => 1, + ]; + if (strlen($table) > 0) { + $_url_params['db'] = $db; + $_url_params['table'] = $table; + $doedit_goto = ''; + } elseif (strlen($db) > 0) { + $_url_params['db'] = $db; + $doedit_goto = ''; + } else { + $doedit_goto = ''; + } + + $error_msg .= $doedit_goto + . self::getIcon('b_edit', __('Edit')) + . ''; + } + + $error_msg .= '

    ' . "\n" + . '

    ' . "\n" + . $formatted_sql . "\n" + . '

    ' . "\n"; + } + + // Display server's error. + if (! empty($server_msg)) { + $server_msg = preg_replace( + "@((\015\012)|(\015)|(\012)){3,}@", + "\n\n", + $server_msg + ); + + // Adds a link to MySQL documentation. + $error_msg .= '

    ' . "\n" + . ' ' . __('MySQL said: ') . '' + . self::showMySQLDocu('Error-messages-server') + . "\n" + . '

    ' . "\n"; + + // The error message will be displayed within a CODE segment. + // To preserve original formatting, but allow word-wrapping, + // a couple of replacements are done. + // All non-single blanks and TAB-characters are replaced with their + // HTML-counterpart + $server_msg = str_replace( + [ + ' ', + "\t", + ], + [ + '  ', + '    ', + ], + $server_msg + ); + + // Replace line breaks + $server_msg = nl2br($server_msg); + + $error_msg .= '' . $server_msg . '
    '; + } + + $error_msg .= '
    '; + $_SESSION['Import_message']['message'] = $error_msg; + + if (! $exit) { + return $error_msg; + } + + /** + * If this is an AJAX request, there is no "Back" link and + * `Response()` is used to send the response. + */ + $response = Response::getInstance(); + if ($response->isAjax()) { + $response->setRequestStatus(false); + $response->addJSON('message', $error_msg); + exit; + } + + if (! empty($back_url)) { + if (mb_strstr($back_url, '?')) { + $back_url .= '&no_history=true'; + } else { + $back_url .= '?no_history=true'; + } + + $_SESSION['Import_message']['go_back_url'] = $back_url; + + $error_msg .= '
    ' + . '[ ' . __('Back') . ' ]' + . '
    ' . "\n\n"; + } + + exit($error_msg); + } + + /** + * Check the correct row count + * + * @param string $db the db name + * @param array $table the table infos + * + * @return int the possibly modified row count + * + */ + private static function _checkRowCount($db, array $table) + { + $rowCount = 0; + + if ($table['Rows'] === null) { + // Do not check exact row count here, + // if row count is invalid possibly the table is defect + // and this would break the navigation panel; + // but we can check row count if this is a view or the + // information_schema database + // since Table::countRecords() returns a limited row count + // in this case. + + // set this because Table::countRecords() can use it + $tbl_is_view = $table['TABLE_TYPE'] == 'VIEW'; + + if ($tbl_is_view || $GLOBALS['dbi']->isSystemSchema($db)) { + $rowCount = $GLOBALS['dbi'] + ->getTable($db, $table['Name']) + ->countRecords(); + } + } + return $rowCount; + } + + /** + * returns array with tables of given db with extended information and grouped + * + * @param string $db name of db + * @param string $tables name of tables + * @param integer $limit_offset list offset + * @param int|bool $limit_count max tables to return + * + * @return array (recursive) grouped table list + */ + public static function getTableList( + $db, + $tables = null, + $limit_offset = 0, + $limit_count = false + ) { + $sep = $GLOBALS['cfg']['NavigationTreeTableSeparator']; + + if ($tables === null) { + $tables = $GLOBALS['dbi']->getTablesFull( + $db, + '', + false, + $limit_offset, + $limit_count + ); + if ($GLOBALS['cfg']['NaturalOrder']) { + uksort($tables, 'strnatcasecmp'); + } + } + + if (count($tables) < 1) { + return $tables; + } + + $default = [ + 'Name' => '', + 'Rows' => 0, + 'Comment' => '', + 'disp_name' => '', + ]; + + $table_groups = []; + + foreach ($tables as $table_name => $table) { + $table['Rows'] = self::_checkRowCount($db, $table); + + // in $group we save the reference to the place in $table_groups + // where to store the table info + if ($GLOBALS['cfg']['NavigationTreeEnableGrouping'] + && $sep && mb_strstr($table_name, $sep) + ) { + $parts = explode($sep, $table_name); + + $group =& $table_groups; + $i = 0; + $group_name_full = ''; + $parts_cnt = count($parts) - 1; + + while (($i < $parts_cnt) + && ($i < $GLOBALS['cfg']['NavigationTreeTableLevel']) + ) { + $group_name = $parts[$i] . $sep; + $group_name_full .= $group_name; + + if (! isset($group[$group_name])) { + $group[$group_name] = []; + $group[$group_name]['is' . $sep . 'group'] = true; + $group[$group_name]['tab' . $sep . 'count'] = 1; + $group[$group_name]['tab' . $sep . 'group'] + = $group_name_full; + } elseif (! isset($group[$group_name]['is' . $sep . 'group'])) { + $table = $group[$group_name]; + $group[$group_name] = []; + $group[$group_name][$group_name] = $table; + $group[$group_name]['is' . $sep . 'group'] = true; + $group[$group_name]['tab' . $sep . 'count'] = 1; + $group[$group_name]['tab' . $sep . 'group'] + = $group_name_full; + } else { + $group[$group_name]['tab' . $sep . 'count']++; + } + + $group =& $group[$group_name]; + $i++; + } + } else { + if (! isset($table_groups[$table_name])) { + $table_groups[$table_name] = []; + } + $group =& $table_groups; + } + + $table['disp_name'] = $table['Name']; + $group[$table_name] = array_merge($default, $table); + } + + return $table_groups; + } + + /* ----------------------- Set of misc functions ----------------------- */ + + /** + * Adds backquotes on both sides of a database, table or field name. + * and escapes backquotes inside the name with another backquote + * + * example: + * + * echo backquote('owner`s db'); // `owner``s db` + * + * + * + * @param mixed $a_name the database, table or field name to "backquote" + * or array of it + * @param boolean $do_it a flag to bypass this function (used by dump + * functions) + * + * @return mixed the "backquoted" database, table or field name + * + * @access public + */ + public static function backquote($a_name, $do_it = true) + { + if (is_array($a_name)) { + foreach ($a_name as &$data) { + $data = self::backquote($data, $do_it); + } + return $a_name; + } + + if (! $do_it) { + if (! (Context::isKeyword($a_name) & Token::FLAG_KEYWORD_RESERVED) + ) { + return $a_name; + } + } + + // '0' is also empty for php :-( + if (strlen((string) $a_name) > 0 && $a_name !== '*') { + return '`' . str_replace('`', '``', (string) $a_name) . '`'; + } + + return $a_name; + } // end of the 'backquote()' function + + /** + * Adds backquotes on both sides of a database, table or field name. + * in compatibility mode + * + * example: + * + * echo backquoteCompat('owner`s db'); // `owner``s db` + * + * + * + * @param mixed $a_name the database, table or field name to + * "backquote" or array of it + * @param string $compatibility string compatibility mode (used by dump + * functions) + * @param boolean $do_it a flag to bypass this function (used by dump + * functions) + * + * @return mixed the "backquoted" database, table or field name + * + * @access public + */ + public static function backquoteCompat( + $a_name, + $compatibility = 'MSSQL', + $do_it = true + ) { + if (is_array($a_name)) { + foreach ($a_name as &$data) { + $data = self::backquoteCompat($data, $compatibility, $do_it); + } + return $a_name; + } + + if (! $do_it) { + if (! Context::isKeyword($a_name)) { + return $a_name; + } + } + + // @todo add more compatibility cases (ORACLE for example) + switch ($compatibility) { + case 'MSSQL': + $quote = '"'; + break; + default: + $quote = "`"; + break; + } + + // '0' is also empty for php :-( + if (strlen((string) $a_name) > 0 && $a_name !== '*') { + return $quote . $a_name . $quote; + } + + return $a_name; + } // end of the 'backquoteCompat()' function + + /** + * Prepare the message and the query + * usually the message is the result of the query executed + * + * @param Message|string $message the message to display + * @param string $sql_query the query to display + * @param string $type the type (level) of the message + * + * @return string + * + * @access public + */ + public static function getMessage( + $message, + $sql_query = null, + $type = 'notice' + ) { + global $cfg; + $template = new Template(); + $retval = ''; + + if (null === $sql_query) { + if (! empty($GLOBALS['display_query'])) { + $sql_query = $GLOBALS['display_query']; + } elseif (! empty($GLOBALS['unparsed_sql'])) { + $sql_query = $GLOBALS['unparsed_sql']; + } elseif (! empty($GLOBALS['sql_query'])) { + $sql_query = $GLOBALS['sql_query']; + } else { + $sql_query = ''; + } + } + + $render_sql = $cfg['ShowSQL'] == true && ! empty($sql_query) && $sql_query !== ';'; + + if (isset($GLOBALS['using_bookmark_message'])) { + $retval .= $GLOBALS['using_bookmark_message']->getDisplay(); + unset($GLOBALS['using_bookmark_message']); + } + + if ($render_sql) { + $retval .= '
    ' . "\n"; + } + + if ($message instanceof Message) { + if (isset($GLOBALS['special_message'])) { + $message->addText($GLOBALS['special_message']); + unset($GLOBALS['special_message']); + } + $retval .= $message->getDisplay(); + } else { + $retval .= '
    '; + $retval .= Sanitize::sanitizeMessage($message); + if (isset($GLOBALS['special_message'])) { + $retval .= Sanitize::sanitizeMessage($GLOBALS['special_message']); + unset($GLOBALS['special_message']); + } + $retval .= '
    '; + } + + if ($render_sql) { + $query_too_big = false; + + $queryLength = mb_strlen($sql_query); + if ($queryLength > $cfg['MaxCharactersInDisplayedSQL']) { + // when the query is large (for example an INSERT of binary + // data), the parser chokes; so avoid parsing the query + $query_too_big = true; + $query_base = mb_substr( + $sql_query, + 0, + $cfg['MaxCharactersInDisplayedSQL'] + ) . '[...]'; + } else { + $query_base = $sql_query; + } + + // Html format the query to be displayed + // If we want to show some sql code it is easiest to create it here + /* SQL-Parser-Analyzer */ + + if (! empty($GLOBALS['show_as_php'])) { + $new_line = '\\n"
    ' . "\n" . '    . "'; + $query_base = htmlspecialchars(addslashes($query_base)); + $query_base = preg_replace( + '/((\015\012)|(\015)|(\012))/', + $new_line, + $query_base + ); + $query_base = '
    ' . "\n"
    +                    . '$sql = "' . $query_base . '";' . "\n"
    +                    . '
    '; + } elseif ($query_too_big) { + $query_base = '
    ' . "\n" .
    +                    htmlspecialchars($query_base) .
    +                    '
    '; + } else { + $query_base = self::formatSql($query_base); + } + + // Prepares links that may be displayed to edit/explain the query + // (don't go to default pages, we must go to the page + // where the query box is available) + + // Basic url query part + $url_params = []; + if (! isset($GLOBALS['db'])) { + $GLOBALS['db'] = ''; + } + if (strlen($GLOBALS['db']) > 0) { + $url_params['db'] = $GLOBALS['db']; + if (strlen($GLOBALS['table']) > 0) { + $url_params['table'] = $GLOBALS['table']; + $edit_link = 'tbl_sql.php'; + } else { + $edit_link = 'db_sql.php'; + } + } else { + $edit_link = 'server_sql.php'; + } + + // Want to have the query explained + // but only explain a SELECT (that has not been explained) + /* SQL-Parser-Analyzer */ + $explain_link = ''; + $is_select = preg_match('@^SELECT[[:space:]]+@i', $sql_query); + if (! empty($cfg['SQLQuery']['Explain']) && ! $query_too_big) { + $explain_params = $url_params; + if ($is_select) { + $explain_params['sql_query'] = 'EXPLAIN ' . $sql_query; + $explain_link = ' [ ' + . self::linkOrButton( + 'import.php' . Url::getCommon($explain_params), + __('Explain SQL') + ) . ' ]'; + } elseif (preg_match( + '@^EXPLAIN[[:space:]]+SELECT[[:space:]]+@i', + $sql_query + )) { + $explain_params['sql_query'] + = mb_substr($sql_query, 8); + $explain_link = ' [ ' + . self::linkOrButton( + 'import.php' . Url::getCommon($explain_params), + __('Skip Explain SQL') + ) . ']'; + $url = 'https://mariadb.org/explain_analyzer/analyze/' + . '?client=phpMyAdmin&raw_explain=' + . urlencode(self::_generateRowQueryOutput($sql_query)); + $explain_link .= ' [' + . self::linkOrButton( + htmlspecialchars('url.php?url=' . urlencode($url)), + sprintf(__('Analyze Explain at %s'), 'mariadb.org'), + [], + '_blank' + ) . ' ]'; + } + } //show explain + + $url_params['sql_query'] = $sql_query; + $url_params['show_query'] = 1; + + // even if the query is big and was truncated, offer the chance + // to edit it (unless it's enormous, see linkOrButton() ) + if (! empty($cfg['SQLQuery']['Edit']) + && empty($GLOBALS['show_as_php']) + ) { + $edit_link .= Url::getCommon($url_params); + $edit_link = ' [ ' + . self::linkOrButton($edit_link, __('Edit')) + . ' ]'; + } else { + $edit_link = ''; + } + + // Also we would like to get the SQL formed in some nice + // php-code + if (! empty($cfg['SQLQuery']['ShowAsPHP']) && ! $query_too_big) { + if (! empty($GLOBALS['show_as_php'])) { + $php_link = ' [ ' + . self::linkOrButton( + 'import.php' . Url::getCommon($url_params), + __('Without PHP code') + ) + . ' ]'; + + $php_link .= ' [ ' + . self::linkOrButton( + 'import.php' . Url::getCommon($url_params), + __('Submit query') + ) + . ' ]'; + } else { + $php_params = $url_params; + $php_params['show_as_php'] = 1; + $php_link = ' [ ' + . self::linkOrButton( + 'import.php' . Url::getCommon($php_params), + __('Create PHP code') + ) + . ' ]'; + } + } else { + $php_link = ''; + } //show as php + + // Refresh query + if (! empty($cfg['SQLQuery']['Refresh']) + && ! isset($GLOBALS['show_as_php']) // 'Submit query' does the same + && preg_match('@^(SELECT|SHOW)[[:space:]]+@i', $sql_query) + ) { + $refresh_link = 'sql.php' . Url::getCommon($url_params); + $refresh_link = ' [ ' + . self::linkOrButton($refresh_link, __('Refresh')) . ']'; + } else { + $refresh_link = ''; + } //refresh + + $retval .= '
    '; + $retval .= $query_base; + $retval .= '
    '; + + $retval .= ''; + + $retval .= '
    '; + } + + return $retval; + } // end of the 'getMessage()' function + + /** + * Execute an EXPLAIN query and formats results similar to MySQL command line + * utility. + * + * @param string $sqlQuery EXPLAIN query + * + * @return string query resuls + */ + private static function _generateRowQueryOutput($sqlQuery) + { + $ret = ''; + $result = $GLOBALS['dbi']->query($sqlQuery); + if ($result) { + $devider = '+'; + $columnNames = '|'; + $fieldsMeta = $GLOBALS['dbi']->getFieldsMeta($result); + foreach ($fieldsMeta as $meta) { + $devider .= '---+'; + $columnNames .= ' ' . $meta->name . ' |'; + } + $devider .= "\n"; + + $ret .= $devider . $columnNames . "\n" . $devider; + while ($row = $GLOBALS['dbi']->fetchRow($result)) { + $values = '|'; + foreach ($row as $value) { + if ($value === null) { + $value = 'NULL'; + } + $values .= ' ' . $value . ' |'; + } + $ret .= $values . "\n"; + } + $ret .= $devider; + } + return $ret; + } + + /** + * Verifies if current MySQL server supports profiling + * + * @access public + * + * @return boolean whether profiling is supported + */ + public static function profilingSupported() + { + if (! self::cacheExists('profiling_supported')) { + // 5.0.37 has profiling but for example, 5.1.20 does not + // (avoid a trip to the server for MySQL before 5.0.37) + // and do not set a constant as we might be switching servers + if ($GLOBALS['dbi']->fetchValue("SELECT @@have_profiling") + ) { + self::cacheSet('profiling_supported', true); + } else { + self::cacheSet('profiling_supported', false); + } + } + + return self::cacheGet('profiling_supported'); + } + + /** + * Formats $value to byte view + * + * @param double|int $value the value to format + * @param int $limes the sensitiveness + * @param int $comma the number of decimals to retain + * + * @return array|null the formatted value and its unit + * + * @access public + */ + public static function formatByteDown($value, $limes = 6, $comma = 0) + { + if ($value === null) { + return null; + } + + $byteUnits = [ + /* l10n: shortcuts for Byte */ + __('B'), + /* l10n: shortcuts for Kilobyte */ + __('KiB'), + /* l10n: shortcuts for Megabyte */ + __('MiB'), + /* l10n: shortcuts for Gigabyte */ + __('GiB'), + /* l10n: shortcuts for Terabyte */ + __('TiB'), + /* l10n: shortcuts for Petabyte */ + __('PiB'), + /* l10n: shortcuts for Exabyte */ + __('EiB'), + ]; + + $dh = pow(10, $comma); + $li = pow(10, $limes); + $unit = $byteUnits[0]; + + for ($d = 6, $ex = 15; $d >= 1; $d--, $ex -= 3) { + $unitSize = $li * pow(10, $ex); + if (isset($byteUnits[$d]) && $value >= $unitSize) { + // use 1024.0 to avoid integer overflow on 64-bit machines + $value = round($value / (pow(1024, $d) / $dh)) / $dh; + $unit = $byteUnits[$d]; + break 1; + } // end if + } // end for + + if ($unit != $byteUnits[0]) { + // if the unit is not bytes (as represented in current language) + // reformat with max length of 5 + // 4th parameter=true means do not reformat if value < 1 + $return_value = self::formatNumber($value, 5, $comma, true, false); + } else { + // do not reformat, just handle the locale + $return_value = self::formatNumber($value, 0); + } + + return [ + trim($return_value), + $unit, + ]; + } // end of the 'formatByteDown' function + + + /** + * Formats $value to the given length and appends SI prefixes + * with a $length of 0 no truncation occurs, number is only formatted + * to the current locale + * + * examples: + * + * echo formatNumber(123456789, 6); // 123,457 k + * echo formatNumber(-123456789, 4, 2); // -123.46 M + * echo formatNumber(-0.003, 6); // -3 m + * echo formatNumber(0.003, 3, 3); // 0.003 + * echo formatNumber(0.00003, 3, 2); // 0.03 m + * echo formatNumber(0, 6); // 0 + * + * + * @param double $value the value to format + * @param integer $digits_left number of digits left of the comma + * @param integer $digits_right number of digits right of the comma + * @param boolean $only_down do not reformat numbers below 1 + * @param boolean $noTrailingZero removes trailing zeros right of the comma + * (default: true) + * + * @return string the formatted value and its unit + * + * @access public + */ + public static function formatNumber( + $value, + $digits_left = 3, + $digits_right = 0, + $only_down = false, + $noTrailingZero = true + ) { + if ($value == 0) { + return '0'; + } + + $originalValue = $value; + //number_format is not multibyte safe, str_replace is safe + if ($digits_left === 0) { + $value = number_format( + (float) $value, + $digits_right, + /* l10n: Decimal separator */ + __('.'), + /* l10n: Thousands separator */ + __(',') + ); + if (($originalValue != 0) && (floatval($value) == 0)) { + $value = ' <' . (1 / pow(10, $digits_right)); + } + return $value; + } + + // this units needs no translation, ISO + $units = [ + -8 => 'y', + -7 => 'z', + -6 => 'a', + -5 => 'f', + -4 => 'p', + -3 => 'n', + -2 => 'µ', + -1 => 'm', + 0 => ' ', + 1 => 'k', + 2 => 'M', + 3 => 'G', + 4 => 'T', + 5 => 'P', + 6 => 'E', + 7 => 'Z', + 8 => 'Y', + ]; + /* l10n: Decimal separator */ + $decimal_sep = __('.'); + /* l10n: Thousands separator */ + $thousands_sep = __(','); + + // check for negative value to retain sign + if ($value < 0) { + $sign = '-'; + $value = abs($value); + } else { + $sign = ''; + } + + $dh = pow(10, $digits_right); + + /* + * This gives us the right SI prefix already, + * but $digits_left parameter not incorporated + */ + $d = floor(log10((float) $value) / 3); + /* + * Lowering the SI prefix by 1 gives us an additional 3 zeros + * So if we have 3,6,9,12.. free digits ($digits_left - $cur_digits) + * to use, then lower the SI prefix + */ + $cur_digits = floor(log10($value / pow(1000, $d)) + 1); + if ($digits_left > $cur_digits) { + $d -= floor(($digits_left - $cur_digits) / 3); + } + + if ($d < 0 && $only_down) { + $d = 0; + } + + $value = round($value / (pow(1000, $d) / $dh)) / $dh; + $unit = $units[$d]; + + // number_format is not multibyte safe, str_replace is safe + $formattedValue = number_format( + $value, + $digits_right, + $decimal_sep, + $thousands_sep + ); + // If we don't want any zeros, remove them now + if ($noTrailingZero && strpos($formattedValue, $decimal_sep) !== false) { + $formattedValue = preg_replace('/' . preg_quote($decimal_sep, '/') . '?0+$/', '', $formattedValue); + } + + if ($originalValue != 0 && floatval($value) == 0) { + return ' <' . number_format( + 1 / pow(10, $digits_right), + $digits_right, + $decimal_sep, + $thousands_sep + ) + . ' ' . $unit; + } + + return $sign . $formattedValue . ' ' . $unit; + } // end of the 'formatNumber' function + + /** + * Returns the number of bytes when a formatted size is given + * + * @param string $formatted_size the size expression (for example 8MB) + * + * @return integer The numerical part of the expression (for example 8) + */ + public static function extractValueFromFormattedSize($formatted_size) + { + $return_value = -1; + + $formatted_size = (string) $formatted_size; + + if (preg_match('/^[0-9]+GB$/', $formatted_size)) { + $return_value = (int) mb_substr( + $formatted_size, + 0, + -2 + ) * pow(1024, 3); + } elseif (preg_match('/^[0-9]+MB$/', $formatted_size)) { + $return_value = (int) mb_substr( + $formatted_size, + 0, + -2 + ) * pow(1024, 2); + } elseif (preg_match('/^[0-9]+K$/', $formatted_size)) { + $return_value = (int) mb_substr( + $formatted_size, + 0, + -1 + ) * pow(1024, 1); + } + return $return_value; + } + + /** + * Writes localised date + * + * @param integer $timestamp the current timestamp + * @param string $format format + * + * @return string the formatted date + * + * @access public + */ + public static function localisedDate($timestamp = -1, $format = '') + { + $month = [ + /* l10n: Short month name */ + __('Jan'), + /* l10n: Short month name */ + __('Feb'), + /* l10n: Short month name */ + __('Mar'), + /* l10n: Short month name */ + __('Apr'), + /* l10n: Short month name */ + _pgettext('Short month name', 'May'), + /* l10n: Short month name */ + __('Jun'), + /* l10n: Short month name */ + __('Jul'), + /* l10n: Short month name */ + __('Aug'), + /* l10n: Short month name */ + __('Sep'), + /* l10n: Short month name */ + __('Oct'), + /* l10n: Short month name */ + __('Nov'), + /* l10n: Short month name */ + __('Dec'), + ]; + $day_of_week = [ + /* l10n: Short week day name for Sunday */ + _pgettext('Short week day name', 'Sun'), + /* l10n: Short week day name for Monday */ + __('Mon'), + /* l10n: Short week day name for Tuesday */ + __('Tue'), + /* l10n: Short week day name for Wednesday */ + __('Wed'), + /* l10n: Short week day name for Thursday */ + __('Thu'), + /* l10n: Short week day name for Friday */ + __('Fri'), + /* l10n: Short week day name for Saturday */ + __('Sat'), + ]; + + if ($format == '') { + /* l10n: See https://www.php.net/manual/en/function.strftime.php */ + $format = __('%B %d, %Y at %I:%M %p'); + } + + if ($timestamp == -1) { + $timestamp = time(); + } + + $date = preg_replace( + '@%[aA]@', + $day_of_week[(int) strftime('%w', (int) $timestamp)], + $format + ); + $date = preg_replace( + '@%[bB]@', + $month[(int) strftime('%m', (int) $timestamp) - 1], + $date + ); + + /* Fill in AM/PM */ + $hours = (int) date('H', (int) $timestamp); + if ($hours >= 12) { + $am_pm = _pgettext('AM/PM indication in time', 'PM'); + } else { + $am_pm = _pgettext('AM/PM indication in time', 'AM'); + } + $date = preg_replace('@%[pP]@', $am_pm, $date); + + $ret = strftime($date, (int) $timestamp); + // Some OSes such as Win8.1 Traditional Chinese version did not produce UTF-8 + // output here. See https://github.com/phpmyadmin/phpmyadmin/issues/10598 + if (mb_detect_encoding($ret, 'UTF-8', true) != 'UTF-8') { + $ret = date('Y-m-d H:i:s', (int) $timestamp); + } + + return $ret; + } // end of the 'localisedDate()' function + + /** + * returns a tab for tabbed navigation. + * If the variables $link and $args ar left empty, an inactive tab is created + * + * @param array $tab array with all options + * @param array $url_params tab specific URL parameters + * + * @return string html code for one tab, a link if valid otherwise a span + * + * @access public + */ + public static function getHtmlTab(array $tab, array $url_params = []) + { + $template = new Template(); + // default values + $defaults = [ + 'text' => '', + 'class' => '', + 'active' => null, + 'link' => '', + 'sep' => '?', + 'attr' => '', + 'args' => '', + 'warning' => '', + 'fragment' => '', + 'id' => '', + ]; + + $tab = array_merge($defaults, $tab); + + // determine additional style-class + if (empty($tab['class'])) { + if (! empty($tab['active']) + || Core::isValid($GLOBALS['active_page'], 'identical', $tab['link']) + ) { + $tab['class'] = 'active'; + } elseif ($tab['active'] === null && empty($GLOBALS['active_page']) + && (basename($GLOBALS['PMA_PHP_SELF']) == $tab['link']) + ) { + $tab['class'] = 'active'; + } + } + + // build the link + if (! empty($tab['link'])) { + // If there are any tab specific URL parameters, merge those with + // the general URL parameters + if (! empty($tab['args']) && is_array($tab['args'])) { + $url_params = array_merge($url_params, $tab['args']); + } + $tab['link'] = htmlentities($tab['link']) . Url::getCommon($url_params); + } + + if (! empty($tab['fragment'])) { + $tab['link'] .= $tab['fragment']; + } + + // display icon + if (isset($tab['icon'])) { + // avoid generating an alt tag, because it only illustrates + // the text that follows and if browser does not display + // images, the text is duplicated + $tab['text'] = self::getIcon( + $tab['icon'], + $tab['text'], + false, + true, + 'TabsMode' + ); + } elseif (empty($tab['text'])) { + // check to not display an empty link-text + $tab['text'] = '?'; + trigger_error( + 'empty linktext in function ' . __FUNCTION__ . '()', + E_USER_NOTICE + ); + } + + //Set the id for the tab, if set in the params + $tabId = (empty($tab['id']) ? null : $tab['id']); + + $item = []; + if (! empty($tab['link'])) { + $item = [ + 'content' => $tab['text'], + 'url' => [ + 'href' => empty($tab['link']) ? null : $tab['link'], + 'id' => $tabId, + 'class' => 'tab' . htmlentities($tab['class']), + ], + ]; + } else { + $item['content'] = '' . $tab['text'] . ''; + } + + $item['class'] = $tab['class'] == 'active' ? 'active' : ''; + + return $template->render('list/item', $item); + } + + /** + * returns html-code for a tab navigation + * + * @param array $tabs one element per tab + * @param array $url_params additional URL parameters + * @param string $menu_id HTML id attribute for the menu container + * @param bool $resizable whether to add a "resizable" class + * + * @return string html-code for tab-navigation + */ + public static function getHtmlTabs( + array $tabs, + array $url_params, + $menu_id, + $resizable = false + ) { + $class = ''; + if ($resizable) { + $class = ' class="resizable-menu"'; + } + + $tab_navigation = '' . "\n"; + + return $tab_navigation; + } + + /** + * Displays a link, or a link with code to trigger POST request. + * + * POST is used in following cases: + * + * - URL is too long + * - URL components are over Suhosin limits + * - There is SQL query in the parameters + * + * @param string $url the URL + * @param string $message the link message + * @param mixed $tag_params string: js confirmation; array: additional tag + * params (f.e. style="") + * @param string $target target + * + * @return string the results to be echoed or saved in an array + */ + public static function linkOrButton( + $url, + $message, + $tag_params = [], + $target = '' + ) { + $url_length = strlen($url); + + if (! is_array($tag_params)) { + $tmp = $tag_params; + $tag_params = []; + if (! empty($tmp)) { + $tag_params['onclick'] = 'return Functions.confirmLink(this, \'' + . Sanitize::escapeJsString($tmp) . '\')'; + } + unset($tmp); + } + if (! empty($target)) { + $tag_params['target'] = $target; + if ($target === '_blank' && strncmp($url, 'url.php?', 8) == 0) { + $tag_params['rel'] = 'noopener noreferrer'; + } + } + + // Suhosin: Check that each query parameter is not above maximum + $in_suhosin_limits = true; + if ($url_length <= $GLOBALS['cfg']['LinkLengthLimit']) { + $suhosin_get_MaxValueLength = ini_get('suhosin.get.max_value_length'); + if ($suhosin_get_MaxValueLength) { + $query_parts = self::splitURLQuery($url); + foreach ($query_parts as $query_pair) { + if (strpos($query_pair, '=') === false) { + continue; + } + + list(, $eachval) = explode('=', $query_pair); + if (strlen($eachval) > $suhosin_get_MaxValueLength + ) { + $in_suhosin_limits = false; + break; + } + } + } + } + + $tag_params_strings = []; + if (($url_length > $GLOBALS['cfg']['LinkLengthLimit']) + || ! $in_suhosin_limits + // Has as sql_query without a signature + || ( strpos($url, 'sql_query=') !== false && strpos($url, 'sql_signature=') === false) + || strpos($url, 'view[as]=') !== false + ) { + $parts = explode('?', $url, 2); + /* + * The data-post indicates that client should do POST + * this is handled in js/ajax.js + */ + $tag_params_strings[] = 'data-post="' . (isset($parts[1]) ? $parts[1] : '') . '"'; + $url = $parts[0]; + if (array_key_exists('class', $tag_params) + && strpos($tag_params['class'], 'create_view') !== false + ) { + $url .= '?' . explode('&', $parts[1], 2)[0]; + } + } + + foreach ($tag_params as $par_name => $par_value) { + $tag_params_strings[] = $par_name . '="' . htmlspecialchars($par_value) . '"'; + } + + // no whitespace within an else Safari will make it part of the link + return '' + . $message . ''; + } // end of the 'linkOrButton()' function + + /** + * Splits a URL string by parameter + * + * @param string $url the URL + * + * @return array the parameter/value pairs, for example [0] db=sakila + */ + public static function splitURLQuery($url) + { + // decode encoded url separators + $separator = Url::getArgSeparator(); + // on most places separator is still hard coded ... + if ($separator !== '&') { + // ... so always replace & with $separator + $url = str_replace([htmlentities('&'), '&'], [$separator, $separator], $url); + } + + $url = str_replace(htmlentities($separator), $separator, $url); + // end decode + + $url_parts = parse_url($url); + + if (! empty($url_parts['query'])) { + return explode($separator, $url_parts['query']); + } + + return []; + } + + /** + * Returns a given timespan value in a readable format. + * + * @param int $seconds the timespan + * + * @return string the formatted value + */ + public static function timespanFormat($seconds) + { + $days = floor($seconds / 86400); + if ($days > 0) { + $seconds -= $days * 86400; + } + + $hours = floor($seconds / 3600); + if ($days > 0 || $hours > 0) { + $seconds -= $hours * 3600; + } + + $minutes = floor($seconds / 60); + if ($days > 0 || $hours > 0 || $minutes > 0) { + $seconds -= $minutes * 60; + } + + return sprintf( + __('%s days, %s hours, %s minutes and %s seconds'), + (string) $days, + (string) $hours, + (string) $minutes, + (string) $seconds + ); + } + + /** + * Function added to avoid path disclosures. + * Called by each script that needs parameters, it displays + * an error message and, by default, stops the execution. + * + * @param string[] $params The names of the parameters needed by the calling + * script + * @param boolean $request Check parameters in request + * + * @return void + * + * @access public + */ + public static function checkParameters($params, $request = false) + { + $reported_script_name = basename($GLOBALS['PMA_PHP_SELF']); + $found_error = false; + $error_message = ''; + if ($request) { + $array = $_REQUEST; + } else { + $array = $GLOBALS; + } + + foreach ($params as $param) { + if (! isset($array[$param])) { + $error_message .= $reported_script_name + . ': ' . __('Missing parameter:') . ' ' + . $param + . self::showDocu('faq', 'faqmissingparameters', true) + . '[br]'; + $found_error = true; + } + } + if ($found_error) { + Core::fatalError($error_message); + } + } // end function + + /** + * Function to generate unique condition for specified row. + * + * @param resource $handle current query result + * @param integer $fields_cnt number of fields + * @param stdClass[] $fields_meta meta information about fields + * @param array $row current row + * @param boolean $force_unique generate condition only on pk + * or unique + * @param string|boolean $restrict_to_table restrict the unique condition + * to this table or false if + * none + * @param array|null $analyzed_sql_results the analyzed query + * + * @access public + * + * @return array the calculated condition and whether condition is unique + */ + public static function getUniqueCondition( + $handle, + $fields_cnt, + array $fields_meta, + array $row, + $force_unique = false, + $restrict_to_table = false, + $analyzed_sql_results = null + ) { + $primary_key = ''; + $unique_key = ''; + $nonprimary_condition = ''; + $preferred_condition = ''; + $primary_key_array = []; + $unique_key_array = []; + $nonprimary_condition_array = []; + $condition_array = []; + + for ($i = 0; $i < $fields_cnt; ++$i) { + $con_val = ''; + $field_flags = $GLOBALS['dbi']->fieldFlags($handle, $i); + $meta = $fields_meta[$i]; + + // do not use a column alias in a condition + if (! isset($meta->orgname) || strlen($meta->orgname) === 0) { + $meta->orgname = $meta->name; + + if (! empty($analyzed_sql_results['statement']->expr)) { + foreach ($analyzed_sql_results['statement']->expr as $expr) { + if (empty($expr->alias) || empty($expr->column)) { + continue; + } + if (strcasecmp($meta->name, $expr->alias) == 0) { + $meta->orgname = $expr->column; + break; + } + } + } + } + + // Do not use a table alias in a condition. + // Test case is: + // select * from galerie x WHERE + //(select count(*) from galerie y where y.datum=x.datum)>1 + // + // But orgtable is present only with mysqli extension so the + // fix is only for mysqli. + // Also, do not use the original table name if we are dealing with + // a view because this view might be updatable. + // (The isView() verification should not be costly in most cases + // because there is some caching in the function). + if (isset($meta->orgtable) + && ($meta->table != $meta->orgtable) + && ! $GLOBALS['dbi']->getTable($GLOBALS['db'], $meta->table)->isView() + ) { + $meta->table = $meta->orgtable; + } + + // If this field is not from the table which the unique clause needs + // to be restricted to. + if ($restrict_to_table && $restrict_to_table != $meta->table) { + continue; + } + + // to fix the bug where float fields (primary or not) + // can't be matched because of the imprecision of + // floating comparison, use CONCAT + // (also, the syntax "CONCAT(field) IS NULL" + // that we need on the next "if" will work) + if ($meta->type == 'real') { + $con_key = 'CONCAT(' . self::backquote($meta->table) . '.' + . self::backquote($meta->orgname) . ')'; + } else { + $con_key = self::backquote($meta->table) . '.' + . self::backquote($meta->orgname); + } // end if... else... + $condition = ' ' . $con_key . ' '; + + if (! isset($row[$i]) || $row[$i] === null) { + $con_val = 'IS NULL'; + } else { + // timestamp is numeric on some MySQL 4.1 + // for real we use CONCAT above and it should compare to string + if ($meta->numeric + && ($meta->type != 'timestamp') + && ($meta->type != 'real') + ) { + $con_val = '= ' . $row[$i]; + } elseif ((($meta->type == 'blob') || ($meta->type == 'string')) + && false !== stripos($field_flags, 'BINARY') + && ! empty($row[$i]) + ) { + // hexify only if this is a true not empty BLOB or a BINARY + + // do not waste memory building a too big condition + if (mb_strlen($row[$i]) < 1000) { + // use a CAST if possible, to avoid problems + // if the field contains wildcard characters % or _ + $con_val = '= CAST(0x' . bin2hex($row[$i]) . ' AS BINARY)'; + } elseif ($fields_cnt == 1) { + // when this blob is the only field present + // try settling with length comparison + $condition = ' CHAR_LENGTH(' . $con_key . ') '; + $con_val = ' = ' . mb_strlen($row[$i]); + } else { + // this blob won't be part of the final condition + $con_val = null; + } + } elseif (in_array($meta->type, self::getGISDatatypes()) + && ! empty($row[$i]) + ) { + // do not build a too big condition + if (mb_strlen($row[$i]) < 5000) { + $condition .= '=0x' . bin2hex($row[$i]) . ' AND'; + } else { + $condition = ''; + } + } elseif ($meta->type == 'bit') { + $con_val = "= b'" + . self::printableBitValue((int) $row[$i], (int) $meta->length) . "'"; + } else { + $con_val = '= \'' + . $GLOBALS['dbi']->escapeString($row[$i]) . '\''; + } + } + + if ($con_val != null) { + $condition .= $con_val . ' AND'; + + if ($meta->primary_key > 0) { + $primary_key .= $condition; + $primary_key_array[$con_key] = $con_val; + } elseif ($meta->unique_key > 0) { + $unique_key .= $condition; + $unique_key_array[$con_key] = $con_val; + } + + $nonprimary_condition .= $condition; + $nonprimary_condition_array[$con_key] = $con_val; + } + } // end for + + // Correction University of Virginia 19991216: + // prefer primary or unique keys for condition, + // but use conjunction of all values if no primary key + $clause_is_unique = true; + + if ($primary_key) { + $preferred_condition = $primary_key; + $condition_array = $primary_key_array; + } elseif ($unique_key) { + $preferred_condition = $unique_key; + $condition_array = $unique_key_array; + } elseif (! $force_unique) { + $preferred_condition = $nonprimary_condition; + $condition_array = $nonprimary_condition_array; + $clause_is_unique = false; + } + + $where_clause = trim(preg_replace('|\s?AND$|', '', $preferred_condition)); + return [ + $where_clause, + $clause_is_unique, + $condition_array, + ]; + } // end function + + /** + * Generate the charset query part + * + * @param string $collation Collation + * @param boolean $override (optional) force 'CHARACTER SET' keyword + * + * @return string + */ + public static function getCharsetQueryPart($collation, $override = false) + { + list($charset) = explode('_', $collation); + $keyword = ' CHARSET='; + + if ($override) { + $keyword = ' CHARACTER SET '; + } + return $keyword . $charset + . ($charset == $collation ? '' : ' COLLATE ' . $collation); + } + + /** + * Generate a button or image tag + * + * @param string $button_name name of button element + * @param string $button_class class of button or image element + * @param string $text text to display + * @param string $image image to display + * @param string $value value + * + * @return string html content + * + * @access public + */ + public static function getButtonOrImage( + $button_name, + $button_class, + $text, + $image, + $value = '' + ) { + if ($value == '') { + $value = $text; + } + if ($GLOBALS['cfg']['ActionLinksMode'] == 'text') { + return ' ' . "\n"; + } + return '' . "\n"; + } // end function + + /** + * Generate a pagination selector for browsing resultsets + * + * @param string $name The name for the request parameter + * @param int $rows Number of rows in the pagination set + * @param int $pageNow current page number + * @param int $nbTotalPage number of total pages + * @param int $showAll If the number of pages is lower than this + * variable, no pages will be omitted in pagination + * @param int $sliceStart How many rows at the beginning should always + * be shown? + * @param int $sliceEnd How many rows at the end should always be shown? + * @param int $percent Percentage of calculation page offsets to hop to a + * next page + * @param int $range Near the current page, how many pages should + * be considered "nearby" and displayed as well? + * @param string $prompt The prompt to display (sometimes empty) + * + * @return string + * + * @access public + */ + public static function pageselector( + $name, + $rows, + $pageNow = 1, + $nbTotalPage = 1, + $showAll = 200, + $sliceStart = 5, + $sliceEnd = 5, + $percent = 20, + $range = 10, + $prompt = '' + ) { + $increment = floor($nbTotalPage / $percent); + $pageNowMinusRange = ($pageNow - $range); + $pageNowPlusRange = ($pageNow + $range); + + $gotopage = $prompt . ' '; + + return $gotopage; + } // end function + + + /** + * Calculate page number through position + * @param int $pos position of first item + * @param int $max_count number of items per page + * @return int $page_num + * @access public + */ + public static function getPageFromPosition($pos, $max_count) + { + return (int) floor($pos / $max_count) + 1; + } + + /** + * Prepare navigation for a list + * + * @param int $count number of elements in the list + * @param int $pos current position in the list + * @param array $_url_params url parameters + * @param string $script script name for form target + * @param string $frame target frame + * @param int $max_count maximum number of elements to display from + * the list + * @param string $name the name for the request parameter + * @param string[] $classes additional classes for the container + * + * @return string the html content + * + * @access public + * + * @todo use $pos from $_url_params + */ + public static function getListNavigator( + $count, + $pos, + array $_url_params, + $script, + $frame, + $max_count, + $name = 'pos', + $classes = [] + ) { + + // This is often coming from $cfg['MaxTableList'] and + // people sometimes set it to empty string + $max_count = intval($max_count); + if ($max_count <= 0) { + $max_count = 250; + } + + $class = $frame == 'frame_navigation' ? ' class="ajax"' : ''; + + $list_navigator_html = ''; + + if ($max_count < $count) { + $classes[] = 'pageselector'; + $list_navigator_html .= '
    '; + + if ($frame != 'frame_navigation') { + $list_navigator_html .= __('Page number:'); + } + + // Move to the beginning or to the previous page + if ($pos > 0) { + $caption1 = ''; + $caption2 = ''; + if (self::showIcons('TableNavigationLinksMode')) { + $caption1 .= '<< '; + $caption2 .= '< '; + } + if (self::showText('TableNavigationLinksMode')) { + $caption1 .= _pgettext('First page', 'Begin'); + $caption2 .= _pgettext('Previous page', 'Previous'); + } + $title1 = ' title="' . _pgettext('First page', 'Begin') . '"'; + $title2 = ' title="' . _pgettext('Previous page', 'Previous') . '"'; + + $_url_params[$name] = 0; + $list_navigator_html .= '' . $caption1 + . ''; + + $_url_params[$name] = $pos - $max_count; + $list_navigator_html .= ' ' + . $caption2 . ''; + } + + $list_navigator_html .= '
    '; + + $list_navigator_html .= Url::getHiddenInputs($_url_params); + $list_navigator_html .= self::pageselector( + $name, + $max_count, + self::getPageFromPosition($pos, $max_count), + ceil($count / $max_count) + ); + $list_navigator_html .= '
    '; + + if ($pos + $max_count < $count) { + $caption3 = ''; + $caption4 = ''; + if (self::showText('TableNavigationLinksMode')) { + $caption3 .= _pgettext('Next page', 'Next'); + $caption4 .= _pgettext('Last page', 'End'); + } + if (self::showIcons('TableNavigationLinksMode')) { + $caption3 .= ' >'; + $caption4 .= ' >>'; + } + $title3 = ' title="' . _pgettext('Next page', 'Next') . '"'; + $title4 = ' title="' . _pgettext('Last page', 'End') . '"'; + + $_url_params[$name] = $pos + $max_count; + $list_navigator_html .= '' . $caption3 + . ''; + + $_url_params[$name] = floor($count / $max_count) * $max_count; + if ($_url_params[$name] == $count) { + $_url_params[$name] = $count - $max_count; + } + + $list_navigator_html .= ' ' + . $caption4 . ''; + } + $list_navigator_html .= '
    ' . "\n"; + } + + return $list_navigator_html; + } + + /** + * replaces %u in given path with current user name + * + * example: + * + * $user_dir = userDir('/var/pma_tmp/%u/'); // '/var/pma_tmp/root/' + * + * + * + * @param string $dir with wildcard for user + * + * @return string per user directory + */ + public static function userDir($dir) + { + // add trailing slash + if (mb_substr($dir, -1) != '/') { + $dir .= '/'; + } + + return str_replace('%u', Core::securePath($GLOBALS['cfg']['Server']['user']), $dir); + } + + /** + * returns html code for db link to default db page + * + * @param string $database database + * + * @return string html link to default db page + */ + public static function getDbLink($database = '') + { + if (strlen((string) $database) === 0) { + if (strlen((string) $GLOBALS['db']) === 0) { + return ''; + } + $database = $GLOBALS['db']; + } else { + $database = self::unescapeMysqlWildcards($database); + } + + return '' . htmlspecialchars($database) . ''; + } + + /** + * Prepare a lightbulb hint explaining a known external bug + * that affects a functionality + * + * @param string $functionality localized message explaining the func. + * @param string $component 'mysql' (eventually, 'php') + * @param string $minimum_version of this component + * @param string $bugref bug reference for this component + * + * @return String + */ + public static function getExternalBug( + $functionality, + $component, + $minimum_version, + $bugref + ) { + $ext_but_html = ''; + if (($component == 'mysql') && ($GLOBALS['dbi']->getVersion() < $minimum_version)) { + $ext_but_html .= self::showHint( + sprintf( + __('The %s functionality is affected by a known bug, see %s'), + $functionality, + Core::linkURL('https://bugs.mysql.com/') . $bugref + ) + ); + } + return $ext_but_html; + } + + /** + * Generates a set of radio HTML fields + * + * @param string $html_field_name the radio HTML field + * @param array $choices the choices values and labels + * @param string $checked_choice the choice to check by default + * @param boolean $line_break whether to add HTML line break after a choice + * @param boolean $escape_label whether to use htmlspecialchars() on label + * @param string $class enclose each choice with a div of this class + * @param string $id_prefix prefix for the id attribute, name will be + * used if this is not supplied + * + * @return string set of html radio fiels + */ + public static function getRadioFields( + $html_field_name, + array $choices, + $checked_choice = '', + $line_break = true, + $escape_label = true, + $class = '', + $id_prefix = '' + ) { + $template = new Template(); + $radio_html = ''; + + foreach ($choices as $choice_value => $choice_label) { + if (! $id_prefix) { + $id_prefix = $html_field_name; + } + $html_field_id = $id_prefix . '_' . $choice_value; + + if ($choice_value == $checked_choice) { + $checked = 1; + } else { + $checked = 0; + } + $radio_html .= $template->render('radio_fields', [ + 'class' => $class, + 'html_field_name' => $html_field_name, + 'html_field_id' => $html_field_id, + 'choice_value' => $choice_value, + 'is_line_break' => $line_break, + 'choice_label' => $choice_label, + 'escape_label' => $escape_label, + 'checked' => $checked, + ]); + } + + return $radio_html; + } + + /** + * Generates and returns an HTML dropdown + * + * @param string $select_name name for the select element + * @param array $choices choices values + * @param string $active_choice the choice to select by default + * @param string $id id of the select element; can be different in + * case the dropdown is present more than once + * on the page + * @param string $class class for the select element + * @param string $placeholder Placeholder for dropdown if nothing else + * is selected + * + * @return string html content + * + * @todo support titles + */ + public static function getDropdown( + $select_name, + array $choices, + $active_choice, + $id, + $class = '', + $placeholder = null + ) { + $template = new Template(); + $resultOptions = []; + $selected = false; + + foreach ($choices as $one_choice_value => $one_choice_label) { + $resultOptions[$one_choice_value]['value'] = $one_choice_value; + $resultOptions[$one_choice_value]['selected'] = false; + + if ($one_choice_value == $active_choice) { + $resultOptions[$one_choice_value]['selected'] = true; + $selected = true; + } + $resultOptions[$one_choice_value]['label'] = $one_choice_label; + } + return $template->render('dropdown', [ + 'select_name' => $select_name, + 'id' => $id, + 'class' => $class, + 'placeholder' => $placeholder, + 'selected' => $selected, + 'result_options' => $resultOptions, + ]); + } + + /** + * Generates a slider effect (jQjuery) + * Takes care of generating the initial
    and the link + * controlling the slider; you have to generate the
    yourself + * after the sliding section. + * + * @param string $id the id of the
    on which to apply the effect + * @param string $message the message to show as a link + * @param string|null $overrideDefault override InitialSlidersState config + * + * @return string html div element + * + */ + public static function getDivForSliderEffect($id = '', $message = '', $overrideDefault = null) + { + $template = new Template(); + return $template->render('div_for_slider_effect', [ + 'id' => $id, + 'initial_sliders_state' => ($overrideDefault != null) ? $overrideDefault : $GLOBALS['cfg']['InitialSlidersState'], + 'message' => $message, + ]); + } + + /** + * Creates an AJAX sliding toggle button + * (or and equivalent form when AJAX is disabled) + * + * @param string $action The URL for the request to be executed + * @param string $select_name The name for the dropdown box + * @param array $options An array of options (see PhpMyAdmin\Rte\Footer) + * @param string $callback A JS snippet to execute when the request is + * successfully processed + * + * @return string HTML code for the toggle button + */ + public static function toggleButton($action, $select_name, array $options, $callback) + { + $template = new Template(); + // Do the logic first + $link = "$action&" . urlencode($select_name) . "="; + $link_on = $link . urlencode($options[1]['value']); + $link_off = $link . urlencode($options[0]['value']); + + if ($options[1]['selected'] == true) { + $state = 'on'; + } elseif ($options[0]['selected'] == true) { + $state = 'off'; + } else { + $state = 'on'; + } + + return $template->render('toggle_button', [ + 'pma_theme_image' => $GLOBALS['pmaThemeImage'], + 'text_dir' => $GLOBALS['text_dir'], + 'link_on' => $link_on, + 'link_off' => $link_off, + 'toggle_on' => $options[1]['label'], + 'toggle_off' => $options[0]['label'], + 'callback' => $callback, + 'state' => $state, + ]); + } + + /** + * Clears cache content which needs to be refreshed on user change. + * + * @return void + */ + public static function clearUserCache() + { + self::cacheUnset('is_superuser'); + self::cacheUnset('is_createuser'); + self::cacheUnset('is_grantuser'); + } + + /** + * Calculates session cache key + * + * @return string + */ + public static function cacheKey() + { + if (isset($GLOBALS['cfg']['Server']['user'])) { + return 'server_' . $GLOBALS['server'] . '_' . $GLOBALS['cfg']['Server']['user']; + } + + return 'server_' . $GLOBALS['server']; + } + + /** + * Verifies if something is cached in the session + * + * @param string $var variable name + * + * @return boolean + */ + public static function cacheExists($var) + { + return isset($_SESSION['cache'][self::cacheKey()][$var]); + } + + /** + * Gets cached information from the session + * + * @param string $var variable name + * @param Closure $callback callback to fetch the value + * + * @return mixed + */ + public static function cacheGet($var, $callback = null) + { + if (self::cacheExists($var)) { + return $_SESSION['cache'][self::cacheKey()][$var]; + } + + if ($callback) { + $val = $callback(); + self::cacheSet($var, $val); + return $val; + } + return null; + } + + /** + * Caches information in the session + * + * @param string $var variable name + * @param mixed $val value + * + * @return void + */ + public static function cacheSet($var, $val = null) + { + $_SESSION['cache'][self::cacheKey()][$var] = $val; + } + + /** + * Removes cached information from the session + * + * @param string $var variable name + * + * @return void + */ + public static function cacheUnset($var) + { + unset($_SESSION['cache'][self::cacheKey()][$var]); + } + + /** + * Converts a bit value to printable format; + * in MySQL a BIT field can be from 1 to 64 bits so we need this + * function because in PHP, decbin() supports only 32 bits + * on 32-bit servers + * + * @param int $value coming from a BIT field + * @param int $length length + * + * @return string the printable value + */ + public static function printableBitValue(int $value, int $length): string + { + // if running on a 64-bit server or the length is safe for decbin() + if (PHP_INT_SIZE == 8 || $length < 33) { + $printable = decbin($value); + } else { + // FIXME: does not work for the leftmost bit of a 64-bit value + $i = 0; + $printable = ''; + while ($value >= pow(2, $i)) { + ++$i; + } + if ($i != 0) { + --$i; + } + + while ($i >= 0) { + if ($value - pow(2, $i) < 0) { + $printable = '0' . $printable; + } else { + $printable = '1' . $printable; + $value -= pow(2, $i); + } + --$i; + } + $printable = strrev($printable); + } + $printable = str_pad($printable, $length, '0', STR_PAD_LEFT); + return $printable; + } + + /** + * Converts a BIT type default value + * for example, b'010' becomes 010 + * + * @param string $bit_default_value value + * + * @return string the converted value + */ + public static function convertBitDefaultValue($bit_default_value) + { + return rtrim(ltrim(htmlspecialchars_decode($bit_default_value, ENT_QUOTES), "b'"), "'"); + } + + /** + * Extracts the various parts from a column spec + * + * @param string $columnspec Column specification + * + * @return array associative array containing type, spec_in_brackets + * and possibly enum_set_values (another array) + */ + public static function extractColumnSpec($columnspec) + { + $first_bracket_pos = mb_strpos($columnspec, '('); + if ($first_bracket_pos) { + $spec_in_brackets = rtrim( + mb_substr( + $columnspec, + $first_bracket_pos + 1, + mb_strrpos($columnspec, ')') - $first_bracket_pos - 1 + ) + ); + // convert to lowercase just to be sure + $type = mb_strtolower( + rtrim(mb_substr($columnspec, 0, $first_bracket_pos)) + ); + } else { + // Split trailing attributes such as unsigned, + // binary, zerofill and get data type name + $type_parts = explode(' ', $columnspec); + $type = mb_strtolower($type_parts[0]); + $spec_in_brackets = ''; + } + + if ('enum' == $type || 'set' == $type) { + // Define our working vars + $enum_set_values = self::parseEnumSetValues($columnspec, false); + $printtype = $type + . '(' . str_replace("','", "', '", $spec_in_brackets) . ')'; + $binary = false; + $unsigned = false; + $zerofill = false; + } else { + $enum_set_values = []; + + /* Create printable type name */ + $printtype = mb_strtolower($columnspec); + + // Strip the "BINARY" attribute, except if we find "BINARY(" because + // this would be a BINARY or VARBINARY column type; + // by the way, a BLOB should not show the BINARY attribute + // because this is not accepted in MySQL syntax. + if (false !== strpos($printtype, "binary") + && ! preg_match('@binary[\(]@', $printtype) + ) { + $printtype = str_replace("binary", '', $printtype); + $binary = true; + } else { + $binary = false; + } + + $printtype = preg_replace( + '@zerofill@', + '', + $printtype, + -1, + $zerofill_cnt + ); + $zerofill = ($zerofill_cnt > 0); + $printtype = preg_replace( + '@unsigned@', + '', + $printtype, + -1, + $unsigned_cnt + ); + $unsigned = ($unsigned_cnt > 0); + $printtype = trim($printtype); + } + + $attribute = ' '; + if ($binary) { + $attribute = 'BINARY'; + } + if ($unsigned) { + $attribute = 'UNSIGNED'; + } + if ($zerofill) { + $attribute = 'UNSIGNED ZEROFILL'; + } + + $can_contain_collation = false; + if (! $binary + && preg_match( + "@^(char|varchar|text|tinytext|mediumtext|longtext|set|enum)@", + $type + ) + ) { + $can_contain_collation = true; + } + + // for the case ENUM('–','“') + $displayed_type = htmlspecialchars($printtype); + if (mb_strlen($printtype) > $GLOBALS['cfg']['LimitChars']) { + $displayed_type = ''; + $displayed_type .= htmlspecialchars( + mb_substr( + $printtype, + 0, + (int) $GLOBALS['cfg']['LimitChars'] + ) . '...' + ); + $displayed_type .= ''; + } + + return [ + 'type' => $type, + 'spec_in_brackets' => $spec_in_brackets, + 'enum_set_values' => $enum_set_values, + 'print_type' => $printtype, + 'binary' => $binary, + 'unsigned' => $unsigned, + 'zerofill' => $zerofill, + 'attribute' => $attribute, + 'can_contain_collation' => $can_contain_collation, + 'displayed_type' => $displayed_type, + ]; + } + + /** + * Verifies if this table's engine supports foreign keys + * + * @param string $engine engine + * + * @return boolean + */ + public static function isForeignKeySupported($engine) + { + $engine = strtoupper((string) $engine); + if (($engine == 'INNODB') || ($engine == 'PBXT')) { + return true; + } elseif ($engine == 'NDBCLUSTER' || $engine == 'NDB') { + $ndbver = strtolower( + $GLOBALS['dbi']->fetchValue("SELECT @@ndb_version_string") + ); + if (substr($ndbver, 0, 4) == 'ndb-') { + $ndbver = substr($ndbver, 4); + } + return version_compare($ndbver, '7.3', '>='); + } + + return false; + } + + /** + * Is Foreign key check enabled? + * + * @return bool + */ + public static function isForeignKeyCheck() + { + if ($GLOBALS['cfg']['DefaultForeignKeyChecks'] === 'enable') { + return true; + } elseif ($GLOBALS['cfg']['DefaultForeignKeyChecks'] === 'disable') { + return false; + } + return ($GLOBALS['dbi']->getVariable('FOREIGN_KEY_CHECKS') == 'ON'); + } + + /** + * Get HTML for Foreign key check checkbox + * + * @return string HTML for checkbox + */ + public static function getFKCheckbox() + { + $template = new Template(); + return $template->render('fk_checkbox', [ + 'checked' => self::isForeignKeyCheck(), + ]); + } + + /** + * Handle foreign key check request + * + * @return bool Default foreign key checks value + */ + public static function handleDisableFKCheckInit() + { + $default_fk_check_value + = $GLOBALS['dbi']->getVariable('FOREIGN_KEY_CHECKS') == 'ON'; + if (isset($_REQUEST['fk_checks'])) { + if (empty($_REQUEST['fk_checks'])) { + // Disable foreign key checks + $GLOBALS['dbi']->setVariable('FOREIGN_KEY_CHECKS', 'OFF'); + } else { + // Enable foreign key checks + $GLOBALS['dbi']->setVariable('FOREIGN_KEY_CHECKS', 'ON'); + } + } // else do nothing, go with default + return $default_fk_check_value; + } + + /** + * Cleanup changes done for foreign key check + * + * @param bool $default_fk_check_value original value for 'FOREIGN_KEY_CHECKS' + * + * @return void + */ + public static function handleDisableFKCheckCleanup($default_fk_check_value) + { + $GLOBALS['dbi']->setVariable( + 'FOREIGN_KEY_CHECKS', + $default_fk_check_value ? 'ON' : 'OFF' + ); + } + + /** + * Converts GIS data to Well Known Text format + * + * @param string $data GIS data + * @param bool $includeSRID Add SRID to the WKT + * + * @return string GIS data in Well Know Text format + */ + public static function asWKT($data, $includeSRID = false) + { + // Convert to WKT format + $hex = bin2hex($data); + $spatialAsText = 'ASTEXT'; + $spatialSrid = 'SRID'; + if ($GLOBALS['dbi']->getVersion() >= 50600) { + $spatialAsText = 'ST_ASTEXT'; + $spatialSrid = 'ST_SRID'; + } + $wktsql = "SELECT $spatialAsText(x'" . $hex . "')"; + if ($includeSRID) { + $wktsql .= ", $spatialSrid(x'" . $hex . "')"; + } + + $wktresult = $GLOBALS['dbi']->tryQuery( + $wktsql + ); + $wktarr = $GLOBALS['dbi']->fetchRow($wktresult, 0); + $wktval = $wktarr[0] ?? null; + + if ($includeSRID) { + $srid = $wktarr[1] ?? null; + $wktval = "'" . $wktval . "'," . $srid; + } + @$GLOBALS['dbi']->freeResult($wktresult); + + return $wktval; + } + + /** + * If the string starts with a \r\n pair (0x0d0a) add an extra \n + * + * @param string $string string + * + * @return string with the chars replaced + */ + public static function duplicateFirstNewline($string) + { + $first_occurence = mb_strpos($string, "\r\n"); + if ($first_occurence === 0) { + $string = "\n" . $string; + } + return $string; + } + + /** + * Get the action word corresponding to a script name + * in order to display it as a title in navigation panel + * + * @param string $target a valid value for $cfg['NavigationTreeDefaultTabTable'], + * $cfg['NavigationTreeDefaultTabTable2'], + * $cfg['DefaultTabTable'] or $cfg['DefaultTabDatabase'] + * + * @return string Title for the $cfg value + */ + public static function getTitleForTarget($target) + { + $mapping = [ + 'structure' => __('Structure'), + 'sql' => __('SQL'), + 'search' => __('Search'), + 'insert' => __('Insert'), + 'browse' => __('Browse'), + 'operations' => __('Operations'), + + // For backward compatiblity + + // Values for $cfg['DefaultTabTable'] + 'tbl_structure.php' => __('Structure'), + 'tbl_sql.php' => __('SQL'), + 'tbl_select.php' => __('Search'), + 'tbl_change.php' => __('Insert'), + 'sql.php' => __('Browse'), + // Values for $cfg['DefaultTabDatabase'] + 'db_structure.php' => __('Structure'), + 'db_sql.php' => __('SQL'), + 'db_search.php' => __('Search'), + 'db_operations.php' => __('Operations'), + ]; + return isset($mapping[$target]) ? $mapping[$target] : false; + } + + /** + * Get the script name corresponding to a plain English config word + * in order to append in links on navigation and main panel + * + * @param string $target a valid value for + * $cfg['NavigationTreeDefaultTabTable'], + * $cfg['NavigationTreeDefaultTabTable2'], + * $cfg['DefaultTabTable'], $cfg['DefaultTabDatabase'] or + * $cfg['DefaultTabServer'] + * @param string $location one out of 'server', 'table', 'database' + * + * @return string script name corresponding to the config word + */ + public static function getScriptNameForOption($target, $location) + { + if ($location == 'server') { + // Values for $cfg['DefaultTabServer'] + switch ($target) { + case 'welcome': + return 'index.php'; + case 'databases': + return 'server_databases.php'; + case 'status': + return 'server_status.php'; + case 'variables': + return 'server_variables.php'; + case 'privileges': + return 'server_privileges.php'; + } + } elseif ($location == 'database') { + // Values for $cfg['DefaultTabDatabase'] + switch ($target) { + case 'structure': + return 'db_structure.php'; + case 'sql': + return 'db_sql.php'; + case 'search': + return 'db_search.php'; + case 'operations': + return 'db_operations.php'; + } + } elseif ($location == 'table') { + // Values for $cfg['DefaultTabTable'], + // $cfg['NavigationTreeDefaultTabTable'] and + // $cfg['NavigationTreeDefaultTabTable2'] + switch ($target) { + case 'structure': + return 'tbl_structure.php'; + case 'sql': + return 'tbl_sql.php'; + case 'search': + return 'tbl_select.php'; + case 'insert': + return 'tbl_change.php'; + case 'browse': + return 'sql.php'; + } + } + + return $target; + } + + /** + * Formats user string, expanding @VARIABLES@, accepting strftime format + * string. + * + * @param string $string Text where to do expansion. + * @param array|string $escape Function to call for escaping variable values. + * Can also be an array of: + * - the escape method name + * - the class that contains the method + * - location of the class (for inclusion) + * @param array $updates Array with overrides for default parameters + * (obtained from GLOBALS). + * + * @return string + */ + public static function expandUserString( + $string, + $escape = null, + array $updates = [] + ) { + /* Content */ + $vars = []; + $vars['http_host'] = Core::getenv('HTTP_HOST'); + $vars['server_name'] = $GLOBALS['cfg']['Server']['host']; + $vars['server_verbose'] = $GLOBALS['cfg']['Server']['verbose']; + + if (empty($GLOBALS['cfg']['Server']['verbose'])) { + $vars['server_verbose_or_name'] = $GLOBALS['cfg']['Server']['host']; + } else { + $vars['server_verbose_or_name'] = $GLOBALS['cfg']['Server']['verbose']; + } + + $vars['database'] = $GLOBALS['db']; + $vars['table'] = $GLOBALS['table']; + $vars['phpmyadmin_version'] = 'phpMyAdmin ' . PMA_VERSION; + + /* Update forced variables */ + foreach ($updates as $key => $val) { + $vars[$key] = $val; + } + + /* Replacement mapping */ + /* + * The __VAR__ ones are for backward compatibility, because user + * might still have it in cookies. + */ + $replace = [ + '@HTTP_HOST@' => $vars['http_host'], + '@SERVER@' => $vars['server_name'], + '__SERVER__' => $vars['server_name'], + '@VERBOSE@' => $vars['server_verbose'], + '@VSERVER@' => $vars['server_verbose_or_name'], + '@DATABASE@' => $vars['database'], + '__DB__' => $vars['database'], + '@TABLE@' => $vars['table'], + '__TABLE__' => $vars['table'], + '@PHPMYADMIN@' => $vars['phpmyadmin_version'], + ]; + + /* Optional escaping */ + if ($escape !== null) { + if (is_array($escape)) { + $escape_class = new $escape[1]; + $escape_method = $escape[0]; + } + foreach ($replace as $key => $val) { + if (is_array($escape)) { + $replace[$key] = $escape_class->$escape_method($val); + } else { + $replace[$key] = ($escape == 'backquote') + ? self::$escape($val) + : $escape($val); + } + } + } + + /* Backward compatibility in 3.5.x */ + if (mb_strpos($string, '@FIELDS@') !== false) { + $string = strtr($string, ['@FIELDS@' => '@COLUMNS@']); + } + + /* Fetch columns list if required */ + if (mb_strpos($string, '@COLUMNS@') !== false) { + $columns_list = $GLOBALS['dbi']->getColumns( + $GLOBALS['db'], + $GLOBALS['table'] + ); + + // sometimes the table no longer exists at this point + if ($columns_list !== null) { + $column_names = []; + foreach ($columns_list as $column) { + if ($escape !== null) { + $column_names[] = self::$escape($column['Field']); + } else { + $column_names[] = $column['Field']; + } + } + $replace['@COLUMNS@'] = implode(',', $column_names); + } else { + $replace['@COLUMNS@'] = '*'; + } + } + + /* Do the replacement */ + return strtr((string) strftime($string), $replace); + } + + /** + * Prepare the form used to browse anywhere on the local server for a file to + * import + * + * @param string $max_upload_size maximum upload size + * + * @return String + */ + public static function getBrowseUploadFileBlock($max_upload_size) + { + $block_html = ''; + + if ($GLOBALS['is_upload'] && ! empty($GLOBALS['cfg']['UploadDir'])) { + $block_html .= '